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 @@ ) 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 @@ type tkaSyncScenario struct { 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 @@ type tkaSyncScenario struct { } } + 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 @@ type tkaSyncScenario struct { // 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 @@ "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 @@ //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 @@ func (src *Persist) Clone() *Persist { 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 @@ func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfi Provider string LoginName string UserProfile tailcfg.UserProfile + NetworkLockKey key.NLPrivate }{})