summaryrefslogtreecommitdiffhomepage
path: root/control/controlclient
diff options
context:
space:
mode:
Diffstat (limited to 'control/controlclient')
-rw-r--r--control/controlclient/auto.go11
-rw-r--r--control/controlclient/direct.go23
-rw-r--r--control/controlclient/sign.go31
-rw-r--r--control/controlclient/sign_supported.go160
-rw-r--r--control/controlclient/sign_unsupported.go17
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
+}