From 235309adc4dd5bcfed2ddf09464de67432b37631 Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Mon, 14 Nov 2022 17:29:49 +0500 Subject: [PATCH] all: store NL keys per profile This moves the NetworkLock key from a dedicated StateKey to be part of the persist.Persist struct. This struct is stored as part for ipn.Prefs and is also the place where we store the NodeKey. It also moves the ChonkDir from "/tka" to "/tka-profile/". The rename was intentional to be able to delete the "/tka" dir if it exists. This means that we will have a unique key per profile, and a unique directory per profile. Note: `tailscale logout` will delete the entire profile, including any keys. It currently does not delete the ChonkDir. Signed-off-by: Maisem Ali --- cmd/tailscale/cli/network-lock.go | 12 +-- control/controlclient/direct.go | 25 ++---- ipn/ipnlocal/local.go | 121 ++++++++++++------------------ ipn/ipnlocal/network-lock.go | 62 +++++++++++---- ipn/ipnlocal/network-lock_test.go | 94 ++++++++++++++--------- ipn/ipnlocal/profiles.go | 4 +- ipn/ipnserver/server.go | 18 ----- ipn/ipnstate/ipnstate.go | 1 + types/key/nl.go | 5 ++ types/persist/persist.go | 7 +- types/persist/persist_clone.go | 1 + types/persist/persist_test.go | 13 +++- types/persist/persist_view.go | 2 + 13 files changed, 198 insertions(+), 167 deletions(-) diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go index e70491477..8888d86ff 100644 --- a/cmd/tailscale/cli/network-lock.go +++ b/cmd/tailscale/cli/network-lock.go @@ -93,12 +93,14 @@ func runNetworkLockStatus(ctx context.Context, args []string) error { fmt.Println() } - p, err := st.PublicKey.MarshalText() - if err != nil { - return err + if !st.PublicKey.IsZero() { + p, err := st.PublicKey.MarshalText() + if err != nil { + return err + } + fmt.Printf("This node's public-key: %s\n", p) + fmt.Println() } - fmt.Printf("This node's public-key: %s\n", p) - fmt.Println() if st.Enabled && len(st.TrustedKeys) > 0 { fmt.Println("Keys trusted to make changes to network-lock:") diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index d9ce395ac..b889a8fe3 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -70,7 +70,6 @@ type Direct struct { linkMon *monitor.Mon // or nil discoPubKey key.DiscoPublic getMachinePrivKey func() (key.MachinePrivate, error) - getNLPrivateKey func() (key.NLPrivate, error) // or nil debugFlags []string keepSharerAndUserSplit bool skipIPForwardingCheck bool @@ -118,10 +117,6 @@ type Options struct { Dialer *tsdial.Dialer // non-nil C2NHandler http.Handler // or nil - // GetNLPrivateKey specifies an optional function to use - // Network Lock. If nil, it's not used. - GetNLPrivateKey func() (key.NLPrivate, error) - // Status is called when there's a change in status. Status func(Status) @@ -232,7 +227,6 @@ func NewDirect(opts Options) (*Direct, error) { c := &Direct{ httpc: httpc, getMachinePrivKey: opts.GetMachinePrivateKey, - getNLPrivateKey: opts.GetNLPrivateKey, serverURL: opts.ServerURL, timeNow: opts.TimeNow, logf: opts.Logf, @@ -494,6 +488,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new if !persist.OldPrivateNodeKey.IsZero() { oldNodeKey = persist.OldPrivateNodeKey.Public() } + if persist.NetworkLockKey.IsZero() { + persist.NetworkLockKey = key.NewNLPrivate() + } + nlPub := persist.NetworkLockKey.Public() if tryingNewKey.IsZero() { if opt.Logout { @@ -502,19 +500,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new log.Fatalf("tryingNewKey is empty, give up") } - var nlPub key.NLPublic var nodeKeySignature tkatype.MarshaledSignature - if c.getNLPrivateKey != nil { - priv, err := c.getNLPrivateKey() - if err != nil { - return false, "", nil, fmt.Errorf("get nl key: %v", err) - } - nlPub = priv.Public() - - if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != nil { - if nodeKeySignature, err = resignNKS(priv, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil { - c.logf("Failed re-signing node-key signature: %v", err) - } + if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != nil { + if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil { + c.logf("Failed re-signing node-key signature: %v", err) } } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 9e1e7db6f..0dfe2ccb2 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -159,7 +159,6 @@ type LocalBackend struct { ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto inServerMode bool machinePrivKey key.MachinePrivate - nlPrivKey key.NLPrivate tka *tkaState state ipn.State capFileSharing bool // whether netMap contains the file sharing capability @@ -832,6 +831,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { if !prefs.Persist.View().Equals(*st.Persist) { prefsChanged = true prefs.Persist = st.Persist.AsStruct() + if err := b.initTKALocked(); err != nil { + b.logf("initTKALocked: %v", err) + } } } if st.URL != "" { @@ -1174,9 +1176,6 @@ 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 := prefs.LoggedOut() @@ -1232,7 +1231,6 @@ 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(), - GetNLPrivateKey: b.createGetNLPrivateKeyFunc(), Logf: logger.WithPrefix(b.logf, "control: "), Persist: *persistv, ServerURL: b.serverURL, @@ -1759,21 +1757,6 @@ func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePriva } } -func (b *LocalBackend) createGetNLPrivateKeyFunc() func() (key.NLPrivate, error) { - var cache syncs.AtomicValue[key.NLPrivate] - return func() (key.NLPrivate, error) { - b.mu.Lock() - defer b.mu.Unlock() - if v, ok := cache.LoadOk(); ok { - return v, nil - } - - priv := b.nlPrivKey - cache.Store(priv) - return priv, nil - } -} - // initMachineKeyLocked is called to initialize b.machinePrivKey. // // b.prefs must already be initialized. @@ -1827,46 +1810,6 @@ 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 -} - // migrateStateLocked migrates state from the frontend to the backend. // It is a no-op if prefs is nil // b.mu must be held. @@ -2705,17 +2648,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger. 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, storage *tka.FS) { - b.tka = &tkaState{ - authority: a, - storage: storage, - } -} - // SetVarRoot sets the root directory of Tailscale's writable // storage area . (e.g. "/var/lib/tailscale") // @@ -4053,11 +3985,58 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error { return b.resetForProfileChangeLockedOnEntry() } +func (b *LocalBackend) initTKALocked() error { + cp := b.pm.CurrentProfile() + if cp.ID == "" { + b.tka = nil + return nil + } + if b.tka != nil { + if b.tka.profile == cp.ID { + // Already initialized. + return nil + } + // As we're switching profiles, we need to reset the TKA to nil. + b.tka = nil + } + root := b.TailscaleVarRoot() + if root == "" { + b.tka = nil + b.logf("network-lock unavailable; no state directory") + return nil + } + + chonkDir := b.chonkPathLocked() + if _, err := os.Stat(chonkDir); err == nil { + // The directory exists, which means network-lock has been initialized. + storage, err := tka.ChonkDir(chonkDir) + if err != nil { + return fmt.Errorf("opening tailchonk: %v", err) + } + authority, err := tka.Open(storage) + if err != nil { + return fmt.Errorf("initializing tka: %v", err) + } + + b.tka = &tkaState{ + profile: cp.ID, + authority: authority, + storage: storage, + } + b.logf("tka initialized at head %x", authority.Head()) + } + + return nil +} + // resetForProfileChangeLockedOnEntry resets the backend for a profile change. func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error { b.setNetMapLocked(nil) // Reset netmap. // Reset the NetworkMap in the engine b.e.SetNetworkMap(new(netmap.NetworkMap)) + if err := b.initTKALocked(); err != nil { + return err + } b.enterStateLockedOnEntry(ipn.NoState) // Reset state. return b.Start(ipn.Options{}) } diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index 7e1095ad1..9823367da 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -35,6 +35,7 @@ var ( ) type tkaState struct { + profile ipn.ProfileID authority *tka.Authority storage *tka.FS } @@ -251,7 +252,7 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error { // b.mu must be held & TKA must be initialized. func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error { if b.tka.authority.ValidDisablement(secret) { - if err := os.RemoveAll(b.chonkPath()); err != nil { + if err := os.RemoveAll(b.chonkPathLocked()); err != nil { return err } b.tka = nil @@ -260,10 +261,10 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error { return errors.New("incorrect disablement secret") } -// chonkPath returns the absolute path to the directory in which TKA +// chonkPathLocked returns the absolute path to the directory in which TKA // state (the 'tailchonk') is stored. -func (b *LocalBackend) chonkPath() string { - return filepath.Join(b.TailscaleVarRoot(), "tka") +func (b *LocalBackend) chonkPathLocked() string { + return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID)) } // tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the @@ -280,7 +281,10 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err return fmt.Errorf("reading genesis: %v", err) } - chonkDir := b.chonkPath() + chonkDir := b.chonkPathLocked() + if err := os.Mkdir(filepath.Dir(chonkDir), 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("creating chonk root dir: %v", err) + } if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) { return fmt.Errorf("mkdir: %v", err) } @@ -295,6 +299,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err } b.tka = &tkaState{ + profile: b.pm.CurrentProfile().ID, authority: authority, storage: chonk, } @@ -329,17 +334,27 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { b.mu.Lock() defer b.mu.Unlock() - var nodeKey *key.NodePublic + var ( + nodeKey *key.NodePublic + nlPriv key.NLPrivate + ) if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() { nkp := p.Persist().PublicNodeKey() nodeKey = &nkp + nlPriv = p.Persist().NetworkLockKey } + if nlPriv.IsZero() { + return &ipnstate.NetworkLockStatus{ + Enabled: false, + NodeKey: nodeKey, + } + } if b.tka == nil { return &ipnstate.NetworkLockStatus{ Enabled: false, - PublicKey: b.nlPrivKey.Public(), NodeKey: nodeKey, + PublicKey: nlPriv.Public(), } } @@ -365,7 +380,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { return &ipnstate.NetworkLockStatus{ Enabled: true, Head: &head, - PublicKey: b.nlPrivKey.Public(), + PublicKey: nlPriv.Public(), NodeKey: nodeKey, NodeKeySigned: selfAuthorized, TrustedKeys: outKeys, @@ -387,12 +402,14 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt } var ourNodeKey key.NodePublic + var nlPriv key.NLPrivate b.mu.Lock() if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() { ourNodeKey = p.Persist().PublicNodeKey() + nlPriv = p.Persist().NetworkLockKey } b.mu.Unlock() - if ourNodeKey.IsZero() { + if ourNodeKey.IsZero() || nlPriv.IsZero() { return errors.New("no node-key: is tailscale logged in?") } @@ -407,7 +424,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt // - DisablementSecret: value needed to disable. // - DisablementValue: the KDF of the disablement secret, a public value. DisablementSecrets: disablementValues, - }, b.nlPrivKey) + }, nlPriv) if err != nil { return fmt.Errorf("tka.Create: %v", err) } @@ -430,7 +447,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt // satisfy network-lock checks. sigs := make(map[tailcfg.NodeID]tkatype.MarshaledSignature, len(initResp.NeedSignatures)) for _, nodeInfo := range initResp.NeedSignatures { - nks, err := signNodeKey(nodeInfo, b.nlPrivKey) + nks, err := signNodeKey(nodeInfo, nlPriv) if err != nil { return fmt.Errorf("generating signature: %v", err) } @@ -470,10 +487,18 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic [] b.mu.Lock() defer b.mu.Unlock() + var nlPriv key.NLPrivate + if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil { + nlPriv = p.Persist().NetworkLockKey + } + if nlPriv.IsZero() { + return key.NodePublic{}, tka.NodeKeySignature{}, errMissingNetmap + } + if b.tka == nil { return key.NodePublic{}, tka.NodeKeySignature{}, errNetworkLockNotActive } - if !b.tka.authority.KeyTrusted(b.nlPrivKey.KeyID()) { + if !b.tka.authority.KeyTrusted(nlPriv.KeyID()) { return key.NodePublic{}, tka.NodeKeySignature{}, errors.New("this node is not trusted by network lock") } @@ -483,11 +508,11 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic [] } sig := tka.NodeKeySignature{ SigKind: tka.SigDirect, - KeyID: b.nlPrivKey.KeyID(), + KeyID: nlPriv.KeyID(), Pubkey: p, WrappingPubkey: rotationPublic, } - sig.Signature, err = b.nlPrivKey.SignNKS(sig.SigHash()) + sig.Signature, err = nlPriv.SignNKS(sig.SigHash()) if err != nil { return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err) } @@ -527,11 +552,18 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err if err := b.CanSupportNetworkLock(); err != nil { return err } + var nlPriv key.NLPrivate + if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil { + nlPriv = p.Persist().NetworkLockKey + } + if nlPriv.IsZero() { + return errMissingNetmap + } if b.tka == nil { return errNetworkLockNotActive } - updater := b.tka.authority.NewUpdater(b.nlPrivKey) + updater := b.tka.authority.NewUpdater(nlPriv) for _, addKey := range addKeys { if err := updater.AddKey(addKey); err != nil { diff --git a/ipn/ipnlocal/network-lock_test.go b/ipn/ipnlocal/network-lock_test.go index 88e45a102..6d34b16a8 100644 --- a/ipn/ipnlocal/network-lock_test.go +++ b/ipn/ipnlocal/network-lock_test.go @@ -122,7 +122,10 @@ func TestTKAEnablementFlow(t *testing.T) { cc := fakeControlClient(t, client) pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) must.Do(pm.SetPrefs((&ipn.Prefs{ - Persist: &persist.Persist{PrivateNodeKey: nodePriv}, + Persist: &persist.Persist{ + PrivateNodeKey: nodePriv, + NetworkLockKey: nlPriv, + }, }).View())) b := LocalBackend{ varRoot: temp, @@ -151,15 +154,25 @@ func TestTKAEnablementFlow(t *testing.T) { func TestTKADisablementFlow(t *testing.T) { envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "") - temp := t.TempDir() - os.Mkdir(filepath.Join(temp, "tka"), 0755) nodePriv := key.NewNode() // Make a fake TKA authority, to seed local state. disablementSecret := bytes.Repeat([]byte{0xa5}, 32) nlPriv := key.NewNLPrivate() key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} - chonk, err := tka.ChonkDir(filepath.Join(temp, "tka")) + + pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) + must.Do(pm.SetPrefs((&ipn.Prefs{ + Persist: &persist.Persist{ + PrivateNodeKey: nodePriv, + NetworkLockKey: nlPriv, + }, + }).View())) + + temp := t.TempDir() + tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) + os.Mkdir(tkaPath, 0755) + chonk, err := tka.ChonkDir(tkaPath) if err != nil { t.Fatal(err) } @@ -217,10 +230,6 @@ func TestTKADisablementFlow(t *testing.T) { defer ts.Close() cc := fakeControlClient(t, client) - pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) - must.Do(pm.SetPrefs((&ipn.Prefs{ - Persist: &persist.Persist{PrivateNodeKey: nodePriv}, - }).View())) b := LocalBackend{ varRoot: temp, cc: cc, @@ -260,7 +269,7 @@ func TestTKADisablementFlow(t *testing.T) { if b.tka != nil { t.Fatal("tka was not shut down") } - if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) { + if _, err := os.Stat(b.chonkPathLocked()); err == nil || !os.IsNotExist(err) { t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err) } } @@ -345,10 +354,15 @@ func TestTKASync(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - temp := t.TempDir() - os.Mkdir(filepath.Join(temp, "tka"), 0755) nodePriv := key.NewNode() nlPriv := key.NewNLPrivate() + pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) + must.Do(pm.SetPrefs((&ipn.Prefs{ + Persist: &persist.Persist{ + PrivateNodeKey: nodePriv, + NetworkLockKey: nlPriv, + }, + }).View())) // Setup the tka authority on the control plane. key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} @@ -366,8 +380,11 @@ func TestTKASync(t *testing.T) { } } + temp := t.TempDir() + tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) + os.Mkdir(tkaPath, 0755) // Setup the TKA authority on the node. - nodeStorage, err := tka.ChonkDir(filepath.Join(temp, "tka")) + nodeStorage, err := tka.ChonkDir(tkaPath) if err != nil { t.Fatal(err) } @@ -467,10 +484,6 @@ func TestTKASync(t *testing.T) { // Setup the client. cc := fakeControlClient(t, client) - pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) - must.Do(pm.SetPrefs((&ipn.Prefs{ - Persist: &persist.Persist{PrivateNodeKey: nodePriv}, - }).View())) b := LocalBackend{ varRoot: temp, cc: cc, @@ -564,15 +577,25 @@ func TestTKAFilterNetmap(t *testing.T) { func TestTKADisable(t *testing.T) { envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "") - temp := t.TempDir() - os.Mkdir(filepath.Join(temp, "tka"), 0755) nodePriv := key.NewNode() // Make a fake TKA authority, to seed local state. disablementSecret := bytes.Repeat([]byte{0xa5}, 32) nlPriv := key.NewNLPrivate() + + pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) + must.Do(pm.SetPrefs((&ipn.Prefs{ + Persist: &persist.Persist{ + PrivateNodeKey: nodePriv, + NetworkLockKey: nlPriv, + }, + }).View())) + + temp := t.TempDir() + tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) + os.Mkdir(tkaPath, 0755) key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} - chonk, err := tka.ChonkDir(filepath.Join(temp, "tka")) + chonk, err := tka.ChonkDir(tkaPath) if err != nil { t.Fatal(err) } @@ -623,17 +646,13 @@ func TestTKADisable(t *testing.T) { defer ts.Close() cc := fakeControlClient(t, client) - pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) - must.Do(pm.SetPrefs((&ipn.Prefs{ - Persist: &persist.Persist{PrivateNodeKey: nodePriv}, - }).View())) - b := LocalBackend{ varRoot: temp, cc: cc, ccAuto: cc, logf: t.Logf, tka: &tkaState{ + profile: pm.CurrentProfile().ID, authority: authority, storage: chonk, }, @@ -653,16 +672,26 @@ func TestTKADisable(t *testing.T) { func TestTKASign(t *testing.T) { envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "") - temp := t.TempDir() - os.Mkdir(filepath.Join(temp, "tka"), 0755) nodePriv := key.NewNode() toSign := key.NewNode() + nlPriv := key.NewNLPrivate() + + pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) + must.Do(pm.SetPrefs((&ipn.Prefs{ + Persist: &persist.Persist{ + PrivateNodeKey: nodePriv, + NetworkLockKey: nlPriv, + }, + }).View())) // Make a fake TKA authority, to seed local state. disablementSecret := bytes.Repeat([]byte{0xa5}, 32) - nlPriv := key.NewNLPrivate() key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} - chonk, err := tka.ChonkDir(filepath.Join(temp, "tka")) + + temp := t.TempDir() + tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) + os.Mkdir(tkaPath, 0755) + chonk, err := tka.ChonkDir(tkaPath) if err != nil { t.Fatal(err) } @@ -709,10 +738,6 @@ func TestTKASign(t *testing.T) { } })) defer ts.Close() - pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) - must.Do(pm.SetPrefs((&ipn.Prefs{ - Persist: &persist.Persist{PrivateNodeKey: nodePriv}, - }).View())) cc := fakeControlClient(t, client) b := LocalBackend{ varRoot: temp, @@ -723,9 +748,8 @@ func TestTKASign(t *testing.T) { authority: authority, storage: chonk, }, - pm: pm, - store: pm.Store(), - nlPrivKey: nlPriv, + pm: pm, + store: pm.Store(), } if err := b.NetworkLockSign(toSign.Public(), nil); err != nil { diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index f330c2e9a..1180b07c5 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -252,8 +252,8 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error return savedPrefs.View(), nil } -// CurrentProfile returns the name and ID of the current profile, or "" if the profile -// is not named. +// CurrentProfile returns the current LoginProfile. +// The value may be zero if the profile is not persisted. func (pm *profileManager) CurrentProfile() ipn.LoginProfile { return *pm.currentProfile } diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 9c73d7bf2..7d1317a55 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -42,7 +42,6 @@ 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" @@ -751,24 +750,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi }) if root := b.TailscaleVarRoot(); root != "" { - chonkDir := filepath.Join(root, "tka") - if _, err := os.Stat(chonkDir); err == nil { - // The directory exists, which means network-lock has been initialized. - storage, err := tka.ChonkDir(chonkDir) - if err != nil { - return nil, fmt.Errorf("opening tailchonk: %v", err) - } - authority, err := tka.Open(storage) - if err != nil { - return nil, fmt.Errorf("initializing tka: %v", err) - } - b.SetTailnetKeyAuthority(authority, storage) - logf("tka initialized at head %x", authority.Head()) - } - dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json")) - } else { - logf("network-lock unavailable; no state directory") } dg := distro.Get() diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 58f3084ba..bed9e7ddf 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -86,6 +86,7 @@ type NetworkLockStatus struct { Head *[32]byte // PublicKey describes the node's network-lock public key. + // It may be zero if the node has not logged in. PublicKey key.NLPublic // NodeKey describes the node's current node-key. This field is not diff --git a/types/key/nl.go b/types/key/nl.go index 3f1564454..4a778d58f 100644 --- a/types/key/nl.go +++ b/types/key/nl.go @@ -60,6 +60,11 @@ func (k NLPrivate) MarshalText() ([]byte, error) { return toHex(k.k[:], nlPrivateHexPrefix), nil } +// Equal reports whether k and other are the same key. +func (k NLPrivate) Equal(other NLPrivate) bool { + return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1 +} + // Public returns the public component of this key. func (k NLPrivate) Public() NLPublic { var out NLPublic diff --git a/types/persist/persist.go b/types/persist/persist.go index 3e3b06cba..3f4162eac 100644 --- a/types/persist/persist.go +++ b/types/persist/persist.go @@ -16,7 +16,8 @@ import ( //go:generate go run tailscale.com/cmd/viewer -type=Persist // Persist is the JSON type stored on disk on nodes to remember their -// settings between runs. +// settings between runs. This is stored as part of ipn.Prefs and is +// persisted per ipn.LoginProfile. type Persist struct { _ structs.Incomparable @@ -36,6 +37,7 @@ type Persist struct { Provider string LoginName string UserProfile tailcfg.UserProfile + NetworkLockKey key.NLPrivate } // PublicNodeKey returns the public key for the node key. @@ -65,7 +67,8 @@ func (p *Persist) Equals(p2 *Persist) bool { p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) && p.Provider == p2.Provider && p.LoginName == p2.LoginName && - p.UserProfile == p2.UserProfile + p.UserProfile == p2.UserProfile && + p.NetworkLockKey.Equal(p2.NetworkLockKey) } func (p *Persist) Pretty() string { diff --git a/types/persist/persist_clone.go b/types/persist/persist_clone.go index a6292bd04..4d7924665 100644 --- a/types/persist/persist_clone.go +++ b/types/persist/persist_clone.go @@ -32,4 +32,5 @@ var _PersistCloneNeedsRegeneration = Persist(struct { Provider string LoginName string UserProfile tailcfg.UserProfile + NetworkLockKey key.NLPrivate }{}) diff --git a/types/persist/persist_test.go b/types/persist/persist_test.go index e23f82189..ac1401b87 100644 --- a/types/persist/persist_test.go +++ b/types/persist/persist_test.go @@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) { } func TestPersistEqual(t *testing.T) { - persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName", "UserProfile"} + persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName", "UserProfile", "NetworkLockKey"} if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) { t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n", have, persistHandles) @@ -30,6 +30,7 @@ func TestPersistEqual(t *testing.T) { m1 := key.NewMachine() k1 := key.NewNode() + nl1 := key.NewNLPrivate() tests := []struct { a, b *Persist want bool @@ -112,6 +113,16 @@ func TestPersistEqual(t *testing.T) { }}, false, }, + { + &Persist{NetworkLockKey: nl1}, + &Persist{NetworkLockKey: nl1}, + true, + }, + { + &Persist{NetworkLockKey: nl1}, + &Persist{NetworkLockKey: key.NewNLPrivate()}, + false, + }, } for i, test := range tests { if got := test.a.Equals(test.b); got != test.want { diff --git a/types/persist/persist_view.go b/types/persist/persist_view.go index f4c28187b..ee3976346 100644 --- a/types/persist/persist_view.go +++ b/types/persist/persist_view.go @@ -70,6 +70,7 @@ func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivat func (v PersistView) Provider() string { return v.ж.Provider } func (v PersistView) LoginName() string { return v.ж.LoginName } func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile } +func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _PersistViewNeedsRegeneration = Persist(struct { @@ -80,4 +81,5 @@ var _PersistViewNeedsRegeneration = Persist(struct { Provider string LoginName string UserProfile tailcfg.UserProfile + NetworkLockKey key.NLPrivate }{})