diff options
Diffstat (limited to 'control/controlclient')
| -rw-r--r-- | control/controlclient/auto.go | 11 | ||||
| -rw-r--r-- | control/controlclient/direct.go | 23 | ||||
| -rw-r--r-- | control/controlclient/sign.go | 31 | ||||
| -rw-r--r-- | control/controlclient/sign_supported.go | 160 | ||||
| -rw-r--r-- | control/controlclient/sign_unsupported.go | 17 |
5 files changed, 232 insertions, 10 deletions
diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index 2549bd9af..c731666e1 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -17,7 +17,6 @@ import ( "sync" "time" - "golang.org/x/oauth2" "tailscale.com/health" "tailscale.com/logtail/backoff" "tailscale.com/tailcfg" @@ -102,10 +101,10 @@ func (s Status) String() string { type LoginGoal struct { _ structs.Incomparable - wantLoggedIn bool // true if we *want* to be logged in - token *oauth2.Token // oauth token to use when logging in - flags LoginFlags // flags to use when logging in - url string // auth url that needs to be visited + wantLoggedIn bool // true if we *want* to be logged in + token *tailcfg.Oauth2Token // oauth token to use when logging in + flags LoginFlags // flags to use when logging in + url string // auth url that needs to be visited } // Client connects to a tailcontrol server for a node. @@ -668,7 +667,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ c.mu.Unlock() } -func (c *Client) Login(t *oauth2.Token, flags LoginFlags) { +func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) { c.logf("client.Login(%v, %v)", t != nil, flags) c.mu.Lock() diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 2977ece67..57631b72e 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -31,7 +31,6 @@ import ( "time" "golang.org/x/crypto/nacl/box" - "golang.org/x/oauth2" "inet.af/netaddr" "tailscale.com/health" "tailscale.com/log/logheap" @@ -266,7 +265,7 @@ func (c *Direct) TryLogout(ctx context.Context) error { return nil } -func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) { +func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) { c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags) return c.doLoginOrRegen(ctx, t, flags, false, "") } @@ -276,7 +275,7 @@ func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newUrl string, e return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url) } -func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) { +func (c *Direct) doLoginOrRegen(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) { mustregen, url, err := c.doLogin(ctx, t, flags, regen, url) if err != nil { return url, err @@ -288,7 +287,7 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags Logi return url, err } -func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) { +func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) { c.mu.Lock() persist := c.persist tryingNewKey := c.tryingNewKey @@ -352,12 +351,14 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, 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(), @@ -366,6 +367,20 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, 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 +} |
