From 4001d0bf2567832ef729b9b39dded95a1fa04b72 Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Mon, 1 Aug 2022 15:46:41 -0700 Subject: [PATCH] assorted: plumb tka initialization & network-lock key into tailscaled - A network-lock key is generated if it doesn't already exist, and stored in the StateStore. The public component is communicated to control during registration. - If TKA state exists on the filesystem, a tailnet key authority is initialized (but nothing is done with it for now). Signed-off-by: Tom DNetto --- cmd/tailscaled/depaware.txt | 2 +- control/controlclient/direct.go | 9 +++++ ipn/ipnlocal/local.go | 69 +++++++++++++++++++++++++++++++++ ipn/ipnserver/server.go | 20 ++++++++++ ipn/store.go | 4 ++ tailcfg/tailcfg.go | 1 + tka/tailchonk.go | 13 +++++++ types/key/nl.go | 7 ++++ 8 files changed, 124 insertions(+), 1 deletion(-) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 3c3d0d9d1..9dabcd918 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -244,7 +244,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/syncs from tailscale.com/control/controlknobs+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh - tailscale.com/tka from tailscale.com/types/key + tailscale.com/tka from tailscale.com/types/key+ W tailscale.com/tsconst from tailscale.com/net/interfaces tailscale.com/tstime from tailscale.com/wgengine/magicsock 💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+ diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 39387549a..96e008de5 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -67,6 +67,7 @@ type Direct struct { linkMon *monitor.Mon // or nil discoPubKey key.DiscoPublic getMachinePrivKey func() (key.MachinePrivate, error) + getNLPublicKey func() (key.NLPublic, error) debugFlags []string keepSharerAndUserSplit bool skipIPForwardingCheck bool @@ -107,6 +108,7 @@ type Options struct { LinkMonitor *monitor.Mon // optional link monitor PopBrowserURL func(url string) // optional func to open browser Dialer *tsdial.Dialer // non-nil + GetNLPublicKey func() (key.NLPublic, error) // Status is called when there's a change in status. Status func(Status) @@ -190,6 +192,7 @@ func NewDirect(opts Options) (*Direct, error) { c := &Direct{ httpc: httpc, getMachinePrivKey: opts.GetMachinePrivateKey, + getNLPublicKey: opts.GetNLPublicKey, serverURL: opts.ServerURL, timeNow: opts.TimeNow, logf: opts.Logf, @@ -424,6 +427,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new oldNodeKey = persist.OldPrivateNodeKey.Public() } + nlPub, err := c.getNLPublicKey() + if err != nil { + return false, "", fmt.Errorf("get nl key: %v", err) + } + if tryingNewKey.IsZero() { if opt.Logout { return false, "", errors.New("no nodekey to log out") @@ -439,6 +447,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new Version: 1, OldNodeKey: oldNodeKey, NodeKey: tryingNewKey.Public(), + NLKey: nlPub, Hostinfo: hi, Followup: opt.URL, Timestamp: &now, diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index f2b336bec..0d85fa26e 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -42,6 +42,7 @@ import ( "tailscale.com/portlist" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tka" "tailscale.com/types/dnstype" "tailscale.com/types/empty" "tailscale.com/types/key" @@ -146,6 +147,8 @@ type LocalBackend struct { prefs *ipn.Prefs inServerMode bool machinePrivKey key.MachinePrivate + nlPrivKey key.NLPrivate + tka *tka.Authority state ipn.State capFileSharing bool // whether netMap contains the file sharing capability // hostinfo is mutated in-place while mu is held. @@ -997,6 +1000,9 @@ func (b *LocalBackend) Start(opts ipn.Options) error { return fmt.Errorf("initMachineKeyLocked: %w", err) } } + if err := b.initNLKeyLocked(); err != nil { + return fmt.Errorf("initNLKeyLocked: %w", err) + } loggedOut := b.prefs.LoggedOut @@ -1056,6 +1062,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error { // but it won't take effect until the next Start(). cc, err := b.getNewControlClientFunc()(controlclient.Options{ GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(), + GetNLPublicKey: b.createGetNLPublicKeyFunc(), Logf: logger.WithPrefix(b.logf, "control: "), Persist: *persistv, ServerURL: b.serverURL, @@ -1515,6 +1522,21 @@ func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePriva } } +func (b *LocalBackend) createGetNLPublicKeyFunc() func() (key.NLPublic, error) { + var cache atomic.Value + return func() (key.NLPublic, error) { + b.mu.Lock() + defer b.mu.Unlock() + if v, ok := cache.Load().(key.NLPublic); ok { + return v, nil + } + + pub := b.nlPrivKey.Public() + cache.Store(pub) + return pub, nil + } +} + // initMachineKeyLocked is called to initialize b.machinePrivKey. // // b.prefs must already be initialized. @@ -1573,6 +1595,45 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) { return nil } +// initNLKeyLocked is called to initialize b.nlPrivKey. +// +// b.prefs must already be initialized. +// b.stateKey should be set too, but just for nicer log messages. +// b.mu must be held. +func (b *LocalBackend) initNLKeyLocked() (err error) { + if !b.nlPrivKey.IsZero() { + // Already set. + return nil + } + + keyText, err := b.store.ReadState(ipn.NLKeyStateKey) + if err == nil { + if err := b.nlPrivKey.UnmarshalText(keyText); err != nil { + return fmt.Errorf("invalid key in %s key of %v: %w", ipn.NLKeyStateKey, b.store, err) + } + if b.nlPrivKey.IsZero() { + return fmt.Errorf("invalid zero key stored in %v key of %v", ipn.NLKeyStateKey, b.store) + } + return nil + } + if err != ipn.ErrStateNotExist { + return fmt.Errorf("error reading %v key of %v: %w", ipn.NLKeyStateKey, b.store, err) + } + + // If we didn't find one already on disk, generate a new one. + b.logf("generating new network-lock key") + b.nlPrivKey = key.NewNLPrivate() + + keyText, _ = b.nlPrivKey.MarshalText() + if err := b.store.WriteState(ipn.NLKeyStateKey, keyText); err != nil { + b.logf("error writing network-lock key to store: %v", err) + return err + } + + b.logf("network-lock key written to store") + return nil +} + // writeServerModeStartState stores the ServerModeStartKey value based on the current // user and prefs. If userID is blank or prefs is blank, no work is done. // @@ -2437,6 +2498,14 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log return dcfg } +// SetTailnetKeyAuthority sets the key authority which should be +// used for locked tailnets. +// +// It should only be called before the LocalBackend is used. +func (b *LocalBackend) SetTailnetKeyAuthority(a *tka.Authority) { + b.tka = a +} + // SetVarRoot sets the root directory of Tailscale's writable // storage area . (e.g. "/var/lib/tailscale") // diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 42cccbeaa..42fe8c28e 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -42,6 +42,7 @@ import ( "tailscale.com/net/tsdial" "tailscale.com/safesocket" "tailscale.com/smallzstd" + "tailscale.com/tka" "tailscale.com/types/logger" "tailscale.com/util/groupmember" "tailscale.com/util/pidowner" @@ -770,6 +771,25 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi return smallzstd.NewDecoder(nil) }) + if root := b.TailscaleVarRoot(); root != "" { + chonkDir := filepath.Join(root, "chonk") + if _, err := os.Stat(chonkDir); err == nil { + // The directory exists, which means network-lock has been initialized. + chonk, err := tka.ChonkDir(chonkDir) + if err != nil { + return nil, fmt.Errorf("opening tailchonk: %v", err) + } + authority, err := tka.Open(chonk) + if err != nil { + return nil, fmt.Errorf("initializing tka: %v", err) + } + b.SetTailnetKeyAuthority(authority) + logf("tka initialized at head %x", authority.Head()) + } + } else { + logf("network-lock unavailable; no state directory") + } + dg := distro.Get() switch dg { case distro.Synology, distro.TrueNAS, distro.QNAP: diff --git a/ipn/store.go b/ipn/store.go index 8dbc2de69..46df80d09 100644 --- a/ipn/store.go +++ b/ipn/store.go @@ -34,6 +34,10 @@ const ( // the server should start with the Prefs JSON loaded from // StateKey "user-1234". ServerModeStartKey = StateKey("server-mode-start-key") + + // NLKeyStateKey is the key under which we store the nodes' + // network-lock node key, in its key.NLPrivate.MarshalText representation. + NLKeyStateKey = StateKey("_nl-node-key") ) // StateStore persists state, and produces it back on request. diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 04b1117a9..f0a0241ee 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -763,6 +763,7 @@ type RegisterRequest struct { NodeKey key.NodePublic OldNodeKey key.NodePublic + NLKey key.NLPublic Auth struct { _ structs.Incomparable // One of Provider/LoginName, Oauth2Token, or AuthKey is set. diff --git a/tka/tailchonk.go b/tka/tailchonk.go index dfcc2d080..e62e1d520 100644 --- a/tka/tailchonk.go +++ b/tka/tailchonk.go @@ -174,6 +174,19 @@ type FS struct { mu sync.RWMutex } +// ChonkDir returns an implementation of Chonk which uses the +// given directory to store TKA state. +func ChonkDir(dir string) (*FS, error) { + stat, err := os.Stat(dir) + if err != nil { + return nil, err + } + if !stat.IsDir() { + return nil, fmt.Errorf("chonk directory %q is a file", dir) + } + return &FS{base: dir}, nil +} + // fsHashInfo describes how information about an AUMHash is represented // on disk. // diff --git a/types/key/nl.go b/types/key/nl.go index 6503ca66f..ceaf244e2 100644 --- a/types/key/nl.go +++ b/types/key/nl.go @@ -6,6 +6,7 @@ package key import ( "crypto/ed25519" + "crypto/subtle" "go4.org/mem" "tailscale.com/tka" @@ -29,6 +30,12 @@ type NLPrivate struct { k [ed25519.PrivateKeySize]byte } +// IsZero reports whether k is the zero value. +func (k NLPrivate) IsZero() bool { + empty := NLPrivate{} + return subtle.ConstantTimeCompare(k.k[:], empty.k[:]) == 1 +} + // NewNLPrivate creates and returns a new network-lock key. func NewNLPrivate() NLPrivate { // ed25519.GenerateKey 'clamps' the key, not that it