diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index bfabd1935..cc9d54e12 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -3,6 +3,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router + W 💣 github.com/github/certstore from tailscale.com/control/controlclient github.com/go-multierror/multierror from tailscale.com/wgengine/router+ W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+ W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet @@ -19,6 +20,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ L github.com/mdlayher/sdnotify from tailscale.com/util/systemd + W github.com/pkg/errors from github.com/github/certstore 💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+ 💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+ 💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device @@ -132,7 +134,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/systemd from tailscale.com/control/controlclient+ - tailscale.com/util/winutil from tailscale.com/logpolicy + tailscale.com/util/winutil from tailscale.com/logpolicy+ tailscale.com/version from tailscale.com/cmd/tailscaled+ tailscale.com/version/distro from tailscale.com/control/controlclient+ tailscale.com/wgengine from tailscale.com/cmd/tailscaled+ diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 5254e5d17..57631b72e 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -351,12 +351,14 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi err = errors.New("hostinfo: BackendLogID missing") return regen, url, err } + now := time.Now().Round(time.Second) request := tailcfg.RegisterRequest{ Version: 1, OldNodeKey: tailcfg.NodeKey(oldNodeKey), NodeKey: tailcfg.NodeKey(tryingNewKey.Public()), Hostinfo: hostinfo, Followup: url, + Timestamp: &now, } c.logf("RegisterReq: onode=%v node=%v fup=%v", request.OldNodeKey.ShortString(), @@ -365,6 +367,20 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi request.Auth.Provider = persist.Provider request.Auth.LoginName = persist.LoginName request.Auth.AuthKey = authKey + err = signRegisterRequest(&request, c.serverURL, c.serverKey, c.machinePrivKey.Public()) + if err != nil { + // If signing failed, clear all related fields + request.SignatureType = tailcfg.SignatureNone + request.Timestamp = nil + request.DeviceCert = nil + request.Signature = nil + + // Don't log the common error types. Signatures are not usually enabled, + // so these are expected. + if err != errCertificateNotConfigured && err != errNoCertStore { + c.logf("RegisterReq sign error: %v", err) + } + } bodyData, err := encode(request, &serverKey, &c.machinePrivKey) if err != nil { return regen, url, err diff --git a/control/controlclient/sign.go b/control/controlclient/sign.go new file mode 100644 index 000000000..83b35f6f7 --- /dev/null +++ b/control/controlclient/sign.go @@ -0,0 +1,31 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package controlclient + +import ( + "crypto" + "errors" + "fmt" + "time" + + "tailscale.com/types/wgkey" +) + +var ( + errNoCertStore = errors.New("no certificate store") + errCertificateNotConfigured = errors.New("no certificate subject configured") +) + +// HashRegisterRequest generates the hash required sign or verify a +// tailcfg.RegisterRequest with tailcfg.SignatureV1. +func HashRegisterRequest(ts time.Time, serverURL string, deviceCert []byte, serverPubKey, machinePubKey wgkey.Key) []byte { + h := crypto.SHA256.New() + + // hash.Hash.Write never returns an error, so we don't check for one here. + fmt.Fprintf(h, "%s%s%s%s%s", + ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey) + + return h.Sum(nil) +} diff --git a/control/controlclient/sign_supported.go b/control/controlclient/sign_supported.go new file mode 100644 index 000000000..9eadcafd0 --- /dev/null +++ b/control/controlclient/sign_supported.go @@ -0,0 +1,160 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows,cgo + +// darwin,cgo is also supported by certstore but machineCertificateSubject will +// need to be loaded by a different mechanism, so this is not currently enabled +// on darwin. + +package controlclient + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "sync" + + "github.com/github/certstore" + "tailscale.com/tailcfg" + "tailscale.com/types/wgkey" + "tailscale.com/util/winutil" +) + +var getMachineCertificateSubjectOnce struct { + sync.Once + v string // Subject of machine certificate to search for +} + +// getMachineCertificateSubject returns the exact name of a Subject that needs +// to be present in an identity's certificate chain to sign a RegisterRequest, +// formatted as per pkix.Name.String(). The Subject may be that of the identity +// itself, an intermediate CA or the root CA. +// +// If getMachineCertificateSubject() returns "" then no lookup will occur and +// each RegisterRequest will be unsigned. +// +// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA" +func getMachineCertificateSubject() string { + getMachineCertificateSubjectOnce.Do(func() { + getMachineCertificateSubjectOnce.v = winutil.GetRegString("MachineCertificateSubject", "") + }) + + return getMachineCertificateSubjectOnce.v +} + +var ( + errNoMatch = errors.New("no matching certificate") + errBadRequest = errors.New("malformed request") +) + +// findIdentity locates an identity from the Windows or Darwin certificate +// store. It returns the first certificate with a matching Subject anywhere in +// its certificate chain, so it is possible to search for the leaf certificate, +// intermediate CA or root CA. If err is nil then the returned identity will +// never be nil (if no identity is found, the error errNoMatch will be +// returned). If an identity is returned then its certificate chain is also +// returned. +func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x509.Certificate, error) { + ids, err := st.Identities() + if err != nil { + return nil, nil, err + } + + var selected certstore.Identity + var chain []*x509.Certificate + + for _, id := range ids { + chain, err = id.CertificateChain() + if err != nil { + continue + } + + if chain[0].PublicKeyAlgorithm != x509.RSA { + continue + } + + for _, c := range chain { + if c.Subject.String() == subject { + selected = id + break + } + } + } + + for _, id := range ids { + if id != selected { + id.Close() + } + } + + if selected == nil { + return nil, nil, errNoMatch + } + + return selected, chain, nil +} + +// signRegisterRequest looks for a suitable machine identity from the local +// system certificate store, and if one is found, signs the RegisterRequest +// using that identity's public key. In addition to the signature, the full +// certificate chain is included so that the control server can validate the +// certificate from a copy of the root CA's certificate. +func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) (err error) { + defer func() { + if err != nil { + err = fmt.Errorf("signRegisterRequest: %w", err) + } + }() + + if req.Timestamp == nil { + return errBadRequest + } + + machineCertificateSubject := getMachineCertificateSubject() + if machineCertificateSubject == "" { + return errCertificateNotConfigured + } + + st, err := certstore.Open(certstore.System) + if err != nil { + return fmt.Errorf("open cert store: %w", err) + } + defer st.Close() + + id, chain, err := findIdentity(machineCertificateSubject, st) + if err != nil { + return fmt.Errorf("find identity: %w", err) + } + defer id.Close() + + signer, err := id.Signer() + if err != nil { + return fmt.Errorf("create signer: %w", err) + } + + cl := 0 + for _, c := range chain { + cl += len(c.Raw) + } + req.DeviceCert = make([]byte, 0, cl) + for _, c := range chain { + req.DeviceCert = append(req.DeviceCert, c.Raw...) + } + + h := HashRegisterRequest(req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey) + + req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + Hash: crypto.SHA256, + }) + if err != nil { + return fmt.Errorf("sign: %w", err) + } + req.SignatureType = tailcfg.SignatureV1 + + return nil +} diff --git a/control/controlclient/sign_unsupported.go b/control/controlclient/sign_unsupported.go new file mode 100644 index 000000000..e20ced316 --- /dev/null +++ b/control/controlclient/sign_unsupported.go @@ -0,0 +1,17 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !windows !cgo + +package controlclient + +import ( + "tailscale.com/tailcfg" + "tailscale.com/types/wgkey" +) + +// signRegisterRequest on non-supported platforms always returns errNoCertStore. +func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) error { + return errNoCertStore +} diff --git a/go.mod b/go.mod index 05158381e..479431041 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/coreos/go-iptables v0.4.5 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/github/certstore v0.1.0 github.com/gliderlabs/ssh v0.2.2 github.com/go-multierror/multierror v1.0.2 github.com/go-ole/go-ole v1.2.4 @@ -42,3 +43,5 @@ require ( inet.af/peercred v0.0.0-20210302202138-56e694897155 rsc.io/goversion v1.2.0 ) + +replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2 diff --git a/go.sum b/go.sum index 5562d64b6..1d2d82a4f 100644 --- a/go.sum +++ b/go.sum @@ -17,12 +17,16 @@ github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRY github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2 h1:TGPWAij+nY2FB7TlyUTqTmYvXJon/AZAfRMYc/76K80= +github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2/go.mod h1:Sgb3YVYOB2iCO06NJ6We5gjXe7uxxM3zPYoEXjuTKno= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo= diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index cd1d159d9..30119b671 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -539,6 +539,61 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool { return reflect.DeepEqual(h, h2) } +// SignatureType specifies a scheme for signing RegisterRequest messages. It +// specifies the crypto algorithms to use, the contents of what is signed, and +// any other relevant details. Historically, requests were unsigned so the zero +// value is SignatureNone. +type SignatureType int + +const ( + // SignatureNone indicates that there is no signature, no Timestamp is + // required (but may be specified if desired), and both DeviceCert and + // Signature should be empty. + SignatureNone = SignatureType(iota) + // SignatureUnknown represents an unknown signature scheme, which should + // be considered an error if seen. + SignatureUnknown + // SignatureV1 is computed as RSA-PSS-Sign(privateKeyForDeviceCert, + // SHA256(Timestamp || ServerIdentity || DeviceCert || ServerPubKey || + // MachinePubKey)). The PSS salt length is equal to hash length + // (rsa.PSSSaltLengthEqualsHash). Device cert is required. + SignatureV1 +) + +func (st SignatureType) MarshalText() ([]byte, error) { + return []byte(st.String()), nil +} + +func (st *SignatureType) UnmarshalText(b []byte) error { + switch string(b) { + case "signature-none": + *st = SignatureNone + case "signature-v1": + *st = SignatureV1 + default: + var val int + if _, err := fmt.Sscanf(string(b), "signature-unknown(%d)", &val); err != nil { + *st = SignatureType(val) + } else { + *st = SignatureUnknown + } + } + return nil +} + +func (st SignatureType) String() string { + switch st { + case SignatureNone: + return "signature-none" + case SignatureUnknown: + return "signature-unknown" + case SignatureV1: + return "signature-v1" + default: + return fmt.Sprintf("signature-unknown(%d)", int(st)) + } +} + // RegisterRequest is sent by a client to register the key for a node. // It is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box, // using the local machine key, and sent to: @@ -558,6 +613,13 @@ type RegisterRequest struct { Expiry time.Time // requested key expiry, server policy may override Followup string // response waits until AuthURL is visited Hostinfo *Hostinfo + + // The following fields are not used for SignatureNone and are required for + // SignatureV1: + SignatureType SignatureType `json:",omitempty"` + Timestamp *time.Time `json:",omitempty"` // creation time of request to prevent replay + DeviceCert []byte `json:",omitempty"` // X.509 certificate for client device + Signature []byte `json:",omitempty"` // as described by SignatureType } // Clone makes a deep copy of RegisterRequest. @@ -574,6 +636,8 @@ func (req *RegisterRequest) Clone() *RegisterRequest { tok := *res.Auth.Oauth2Token res.Auth.Oauth2Token = &tok } + res.DeviceCert = append(res.DeviceCert[:0:0], res.DeviceCert...) + res.Signature = append(res.Signature[:0:0], res.Signature...) return res }