mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 10:11:18 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			316 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) Tailscale Inc & AUTHORS
 | 
						|
// SPDX-License-Identifier: BSD-3-Clause
 | 
						|
 | 
						|
package tka
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
 | 
						|
	"golang.org/x/crypto/argon2"
 | 
						|
	"tailscale.com/types/tkatype"
 | 
						|
)
 | 
						|
 | 
						|
// ErrNoSuchKey is returned if the key referenced by a KeyID does not exist.
 | 
						|
var ErrNoSuchKey = errors.New("key not found")
 | 
						|
 | 
						|
// State describes Tailnet Key Authority state at an instant in time.
 | 
						|
//
 | 
						|
// State is mutated by applying Authority Update Messages (AUMs), resulting
 | 
						|
// in a new State.
 | 
						|
type State struct {
 | 
						|
	// LastAUMHash is the blake2s digest of the last-applied AUM.
 | 
						|
	// Because AUMs are strictly ordered and form a hash chain, we
 | 
						|
	// check the previous AUM hash in an update we are applying
 | 
						|
	// is the same as the LastAUMHash.
 | 
						|
	LastAUMHash *AUMHash `cbor:"1,keyasint"`
 | 
						|
 | 
						|
	// DisablementSecrets are KDF-derived values which can be used
 | 
						|
	// to turn off the TKA in the event of a consensus-breaking bug.
 | 
						|
	DisablementSecrets [][]byte `cbor:"2,keyasint"`
 | 
						|
 | 
						|
	// Keys are the public keys of either:
 | 
						|
	//
 | 
						|
	//   1. The signing nodes currently trusted by the TKA.
 | 
						|
	//   2. Ephemeral keys that were used to generate pre-signed auth keys.
 | 
						|
	Keys []Key `cbor:"3,keyasint"`
 | 
						|
 | 
						|
	// StateID's are nonce's, generated on enablement and fixed for
 | 
						|
	// the lifetime of the Tailnet Key Authority. We generate 16-bytes
 | 
						|
	// worth of keyspace here just in case we come up with a cool future
 | 
						|
	// use for this.
 | 
						|
	StateID1 uint64 `cbor:"4,keyasint,omitempty"`
 | 
						|
	StateID2 uint64 `cbor:"5,keyasint,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// GetKey returns the trusted key with the specified KeyID.
 | 
						|
func (s State) GetKey(key tkatype.KeyID) (Key, error) {
 | 
						|
	for _, k := range s.Keys {
 | 
						|
		keyID, err := k.ID()
 | 
						|
		if err != nil {
 | 
						|
			return Key{}, err
 | 
						|
		}
 | 
						|
 | 
						|
		if bytes.Equal(keyID, key) {
 | 
						|
			return k, nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return Key{}, ErrNoSuchKey
 | 
						|
}
 | 
						|
 | 
						|
// Clone makes an independent copy of State.
 | 
						|
//
 | 
						|
// NOTE: There is a difference between a nil slice and an empty
 | 
						|
// slice for encoding purposes, so an implementation of Clone()
 | 
						|
// must take care to preserve this.
 | 
						|
func (s State) Clone() State {
 | 
						|
	out := State{
 | 
						|
		StateID1: s.StateID1,
 | 
						|
		StateID2: s.StateID2,
 | 
						|
	}
 | 
						|
 | 
						|
	if s.LastAUMHash != nil {
 | 
						|
		dupe := *s.LastAUMHash
 | 
						|
		out.LastAUMHash = &dupe
 | 
						|
	}
 | 
						|
 | 
						|
	if s.DisablementSecrets != nil {
 | 
						|
		out.DisablementSecrets = make([][]byte, len(s.DisablementSecrets))
 | 
						|
		for i := range s.DisablementSecrets {
 | 
						|
			out.DisablementSecrets[i] = make([]byte, len(s.DisablementSecrets[i]))
 | 
						|
			copy(out.DisablementSecrets[i], s.DisablementSecrets[i])
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if s.Keys != nil {
 | 
						|
		out.Keys = make([]Key, len(s.Keys))
 | 
						|
		for i := range s.Keys {
 | 
						|
			out.Keys[i] = s.Keys[i].Clone()
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return out
 | 
						|
}
 | 
						|
 | 
						|
// cloneForUpdate is like Clone, except LastAUMHash is set based
 | 
						|
// on the hash of the given update.
 | 
						|
func (s State) cloneForUpdate(update *AUM) State {
 | 
						|
	out := s.Clone()
 | 
						|
	aumHash := update.Hash()
 | 
						|
	out.LastAUMHash = &aumHash
 | 
						|
	return out
 | 
						|
}
 | 
						|
 | 
						|
const disablementLength = 32
 | 
						|
 | 
						|
var disablementSalt = []byte("tailscale network-lock disablement salt")
 | 
						|
 | 
						|
// DisablementKDF computes a public value which can be stored in a
 | 
						|
// key authority, but cannot be reversed to find the input secret.
 | 
						|
//
 | 
						|
// When the output of this function is stored in tka state (i.e. in
 | 
						|
// tka.State.DisablementSecrets) a call to Authority.ValidDisablement()
 | 
						|
// with the input of this function as the argument will return true.
 | 
						|
func DisablementKDF(secret []byte) []byte {
 | 
						|
	// time = 4 (3 recommended, booped to 4 to compensate for less memory)
 | 
						|
	// memory = 16 (32 recommended)
 | 
						|
	// threads = 4
 | 
						|
	// keyLen = 32 (256 bits)
 | 
						|
	return argon2.Key(secret, disablementSalt, 4, 16*1024, 4, disablementLength)
 | 
						|
}
 | 
						|
 | 
						|
// checkDisablement returns true for a valid disablement secret.
 | 
						|
func (s State) checkDisablement(secret []byte) bool {
 | 
						|
	derived := DisablementKDF(secret)
 | 
						|
	for _, candidate := range s.DisablementSecrets {
 | 
						|
		if bytes.Equal(derived, candidate) {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// parentMatches returns true if an AUM can chain to (be applied)
 | 
						|
// to the current state.
 | 
						|
//
 | 
						|
// Specifically, the rules are:
 | 
						|
//   - The last AUM hash must match (transitively, this implies that this
 | 
						|
//     update follows the last update message applied to the state machine)
 | 
						|
//   - Or, the state machine knows no parent (its brand new).
 | 
						|
func (s State) parentMatches(update AUM) bool {
 | 
						|
	if s.LastAUMHash == nil {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
	return bytes.Equal(s.LastAUMHash[:], update.PrevAUMHash)
 | 
						|
}
 | 
						|
 | 
						|
// applyVerifiedAUM computes a new state based on the update provided.
 | 
						|
//
 | 
						|
// The provided update MUST be verified: That is, the AUM must be well-formed
 | 
						|
// (as defined by StaticValidate()), and signatures over the AUM must have
 | 
						|
// been verified.
 | 
						|
func (s State) applyVerifiedAUM(update AUM) (State, error) {
 | 
						|
	// Validate that the update message has the right parent.
 | 
						|
	if !s.parentMatches(update) {
 | 
						|
		return State{}, errors.New("parent AUMHash mismatch")
 | 
						|
	}
 | 
						|
 | 
						|
	switch update.MessageKind {
 | 
						|
	case AUMNoOp:
 | 
						|
		out := s.cloneForUpdate(&update)
 | 
						|
		return out, nil
 | 
						|
 | 
						|
	case AUMCheckpoint:
 | 
						|
		if update.State == nil {
 | 
						|
			return State{}, errors.New("missing checkpoint state")
 | 
						|
		}
 | 
						|
		id1Match, id2Match := update.State.StateID1 == s.StateID1, update.State.StateID2 == s.StateID2
 | 
						|
		if !id1Match || !id2Match {
 | 
						|
			return State{}, errors.New("checkpointed state has an incorrect stateID")
 | 
						|
		}
 | 
						|
		return update.State.cloneForUpdate(&update), nil
 | 
						|
 | 
						|
	case AUMAddKey:
 | 
						|
		if update.Key == nil {
 | 
						|
			return State{}, errors.New("no key to add provided")
 | 
						|
		}
 | 
						|
		keyID, err := update.Key.ID()
 | 
						|
		if err != nil {
 | 
						|
			return State{}, err
 | 
						|
		}
 | 
						|
		if _, err := s.GetKey(keyID); err == nil {
 | 
						|
			return State{}, errors.New("key already exists")
 | 
						|
		}
 | 
						|
		out := s.cloneForUpdate(&update)
 | 
						|
		out.Keys = append(out.Keys, *update.Key)
 | 
						|
		return out, nil
 | 
						|
 | 
						|
	case AUMUpdateKey:
 | 
						|
		k, err := s.GetKey(update.KeyID)
 | 
						|
		if err != nil {
 | 
						|
			return State{}, err
 | 
						|
		}
 | 
						|
		if update.Votes != nil {
 | 
						|
			k.Votes = *update.Votes
 | 
						|
		}
 | 
						|
		if update.Meta != nil {
 | 
						|
			k.Meta = update.Meta
 | 
						|
		}
 | 
						|
		if err := k.StaticValidate(); err != nil {
 | 
						|
			return State{}, fmt.Errorf("updated key fails validation: %v", err)
 | 
						|
		}
 | 
						|
		out := s.cloneForUpdate(&update)
 | 
						|
		for i := range out.Keys {
 | 
						|
			keyID, err := out.Keys[i].ID()
 | 
						|
			if err != nil {
 | 
						|
				return State{}, err
 | 
						|
			}
 | 
						|
			if bytes.Equal(keyID, update.KeyID) {
 | 
						|
				out.Keys[i] = k
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return out, nil
 | 
						|
 | 
						|
	case AUMRemoveKey:
 | 
						|
		idx := -1
 | 
						|
		for i := range s.Keys {
 | 
						|
			keyID, err := s.Keys[i].ID()
 | 
						|
			if err != nil {
 | 
						|
				return State{}, err
 | 
						|
			}
 | 
						|
			if bytes.Equal(update.KeyID, keyID) {
 | 
						|
				idx = i
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if idx < 0 {
 | 
						|
			return State{}, ErrNoSuchKey
 | 
						|
		}
 | 
						|
		out := s.cloneForUpdate(&update)
 | 
						|
		out.Keys = append(out.Keys[:idx], out.Keys[idx+1:]...)
 | 
						|
		return out, nil
 | 
						|
 | 
						|
	default:
 | 
						|
		// An AUM with an unknown message kind was received! That means
 | 
						|
		// that a future version of tailscaled added some feature we don't
 | 
						|
		// understand.
 | 
						|
		//
 | 
						|
		// The future-compatibility contract for AUM message types is that
 | 
						|
		// they must only add new features, not change the semantics of existing
 | 
						|
		// mechanisms or features. As such, old clients can safely ignore them.
 | 
						|
		out := s.cloneForUpdate(&update)
 | 
						|
		return out, nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Upper bound on checkpoint elements, chosen arbitrarily. Intended to
 | 
						|
// cap out insanely large AUMs.
 | 
						|
const (
 | 
						|
	maxDisablementSecrets = 32
 | 
						|
	maxKeys               = 512
 | 
						|
)
 | 
						|
 | 
						|
// staticValidateCheckpoint validates that the state is well-formed for
 | 
						|
// inclusion in a checkpoint AUM.
 | 
						|
func (s *State) staticValidateCheckpoint() error {
 | 
						|
	if s.LastAUMHash != nil {
 | 
						|
		return errors.New("cannot specify a parent AUM")
 | 
						|
	}
 | 
						|
	if len(s.DisablementSecrets) == 0 {
 | 
						|
		return errors.New("at least one disablement secret required")
 | 
						|
	}
 | 
						|
	if numDS := len(s.DisablementSecrets); numDS > maxDisablementSecrets {
 | 
						|
		return fmt.Errorf("too many disablement secrets (%d, max %d)", numDS, maxDisablementSecrets)
 | 
						|
	}
 | 
						|
	for i, ds := range s.DisablementSecrets {
 | 
						|
		if len(ds) != disablementLength {
 | 
						|
			return fmt.Errorf("disablement[%d]: invalid length (got %d, want %d)", i, len(ds), disablementLength)
 | 
						|
		}
 | 
						|
		for j, ds2 := range s.DisablementSecrets {
 | 
						|
			if i == j {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			if bytes.Equal(ds, ds2) {
 | 
						|
				return fmt.Errorf("disablement[%d]: duplicates disablement[%d]", i, j)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(s.Keys) == 0 {
 | 
						|
		return errors.New("at least one key is required")
 | 
						|
	}
 | 
						|
	if numKeys := len(s.Keys); numKeys > maxKeys {
 | 
						|
		return fmt.Errorf("too many keys (%d, max %d)", numKeys, maxKeys)
 | 
						|
	}
 | 
						|
	for i, k := range s.Keys {
 | 
						|
		if err := k.StaticValidate(); err != nil {
 | 
						|
			return fmt.Errorf("key[%d]: %v", i, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// NOTE: The max number of keys is constrained (512), so
 | 
						|
	// O(n^2) is fine.
 | 
						|
	for i, k := range s.Keys {
 | 
						|
		for j, k2 := range s.Keys {
 | 
						|
			if i == j {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			id1, err := k.ID()
 | 
						|
			if err != nil {
 | 
						|
				return fmt.Errorf("key[%d]: %w", i, err)
 | 
						|
			}
 | 
						|
			id2, err := k2.ID()
 | 
						|
			if err != nil {
 | 
						|
				return fmt.Errorf("key[%d]: %w", j, err)
 | 
						|
			}
 | 
						|
 | 
						|
			if bytes.Equal(id1, id2) {
 | 
						|
				return fmt.Errorf("key[%d]: duplicates key[%d]", i, j)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 |