mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-21 07:38:38 +00:00
feature/tpm: implement ipn.StateStore using TPM sealing (#16030)
Updates #15830 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
parent
ad0dfcb185
commit
4979ce7a94
@ -474,7 +474,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
|
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
|
||||||
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box+
|
||||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
|
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
|
||||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||||
LD golang.org/x/crypto/ssh from github.com/pkg/sftp+
|
LD golang.org/x/crypto/ssh from github.com/pkg/sftp+
|
||||||
|
@ -5,14 +5,29 @@
|
|||||||
package tpm
|
package tpm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/go-tpm/tpm2"
|
"github.com/google/go-tpm/tpm2"
|
||||||
"github.com/google/go-tpm/tpm2/transport"
|
"github.com/google/go-tpm/tpm2/transport"
|
||||||
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
"tailscale.com/atomicfile"
|
||||||
"tailscale.com/feature"
|
"tailscale.com/feature"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/store"
|
||||||
|
"tailscale.com/paths"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var infoOnce = sync.OnceValue(info)
|
var infoOnce = sync.OnceValue(info)
|
||||||
@ -22,10 +37,16 @@ func init() {
|
|||||||
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
|
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
|
||||||
hi.TPM = infoOnce()
|
hi.TPM = infoOnce()
|
||||||
})
|
})
|
||||||
|
store.Register(storePrefix, newStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 used in Linux and Windows builds only
|
func info() *tailcfg.TPMInfo {
|
||||||
func infoFromCapabilities(tpm transport.TPM) *tailcfg.TPMInfo {
|
tpm, err := open()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer tpm.Close()
|
||||||
|
|
||||||
info := new(tailcfg.TPMInfo)
|
info := new(tailcfg.TPMInfo)
|
||||||
toStr := func(s *string) func(*tailcfg.TPMInfo, uint32) {
|
toStr := func(s *string) func(*tailcfg.TPMInfo, uint32) {
|
||||||
return func(info *tailcfg.TPMInfo, value uint32) {
|
return func(info *tailcfg.TPMInfo, value uint32) {
|
||||||
@ -81,3 +102,300 @@ func propToString(v uint32) string {
|
|||||||
// Delete any non-printable ASCII characters.
|
// Delete any non-printable ASCII characters.
|
||||||
return string(slices.DeleteFunc(chars, func(b byte) bool { return b < ' ' || b > '~' }))
|
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
|
||||||
|
}
|
||||||
|
@ -4,15 +4,10 @@
|
|||||||
package tpm
|
package tpm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/google/go-tpm/tpm2/transport"
|
||||||
"github.com/google/go-tpm/tpm2/transport/linuxtpm"
|
"github.com/google/go-tpm/tpm2/transport/linuxtpm"
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func info() *tailcfg.TPMInfo {
|
func open() (transport.TPMCloser, error) {
|
||||||
t, err := linuxtpm.Open("/dev/tpm0")
|
return linuxtpm.Open("/dev/tpm0")
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer t.Close()
|
|
||||||
return infoFromCapabilities(t)
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,12 @@
|
|||||||
|
|
||||||
package tpm
|
package tpm
|
||||||
|
|
||||||
import "tailscale.com/tailcfg"
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
func info() *tailcfg.TPMInfo {
|
"github.com/google/go-tpm/tpm2/transport"
|
||||||
return nil
|
)
|
||||||
|
|
||||||
|
func open() (transport.TPMCloser, error) {
|
||||||
|
return nil, errors.New("TPM not supported on this platform")
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,17 @@
|
|||||||
|
|
||||||
package tpm
|
package tpm
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/store"
|
||||||
|
)
|
||||||
|
|
||||||
func TestPropToString(t *testing.T) {
|
func TestPropToString(t *testing.T) {
|
||||||
for prop, want := range map[uint32]string{
|
for prop, want := range map[uint32]string{
|
||||||
@ -17,3 +27,156 @@ func TestPropToString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func skipWithoutTPM(t testing.TB) {
|
||||||
|
tpm, err := open()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("TPM not available")
|
||||||
|
}
|
||||||
|
tpm.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSealUnseal(t *testing.T) {
|
||||||
|
skipWithoutTPM(t)
|
||||||
|
|
||||||
|
data := make([]byte, 100*1024)
|
||||||
|
rand.Read(data)
|
||||||
|
var key [32]byte
|
||||||
|
rand.Read(key[:])
|
||||||
|
|
||||||
|
sealed, err := seal(t.Logf, decryptedData{Key: key, Data: data})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("seal: %v", err)
|
||||||
|
}
|
||||||
|
if bytes.Contains(sealed.Data, data) {
|
||||||
|
t.Fatalf("sealed data %q contains original input %q", sealed.Data, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsealed, err := unseal(t.Logf, *sealed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unseal: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, unsealed.Data) {
|
||||||
|
t.Errorf("got unsealed data: %q, want: %q", unsealed, data)
|
||||||
|
}
|
||||||
|
if key != unsealed.Key {
|
||||||
|
t.Errorf("got unsealed key: %q, want: %q", unsealed.Key, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
skipWithoutTPM(t)
|
||||||
|
|
||||||
|
path := storePrefix + filepath.Join(t.TempDir(), "state")
|
||||||
|
store, err := newStore(t.Logf, path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkState := func(t *testing.T, store ipn.StateStore, k ipn.StateKey, want []byte) {
|
||||||
|
got, err := store.ReadState(k)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ReadState(%q): %v", k, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(want, got) {
|
||||||
|
t.Errorf("ReadState(%q): got %q, want %q", k, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
k1, k2 := ipn.StateKey("k1"), ipn.StateKey("k2")
|
||||||
|
v1, v2 := []byte("v1"), []byte("v2")
|
||||||
|
|
||||||
|
t.Run("read-non-existent-key", func(t *testing.T) {
|
||||||
|
_, err := store.ReadState(k1)
|
||||||
|
if !errors.Is(err, ipn.ErrStateNotExist) {
|
||||||
|
t.Errorf("ReadState succeeded, want %v", ipn.ErrStateNotExist)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read-write-k1", func(t *testing.T) {
|
||||||
|
if err := store.WriteState(k1, v1); err != nil {
|
||||||
|
t.Errorf("WriteState(%q, %q): %v", k1, v1, err)
|
||||||
|
}
|
||||||
|
checkState(t, store, k1, v1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read-write-k2", func(t *testing.T) {
|
||||||
|
if err := store.WriteState(k2, v2); err != nil {
|
||||||
|
t.Errorf("WriteState(%q, %q): %v", k2, v2, err)
|
||||||
|
}
|
||||||
|
checkState(t, store, k2, v2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update-k2", func(t *testing.T) {
|
||||||
|
v2 = []byte("new v2")
|
||||||
|
if err := store.WriteState(k2, v2); err != nil {
|
||||||
|
t.Errorf("WriteState(%q, %q): %v", k2, v2, err)
|
||||||
|
}
|
||||||
|
checkState(t, store, k2, v2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("reopen-store", func(t *testing.T) {
|
||||||
|
store, err := newStore(t.Logf, path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
checkState(t, store, k1, v1)
|
||||||
|
checkState(t, store, k2, v2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStore(b *testing.B) {
|
||||||
|
skipWithoutTPM(b)
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
stores := make(map[string]ipn.StateStore)
|
||||||
|
key := ipn.StateKey(b.Name())
|
||||||
|
|
||||||
|
// Set up tpmStore
|
||||||
|
tpmStore, err := newStore(b.Logf, filepath.Join(b.TempDir(), "tpm.store"))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tpmStore.WriteState(key, []byte("-1")); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
stores["tpmStore"] = tpmStore
|
||||||
|
|
||||||
|
// Set up FileStore
|
||||||
|
fileStore, err := store.NewFileStore(b.Logf, filepath.Join(b.TempDir(), "file.store"))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := fileStore.WriteState(key, []byte("-1")); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
stores["fileStore"] = fileStore
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for name, store := range stores {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
b.Run("write-noop", func(b *testing.B) {
|
||||||
|
for range b.N {
|
||||||
|
if err := store.WriteState(key, []byte("-1")); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("write", func(b *testing.B) {
|
||||||
|
for i := range b.N {
|
||||||
|
if err := store.WriteState(key, []byte(strconv.Itoa(i))); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("read", func(b *testing.B) {
|
||||||
|
for range b.N {
|
||||||
|
if _, err := store.ReadState(key); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,15 +4,10 @@
|
|||||||
package tpm
|
package tpm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/google/go-tpm/tpm2/transport"
|
||||||
"github.com/google/go-tpm/tpm2/transport/windowstpm"
|
"github.com/google/go-tpm/tpm2/transport/windowstpm"
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func info() *tailcfg.TPMInfo {
|
func open() (transport.TPMCloser, error) {
|
||||||
t, err := windowstpm.Open()
|
return windowstpm.Open()
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer t.Close()
|
|
||||||
return infoFromCapabilities(t)
|
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,8 @@ var knownStores map[string]Provider
|
|||||||
// the suffix an AWS ARN for an SSM.
|
// the suffix an AWS ARN for an SSM.
|
||||||
// - (Linux-only) if the string begins with "kube:",
|
// - (Linux-only) if the string begins with "kube:",
|
||||||
// the suffix is a Kubernetes secret name
|
// the suffix is a Kubernetes secret name
|
||||||
|
// - (Linux or Windows) if the string begins with "tpmseal:", the suffix is
|
||||||
|
// filepath that is sealed with the local TPM device.
|
||||||
// - In all other cases, the path is treated as a filepath.
|
// - In all other cases, the path is treated as a filepath.
|
||||||
func New(logf logger.Logf, path string) (ipn.StateStore, error) {
|
func New(logf logger.Logf, path string) (ipn.StateStore, error) {
|
||||||
for prefix, sf := range knownStores {
|
for prefix, sf := range knownStores {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user