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/<profile-id>". 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 <maisem@tailscale.com>
pull/6333/head
Maisem Ali 2 years ago committed by Tom
parent 751f866f01
commit 235309adc4

@ -93,12 +93,14 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
fmt.Println() fmt.Println()
} }
p, err := st.PublicKey.MarshalText() if !st.PublicKey.IsZero() {
if err != nil { p, err := st.PublicKey.MarshalText()
return err 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 { if st.Enabled && len(st.TrustedKeys) > 0 {
fmt.Println("Keys trusted to make changes to network-lock:") fmt.Println("Keys trusted to make changes to network-lock:")

@ -70,7 +70,6 @@ type Direct struct {
linkMon *monitor.Mon // or nil linkMon *monitor.Mon // or nil
discoPubKey key.DiscoPublic discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error) getMachinePrivKey func() (key.MachinePrivate, error)
getNLPrivateKey func() (key.NLPrivate, error) // or nil
debugFlags []string debugFlags []string
keepSharerAndUserSplit bool keepSharerAndUserSplit bool
skipIPForwardingCheck bool skipIPForwardingCheck bool
@ -118,10 +117,6 @@ type Options struct {
Dialer *tsdial.Dialer // non-nil Dialer *tsdial.Dialer // non-nil
C2NHandler http.Handler // or 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 is called when there's a change in status.
Status func(Status) Status func(Status)
@ -232,7 +227,6 @@ func NewDirect(opts Options) (*Direct, error) {
c := &Direct{ c := &Direct{
httpc: httpc, httpc: httpc,
getMachinePrivKey: opts.GetMachinePrivateKey, getMachinePrivKey: opts.GetMachinePrivateKey,
getNLPrivateKey: opts.GetNLPrivateKey,
serverURL: opts.ServerURL, serverURL: opts.ServerURL,
timeNow: opts.TimeNow, timeNow: opts.TimeNow,
logf: opts.Logf, logf: opts.Logf,
@ -494,6 +488,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
if !persist.OldPrivateNodeKey.IsZero() { if !persist.OldPrivateNodeKey.IsZero() {
oldNodeKey = persist.OldPrivateNodeKey.Public() oldNodeKey = persist.OldPrivateNodeKey.Public()
} }
if persist.NetworkLockKey.IsZero() {
persist.NetworkLockKey = key.NewNLPrivate()
}
nlPub := persist.NetworkLockKey.Public()
if tryingNewKey.IsZero() { if tryingNewKey.IsZero() {
if opt.Logout { 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") log.Fatalf("tryingNewKey is empty, give up")
} }
var nlPub key.NLPublic
var nodeKeySignature tkatype.MarshaledSignature var nodeKeySignature tkatype.MarshaledSignature
if c.getNLPrivateKey != nil { if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != nil {
priv, err := c.getNLPrivateKey() if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
if err != nil { c.logf("Failed re-signing node-key signature: %v", err)
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)
}
} }
} }

@ -159,7 +159,6 @@ type LocalBackend struct {
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
inServerMode bool inServerMode bool
machinePrivKey key.MachinePrivate machinePrivKey key.MachinePrivate
nlPrivKey key.NLPrivate
tka *tkaState tka *tkaState
state ipn.State state ipn.State
capFileSharing bool // whether netMap contains the file sharing capability 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) { if !prefs.Persist.View().Equals(*st.Persist) {
prefsChanged = true prefsChanged = true
prefs.Persist = st.Persist.AsStruct() prefs.Persist = st.Persist.AsStruct()
if err := b.initTKALocked(); err != nil {
b.logf("initTKALocked: %v", err)
}
} }
} }
if st.URL != "" { if st.URL != "" {
@ -1174,9 +1176,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
return fmt.Errorf("initMachineKeyLocked: %w", err) return fmt.Errorf("initMachineKeyLocked: %w", err)
} }
} }
if err := b.initNLKeyLocked(); err != nil {
return fmt.Errorf("initNLKeyLocked: %w", err)
}
loggedOut := prefs.LoggedOut() 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(). // but it won't take effect until the next Start().
cc, err := b.getNewControlClientFunc()(controlclient.Options{ cc, err := b.getNewControlClientFunc()(controlclient.Options{
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(), GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
GetNLPrivateKey: b.createGetNLPrivateKeyFunc(),
Logf: logger.WithPrefix(b.logf, "control: "), Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persistv, Persist: *persistv,
ServerURL: b.serverURL, 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. // initMachineKeyLocked is called to initialize b.machinePrivKey.
// //
// b.prefs must already be initialized. // b.prefs must already be initialized.
@ -1827,46 +1810,6 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
return nil 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. // migrateStateLocked migrates state from the frontend to the backend.
// It is a no-op if prefs is nil // It is a no-op if prefs is nil
// b.mu must be held. // b.mu must be held.
@ -2705,17 +2648,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
return dcfg 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 // SetVarRoot sets the root directory of Tailscale's writable
// storage area . (e.g. "/var/lib/tailscale") // storage area . (e.g. "/var/lib/tailscale")
// //
@ -4053,11 +3985,58 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
return b.resetForProfileChangeLockedOnEntry() 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. // resetForProfileChangeLockedOnEntry resets the backend for a profile change.
func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error { func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error {
b.setNetMapLocked(nil) // Reset netmap. b.setNetMapLocked(nil) // Reset netmap.
// Reset the NetworkMap in the engine // Reset the NetworkMap in the engine
b.e.SetNetworkMap(new(netmap.NetworkMap)) b.e.SetNetworkMap(new(netmap.NetworkMap))
if err := b.initTKALocked(); err != nil {
return err
}
b.enterStateLockedOnEntry(ipn.NoState) // Reset state. b.enterStateLockedOnEntry(ipn.NoState) // Reset state.
return b.Start(ipn.Options{}) return b.Start(ipn.Options{})
} }

@ -35,6 +35,7 @@ var (
) )
type tkaState struct { type tkaState struct {
profile ipn.ProfileID
authority *tka.Authority authority *tka.Authority
storage *tka.FS storage *tka.FS
} }
@ -251,7 +252,7 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
// b.mu must be held & TKA must be initialized. // b.mu must be held & TKA must be initialized.
func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error { func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
if b.tka.authority.ValidDisablement(secret) { if b.tka.authority.ValidDisablement(secret) {
if err := os.RemoveAll(b.chonkPath()); err != nil { if err := os.RemoveAll(b.chonkPathLocked()); err != nil {
return err return err
} }
b.tka = nil b.tka = nil
@ -260,10 +261,10 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
return errors.New("incorrect disablement secret") 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. // state (the 'tailchonk') is stored.
func (b *LocalBackend) chonkPath() string { func (b *LocalBackend) chonkPathLocked() string {
return filepath.Join(b.TailscaleVarRoot(), "tka") return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID))
} }
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the // 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) 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) { if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("mkdir: %v", err) return fmt.Errorf("mkdir: %v", err)
} }
@ -295,6 +299,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
} }
b.tka = &tkaState{ b.tka = &tkaState{
profile: b.pm.CurrentProfile().ID,
authority: authority, authority: authority,
storage: chonk, storage: chonk,
} }
@ -329,17 +334,27 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() 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() { if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
nkp := p.Persist().PublicNodeKey() nkp := p.Persist().PublicNodeKey()
nodeKey = &nkp nodeKey = &nkp
nlPriv = p.Persist().NetworkLockKey
} }
if nlPriv.IsZero() {
return &ipnstate.NetworkLockStatus{
Enabled: false,
NodeKey: nodeKey,
}
}
if b.tka == nil { if b.tka == nil {
return &ipnstate.NetworkLockStatus{ return &ipnstate.NetworkLockStatus{
Enabled: false, Enabled: false,
PublicKey: b.nlPrivKey.Public(),
NodeKey: nodeKey, NodeKey: nodeKey,
PublicKey: nlPriv.Public(),
} }
} }
@ -365,7 +380,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
return &ipnstate.NetworkLockStatus{ return &ipnstate.NetworkLockStatus{
Enabled: true, Enabled: true,
Head: &head, Head: &head,
PublicKey: b.nlPrivKey.Public(), PublicKey: nlPriv.Public(),
NodeKey: nodeKey, NodeKey: nodeKey,
NodeKeySigned: selfAuthorized, NodeKeySigned: selfAuthorized,
TrustedKeys: outKeys, TrustedKeys: outKeys,
@ -387,12 +402,14 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
} }
var ourNodeKey key.NodePublic var ourNodeKey key.NodePublic
var nlPriv key.NLPrivate
b.mu.Lock() b.mu.Lock()
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() { if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
ourNodeKey = p.Persist().PublicNodeKey() ourNodeKey = p.Persist().PublicNodeKey()
nlPriv = p.Persist().NetworkLockKey
} }
b.mu.Unlock() b.mu.Unlock()
if ourNodeKey.IsZero() { if ourNodeKey.IsZero() || nlPriv.IsZero() {
return errors.New("no node-key: is tailscale logged in?") 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. // - DisablementSecret: value needed to disable.
// - DisablementValue: the KDF of the disablement secret, a public value. // - DisablementValue: the KDF of the disablement secret, a public value.
DisablementSecrets: disablementValues, DisablementSecrets: disablementValues,
}, b.nlPrivKey) }, nlPriv)
if err != nil { if err != nil {
return fmt.Errorf("tka.Create: %v", err) return fmt.Errorf("tka.Create: %v", err)
} }
@ -430,7 +447,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
// satisfy network-lock checks. // satisfy network-lock checks.
sigs := make(map[tailcfg.NodeID]tkatype.MarshaledSignature, len(initResp.NeedSignatures)) sigs := make(map[tailcfg.NodeID]tkatype.MarshaledSignature, len(initResp.NeedSignatures))
for _, nodeInfo := range initResp.NeedSignatures { for _, nodeInfo := range initResp.NeedSignatures {
nks, err := signNodeKey(nodeInfo, b.nlPrivKey) nks, err := signNodeKey(nodeInfo, nlPriv)
if err != nil { if err != nil {
return fmt.Errorf("generating signature: %v", err) return fmt.Errorf("generating signature: %v", err)
} }
@ -470,10 +487,18 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() 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 { if b.tka == nil {
return key.NodePublic{}, tka.NodeKeySignature{}, errNetworkLockNotActive 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") 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{ sig := tka.NodeKeySignature{
SigKind: tka.SigDirect, SigKind: tka.SigDirect,
KeyID: b.nlPrivKey.KeyID(), KeyID: nlPriv.KeyID(),
Pubkey: p, Pubkey: p,
WrappingPubkey: rotationPublic, WrappingPubkey: rotationPublic,
} }
sig.Signature, err = b.nlPrivKey.SignNKS(sig.SigHash()) sig.Signature, err = nlPriv.SignNKS(sig.SigHash())
if err != nil { if err != nil {
return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err) 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 { if err := b.CanSupportNetworkLock(); err != nil {
return err 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 { if b.tka == nil {
return errNetworkLockNotActive return errNetworkLockNotActive
} }
updater := b.tka.authority.NewUpdater(b.nlPrivKey) updater := b.tka.authority.NewUpdater(nlPriv)
for _, addKey := range addKeys { for _, addKey := range addKeys {
if err := updater.AddKey(addKey); err != nil { if err := updater.AddKey(addKey); err != nil {

@ -122,7 +122,10 @@ func TestTKAEnablementFlow(t *testing.T) {
cc := fakeControlClient(t, client) cc := fakeControlClient(t, client)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, "")) pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{ must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv}, Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View())) }).View()))
b := LocalBackend{ b := LocalBackend{
varRoot: temp, varRoot: temp,
@ -151,15 +154,25 @@ func TestTKAEnablementFlow(t *testing.T) {
func TestTKADisablementFlow(t *testing.T) { func TestTKADisablementFlow(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "") defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode() nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state. // Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32) disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate() nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -217,10 +230,6 @@ func TestTKADisablementFlow(t *testing.T) {
defer ts.Close() defer ts.Close()
cc := fakeControlClient(t, 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{ b := LocalBackend{
varRoot: temp, varRoot: temp,
cc: cc, cc: cc,
@ -260,7 +269,7 @@ func TestTKADisablementFlow(t *testing.T) {
if b.tka != nil { if b.tka != nil {
t.Fatal("tka was not shut down") 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) t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
} }
} }
@ -345,10 +354,15 @@ func TestTKASync(t *testing.T) {
for _, tc := range tcs { for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode() nodePriv := key.NewNode()
nlPriv := key.NewNLPrivate() 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. // Setup the tka authority on the control plane.
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} 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. // Setup the TKA authority on the node.
nodeStorage, err := tka.ChonkDir(filepath.Join(temp, "tka")) nodeStorage, err := tka.ChonkDir(tkaPath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -467,10 +484,6 @@ func TestTKASync(t *testing.T) {
// Setup the client. // Setup the client.
cc := fakeControlClient(t, 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{ b := LocalBackend{
varRoot: temp, varRoot: temp,
cc: cc, cc: cc,
@ -564,15 +577,25 @@ func TestTKAFilterNetmap(t *testing.T) {
func TestTKADisable(t *testing.T) { func TestTKADisable(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "") defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode() nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state. // Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32) disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate() 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} 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -623,17 +646,13 @@ func TestTKADisable(t *testing.T) {
defer ts.Close() defer ts.Close()
cc := fakeControlClient(t, 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{ b := LocalBackend{
varRoot: temp, varRoot: temp,
cc: cc, cc: cc,
ccAuto: cc, ccAuto: cc,
logf: t.Logf, logf: t.Logf,
tka: &tkaState{ tka: &tkaState{
profile: pm.CurrentProfile().ID,
authority: authority, authority: authority,
storage: chonk, storage: chonk,
}, },
@ -653,16 +672,26 @@ func TestTKADisable(t *testing.T) {
func TestTKASign(t *testing.T) { func TestTKASign(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "") defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode() nodePriv := key.NewNode()
toSign := 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. // Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32) disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -709,10 +738,6 @@ func TestTKASign(t *testing.T) {
} }
})) }))
defer ts.Close() 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) cc := fakeControlClient(t, client)
b := LocalBackend{ b := LocalBackend{
varRoot: temp, varRoot: temp,
@ -723,9 +748,8 @@ func TestTKASign(t *testing.T) {
authority: authority, authority: authority,
storage: chonk, storage: chonk,
}, },
pm: pm, pm: pm,
store: pm.Store(), store: pm.Store(),
nlPrivKey: nlPriv,
} }
if err := b.NetworkLockSign(toSign.Public(), nil); err != nil { if err := b.NetworkLockSign(toSign.Public(), nil); err != nil {

@ -252,8 +252,8 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
return savedPrefs.View(), nil return savedPrefs.View(), nil
} }
// CurrentProfile returns the name and ID of the current profile, or "" if the profile // CurrentProfile returns the current LoginProfile.
// is not named. // The value may be zero if the profile is not persisted.
func (pm *profileManager) CurrentProfile() ipn.LoginProfile { func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
return *pm.currentProfile return *pm.currentProfile
} }

@ -42,7 +42,6 @@ import (
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/smallzstd" "tailscale.com/smallzstd"
"tailscale.com/tka"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/groupmember" "tailscale.com/util/groupmember"
"tailscale.com/util/pidowner" "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 != "" { 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")) dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
} else {
logf("network-lock unavailable; no state directory")
} }
dg := distro.Get() dg := distro.Get()

@ -86,6 +86,7 @@ type NetworkLockStatus struct {
Head *[32]byte Head *[32]byte
// PublicKey describes the node's network-lock public key. // PublicKey describes the node's network-lock public key.
// It may be zero if the node has not logged in.
PublicKey key.NLPublic PublicKey key.NLPublic
// NodeKey describes the node's current node-key. This field is not // NodeKey describes the node's current node-key. This field is not

@ -60,6 +60,11 @@ func (k NLPrivate) MarshalText() ([]byte, error) {
return toHex(k.k[:], nlPrivateHexPrefix), nil 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. // Public returns the public component of this key.
func (k NLPrivate) Public() NLPublic { func (k NLPrivate) Public() NLPublic {
var out NLPublic var out NLPublic

@ -16,7 +16,8 @@ import (
//go:generate go run tailscale.com/cmd/viewer -type=Persist //go:generate go run tailscale.com/cmd/viewer -type=Persist
// Persist is the JSON type stored on disk on nodes to remember their // 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 { type Persist struct {
_ structs.Incomparable _ structs.Incomparable
@ -36,6 +37,7 @@ type Persist struct {
Provider string Provider string
LoginName string LoginName string
UserProfile tailcfg.UserProfile UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
} }
// PublicNodeKey returns the public key for the node key. // 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.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.Provider == p2.Provider && p.Provider == p2.Provider &&
p.LoginName == p2.LoginName && p.LoginName == p2.LoginName &&
p.UserProfile == p2.UserProfile p.UserProfile == p2.UserProfile &&
p.NetworkLockKey.Equal(p2.NetworkLockKey)
} }
func (p *Persist) Pretty() string { func (p *Persist) Pretty() string {

@ -32,4 +32,5 @@ var _PersistCloneNeedsRegeneration = Persist(struct {
Provider string Provider string
LoginName string LoginName string
UserProfile tailcfg.UserProfile UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
}{}) }{})

@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
} }
func TestPersistEqual(t *testing.T) { 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) { 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", t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, persistHandles) have, persistHandles)
@ -30,6 +30,7 @@ func TestPersistEqual(t *testing.T) {
m1 := key.NewMachine() m1 := key.NewMachine()
k1 := key.NewNode() k1 := key.NewNode()
nl1 := key.NewNLPrivate()
tests := []struct { tests := []struct {
a, b *Persist a, b *Persist
want bool want bool
@ -112,6 +113,16 @@ func TestPersistEqual(t *testing.T) {
}}, }},
false, false,
}, },
{
&Persist{NetworkLockKey: nl1},
&Persist{NetworkLockKey: nl1},
true,
},
{
&Persist{NetworkLockKey: nl1},
&Persist{NetworkLockKey: key.NewNLPrivate()},
false,
},
} }
for i, test := range tests { for i, test := range tests {
if got := test.a.Equals(test.b); got != test.want { if got := test.a.Equals(test.b); got != test.want {

@ -70,6 +70,7 @@ func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivat
func (v PersistView) Provider() string { return v.ж.Provider } func (v PersistView) Provider() string { return v.ж.Provider }
func (v PersistView) LoginName() string { return v.ж.LoginName } func (v PersistView) LoginName() string { return v.ж.LoginName }
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile } 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PersistViewNeedsRegeneration = Persist(struct { var _PersistViewNeedsRegeneration = Persist(struct {
@ -80,4 +81,5 @@ var _PersistViewNeedsRegeneration = Persist(struct {
Provider string Provider string
LoginName string LoginName string
UserProfile tailcfg.UserProfile UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
}{}) }{})

Loading…
Cancel
Save