|
|
|
|
@ -5,14 +5,29 @@ |
|
|
|
|
package tpm |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"crypto/rand" |
|
|
|
|
"encoding/json" |
|
|
|
|
"errors" |
|
|
|
|
"fmt" |
|
|
|
|
"log" |
|
|
|
|
"os" |
|
|
|
|
"path/filepath" |
|
|
|
|
"slices" |
|
|
|
|
"strings" |
|
|
|
|
"sync" |
|
|
|
|
|
|
|
|
|
"github.com/google/go-tpm/tpm2" |
|
|
|
|
"github.com/google/go-tpm/tpm2/transport" |
|
|
|
|
"golang.org/x/crypto/nacl/secretbox" |
|
|
|
|
"tailscale.com/atomicfile" |
|
|
|
|
"tailscale.com/feature" |
|
|
|
|
"tailscale.com/hostinfo" |
|
|
|
|
"tailscale.com/ipn" |
|
|
|
|
"tailscale.com/ipn/store" |
|
|
|
|
"tailscale.com/paths" |
|
|
|
|
"tailscale.com/tailcfg" |
|
|
|
|
"tailscale.com/types/logger" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var infoOnce = sync.OnceValue(info) |
|
|
|
|
@ -22,10 +37,16 @@ func init() { |
|
|
|
|
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) { |
|
|
|
|
hi.TPM = infoOnce() |
|
|
|
|
}) |
|
|
|
|
store.Register(storePrefix, newStore) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//lint:ignore U1000 used in Linux and Windows builds only
|
|
|
|
|
func infoFromCapabilities(tpm transport.TPM) *tailcfg.TPMInfo { |
|
|
|
|
func info() *tailcfg.TPMInfo { |
|
|
|
|
tpm, err := open() |
|
|
|
|
if err != nil { |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
defer tpm.Close() |
|
|
|
|
|
|
|
|
|
info := new(tailcfg.TPMInfo) |
|
|
|
|
toStr := func(s *string) func(*tailcfg.TPMInfo, uint32) { |
|
|
|
|
return func(info *tailcfg.TPMInfo, value uint32) { |
|
|
|
|
@ -81,3 +102,300 @@ func propToString(v uint32) string { |
|
|
|
|
// Delete any non-printable ASCII characters.
|
|
|
|
|
return string(slices.DeleteFunc(chars, func(b byte) bool { return b < ' ' || b > '~' })) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const storePrefix = "tpmseal:" |
|
|
|
|
|
|
|
|
|
func newStore(logf logger.Logf, path string) (ipn.StateStore, error) { |
|
|
|
|
path = strings.TrimPrefix(path, storePrefix) |
|
|
|
|
if err := paths.MkStateDir(filepath.Dir(path)); err != nil { |
|
|
|
|
return nil, fmt.Errorf("creating state directory: %w", err) |
|
|
|
|
} |
|
|
|
|
var parsed map[ipn.StateKey][]byte |
|
|
|
|
bs, err := os.ReadFile(path) |
|
|
|
|
if err != nil { |
|
|
|
|
if !os.IsNotExist(err) { |
|
|
|
|
return nil, fmt.Errorf("failed to open %q: %w", path, err) |
|
|
|
|
} |
|
|
|
|
logf("tpm.newStore: initializing state file") |
|
|
|
|
|
|
|
|
|
var key [32]byte |
|
|
|
|
// crypto/rand.Read never returns an error.
|
|
|
|
|
rand.Read(key[:]) |
|
|
|
|
|
|
|
|
|
store := &tpmStore{ |
|
|
|
|
logf: logf, |
|
|
|
|
path: path, |
|
|
|
|
key: key, |
|
|
|
|
cache: make(map[ipn.StateKey][]byte), |
|
|
|
|
} |
|
|
|
|
if err := store.writeSealed(); err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to write initial state file: %w", err) |
|
|
|
|
} |
|
|
|
|
return store, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// State file exists, unseal and parse it.
|
|
|
|
|
var sealed encryptedData |
|
|
|
|
if err := json.Unmarshal(bs, &sealed); err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal state file: %w", err) |
|
|
|
|
} |
|
|
|
|
if len(sealed.Data) == 0 || sealed.Key == nil || len(sealed.Nonce) == 0 { |
|
|
|
|
return nil, fmt.Errorf("state file %q has not been TPM-sealed or is corrupt", path) |
|
|
|
|
} |
|
|
|
|
data, err := unseal(logf, sealed) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to unseal state file: %w", err) |
|
|
|
|
} |
|
|
|
|
if err := json.Unmarshal(data.Data, &parsed); err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to parse state file: %w", err) |
|
|
|
|
} |
|
|
|
|
return &tpmStore{ |
|
|
|
|
logf: logf, |
|
|
|
|
path: path, |
|
|
|
|
key: data.Key, |
|
|
|
|
cache: parsed, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// tpmStore is an ipn.StateStore that stores the state in a secretbox-encrypted
|
|
|
|
|
// file using a TPM-sealed symmetric key.
|
|
|
|
|
type tpmStore struct { |
|
|
|
|
logf logger.Logf |
|
|
|
|
path string |
|
|
|
|
key [32]byte |
|
|
|
|
|
|
|
|
|
mu sync.RWMutex |
|
|
|
|
cache map[ipn.StateKey][]byte |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *tpmStore) ReadState(k ipn.StateKey) ([]byte, error) { |
|
|
|
|
s.mu.RLock() |
|
|
|
|
defer s.mu.RUnlock() |
|
|
|
|
v, ok := s.cache[k] |
|
|
|
|
if !ok { |
|
|
|
|
return nil, ipn.ErrStateNotExist |
|
|
|
|
} |
|
|
|
|
return bytes.Clone(v), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *tpmStore) WriteState(k ipn.StateKey, bs []byte) error { |
|
|
|
|
s.mu.Lock() |
|
|
|
|
defer s.mu.Unlock() |
|
|
|
|
if bytes.Equal(s.cache[k], bs) { |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
s.cache[k] = bytes.Clone(bs) |
|
|
|
|
|
|
|
|
|
return s.writeSealed() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *tpmStore) writeSealed() error { |
|
|
|
|
bs, err := json.Marshal(s.cache) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
sealed, err := seal(s.logf, decryptedData{Key: s.key, Data: bs}) |
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("failed to seal state file: %w", err) |
|
|
|
|
} |
|
|
|
|
buf, err := json.Marshal(sealed) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return atomicfile.WriteFile(s.path, buf, 0600) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// The nested levels of encoding and encryption are confusing, so here's what's
|
|
|
|
|
// going on in plain English.
|
|
|
|
|
//
|
|
|
|
|
// Not all TPM devices support symmetric encryption (TPM2_EncryptDecrypt2)
|
|
|
|
|
// natively, but they do support "sealing" small values (see
|
|
|
|
|
// tpmSeal/tpmUnseal). The size limit is too small for the actual state file,
|
|
|
|
|
// so we seal a symmetric key instead. This symmetric key is then used to seal
|
|
|
|
|
// the actual data using nacl/secretbox.
|
|
|
|
|
// Confusingly, both TPMs and secretbox use "seal" terminology.
|
|
|
|
|
//
|
|
|
|
|
// tpmSeal/tpmUnseal do the lower-level sealing of small []byte blobs, which we
|
|
|
|
|
// use to seal a 32-byte secretbox key.
|
|
|
|
|
//
|
|
|
|
|
// seal/unseal do the higher-level sealing of store data using secretbox, and
|
|
|
|
|
// also sealing of the symmetric key using TPM.
|
|
|
|
|
|
|
|
|
|
// decryptedData contains the fully decrypted raw data along with the symmetric
|
|
|
|
|
// key used for secretbox. This struct should only live in memory and never get
|
|
|
|
|
// stored to disk!
|
|
|
|
|
type decryptedData struct { |
|
|
|
|
Key [32]byte |
|
|
|
|
Data []byte |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (decryptedData) MarshalJSON() ([]byte, error) { |
|
|
|
|
return nil, errors.New("[unexpected]: decryptedData should never get JSON-marshaled!") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// encryptedData contains the secretbox-sealed data and nonce, along with a
|
|
|
|
|
// TPM-sealed key. All fields are required.
|
|
|
|
|
type encryptedData struct { |
|
|
|
|
Key *tpmSealedData `json:"key"` |
|
|
|
|
Nonce []byte `json:"nonce"` |
|
|
|
|
Data []byte `json:"data"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func seal(logf logger.Logf, dec decryptedData) (*encryptedData, error) { |
|
|
|
|
var nonce [24]byte |
|
|
|
|
// crypto/rand.Read never returns an error.
|
|
|
|
|
rand.Read(nonce[:]) |
|
|
|
|
|
|
|
|
|
sealedData := secretbox.Seal(nil, dec.Data, &nonce, &dec.Key) |
|
|
|
|
sealedKey, err := tpmSeal(logf, dec.Key[:]) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to seal encryption key to TPM: %w", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return &encryptedData{ |
|
|
|
|
Key: sealedKey, |
|
|
|
|
Nonce: nonce[:], |
|
|
|
|
Data: sealedData, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func unseal(logf logger.Logf, data encryptedData) (*decryptedData, error) { |
|
|
|
|
if len(data.Nonce) != 24 { |
|
|
|
|
return nil, fmt.Errorf("nonce should be 24 bytes long, got %d", len(data.Nonce)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
unsealedKey, err := tpmUnseal(logf, data.Key) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to unseal encryption key with TPM: %w", err) |
|
|
|
|
} |
|
|
|
|
if len(unsealedKey) != 32 { |
|
|
|
|
return nil, fmt.Errorf("unsealed key should be 32 bytes long, got %d", len(unsealedKey)) |
|
|
|
|
} |
|
|
|
|
unsealedData, ok := secretbox.Open(nil, data.Data, (*[24]byte)(data.Nonce), (*[32]byte)(unsealedKey)) |
|
|
|
|
if !ok { |
|
|
|
|
return nil, errors.New("failed to unseal data") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return &decryptedData{ |
|
|
|
|
Key: *(*[32]byte)(unsealedKey), |
|
|
|
|
Data: unsealedData, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type tpmSealedData struct { |
|
|
|
|
Private []byte |
|
|
|
|
Public []byte |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// withSRK runs fn with the loaded Storage Root Key (SRK) handle. The SRK is
|
|
|
|
|
// flushed after fn returns.
|
|
|
|
|
func withSRK(logf logger.Logf, tpm transport.TPM, fn func(srk tpm2.AuthHandle) error) error { |
|
|
|
|
srkCmd := tpm2.CreatePrimary{ |
|
|
|
|
PrimaryHandle: tpm2.TPMRHOwner, |
|
|
|
|
InPublic: tpm2.New2B(tpm2.ECCSRKTemplate), |
|
|
|
|
} |
|
|
|
|
srkRes, err := srkCmd.Execute(tpm) |
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("tpm2.CreatePrimary: %w", err) |
|
|
|
|
} |
|
|
|
|
defer func() { |
|
|
|
|
cmd := tpm2.FlushContext{FlushHandle: srkRes.ObjectHandle} |
|
|
|
|
if _, err := cmd.Execute(tpm); err != nil { |
|
|
|
|
logf("tpm2.FlushContext: failed to flush SRK handle: %v", err) |
|
|
|
|
} |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
return fn(tpm2.AuthHandle{ |
|
|
|
|
Handle: srkRes.ObjectHandle, |
|
|
|
|
Name: srkRes.Name, |
|
|
|
|
Auth: tpm2.HMAC(tpm2.TPMAlgSHA256, 32), |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// tpmSeal seals the data using SRK of the local TPM.
|
|
|
|
|
func tpmSeal(logf logger.Logf, data []byte) (*tpmSealedData, error) { |
|
|
|
|
tpm, err := open() |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("opening TPM: %w", err) |
|
|
|
|
} |
|
|
|
|
defer tpm.Close() |
|
|
|
|
|
|
|
|
|
var res *tpmSealedData |
|
|
|
|
err = withSRK(logf, tpm, func(srk tpm2.AuthHandle) error { |
|
|
|
|
sealCmd := tpm2.Create{ |
|
|
|
|
ParentHandle: srk, |
|
|
|
|
InSensitive: tpm2.TPM2BSensitiveCreate{ |
|
|
|
|
Sensitive: &tpm2.TPMSSensitiveCreate{ |
|
|
|
|
Data: tpm2.NewTPMUSensitiveCreate(&tpm2.TPM2BSensitiveData{ |
|
|
|
|
Buffer: data, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
InPublic: tpm2.New2B(tpm2.TPMTPublic{ |
|
|
|
|
Type: tpm2.TPMAlgKeyedHash, |
|
|
|
|
NameAlg: tpm2.TPMAlgSHA256, |
|
|
|
|
ObjectAttributes: tpm2.TPMAObject{ |
|
|
|
|
FixedTPM: true, |
|
|
|
|
FixedParent: true, |
|
|
|
|
UserWithAuth: true, |
|
|
|
|
}, |
|
|
|
|
}), |
|
|
|
|
} |
|
|
|
|
sealRes, err := sealCmd.Execute(tpm) |
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("tpm2.Create: %w", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
res = &tpmSealedData{ |
|
|
|
|
Private: sealRes.OutPrivate.Buffer, |
|
|
|
|
Public: sealRes.OutPublic.Bytes(), |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
}) |
|
|
|
|
return res, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// tpmUnseal unseals the data using SRK of the local TPM.
|
|
|
|
|
func tpmUnseal(logf logger.Logf, data *tpmSealedData) ([]byte, error) { |
|
|
|
|
tpm, err := open() |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("opening TPM: %w", err) |
|
|
|
|
} |
|
|
|
|
defer tpm.Close() |
|
|
|
|
|
|
|
|
|
var res []byte |
|
|
|
|
err = withSRK(logf, tpm, func(srk tpm2.AuthHandle) error { |
|
|
|
|
// Load the sealed object into the TPM first under SRK.
|
|
|
|
|
loadCmd := tpm2.Load{ |
|
|
|
|
ParentHandle: srk, |
|
|
|
|
InPrivate: tpm2.TPM2BPrivate{Buffer: data.Private}, |
|
|
|
|
InPublic: tpm2.BytesAs2B[tpm2.TPMTPublic](data.Public), |
|
|
|
|
} |
|
|
|
|
loadRes, err := loadCmd.Execute(tpm) |
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("tpm2.Load: %w", err) |
|
|
|
|
} |
|
|
|
|
defer func() { |
|
|
|
|
cmd := tpm2.FlushContext{FlushHandle: loadRes.ObjectHandle} |
|
|
|
|
if _, err := cmd.Execute(tpm); err != nil { |
|
|
|
|
log.Printf("tpm2.FlushContext: failed to flush loaded sealed blob handle: %v", err) |
|
|
|
|
} |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
// Then unseal the object.
|
|
|
|
|
unsealCmd := tpm2.Unseal{ |
|
|
|
|
ItemHandle: tpm2.NamedHandle{ |
|
|
|
|
Handle: loadRes.ObjectHandle, |
|
|
|
|
Name: loadRes.Name, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
unsealRes, err := unsealCmd.Execute(tpm) |
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("tpm2.Unseal: %w", err) |
|
|
|
|
} |
|
|
|
|
res = unsealRes.OutData.Buffer |
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
}) |
|
|
|
|
return res, err |
|
|
|
|
} |
|
|
|
|
|