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 <tom@tailscale.com>
This commit is contained in:
Tom DNetto 2022-08-01 15:46:41 -07:00 committed by Tom
parent 8d45d7e312
commit 4001d0bf25
8 changed files with 124 additions and 1 deletions

View File

@ -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+

View File

@ -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,

View File

@ -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")
//

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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.
//

View File

@ -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