types/persist: add AttestationKey (#17281)

Extend Persist with AttestationKey to record a hardware-backed
attestation key for the node's identity.

Add a flag to tailscaled to allow users to control the use of
hardware-backed keys to bind node identity to individual machines.

Updates tailscale/corp#31269


Change-Id: Idcf40d730a448d85f07f1bebf387f086d4c58be3

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
This commit is contained in:
Patrick O'Doherty
2025-10-10 10:28:36 -07:00
committed by GitHub
parent a2dc517d7d
commit e45557afc0
26 changed files with 370 additions and 42 deletions
+48
View File
@@ -0,0 +1,48 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tpm
package ipnlocal
import (
"errors"
"tailscale.com/feature"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
)
func init() {
feature.HookGenerateAttestationKeyIfEmpty.Set(generateAttestationKeyIfEmpty)
}
// generateAttestationKeyIfEmpty generates a new hardware attestation key if
// none exists. It returns true if a new key was generated and stored in
// p.AttestationKey.
func generateAttestationKeyIfEmpty(p *persist.Persist, logf logger.Logf) (bool, error) {
// attempt to generate a new hardware attestation key if none exists
var ak key.HardwareAttestationKey
if p != nil {
ak = p.AttestationKey
}
if ak == nil || ak.IsZero() {
var err error
ak, err = key.NewHardwareAttestationKey()
if err != nil {
if !errors.Is(err, key.ErrUnsupported) {
logf("failed to create hardware attestation key: %v", err)
}
} else if ak != nil {
logf("using new hardware attestation key: %v", ak.Public())
if p == nil {
p = &persist.Persist{}
}
p.AttestationKey = ak
return true, nil
}
}
return false, nil
}
+33 -5
View File
@@ -392,6 +392,23 @@ type LocalBackend struct {
//
// See tailscale/corp#29969.
overrideExitNodePolicy bool
// hardwareAttested is whether backend should use a hardware-backed key to
// bind the node identity to this device.
hardwareAttested atomic.Bool
}
// SetHardwareAttested enables hardware attestation key signatures in map
// requests, if supported on this platform. SetHardwareAttested should be called
// before Start.
func (b *LocalBackend) SetHardwareAttested() {
b.hardwareAttested.Store(true)
}
// HardwareAttested reports whether hardware-backed attestation keys should be
// used to bind the node's identity to this device.
func (b *LocalBackend) HardwareAttested() bool {
return b.hardwareAttested.Load()
}
// HealthTracker returns the health tracker for the backend.
@@ -2455,10 +2472,23 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
if b.reconcilePrefsLocked(newPrefs) {
prefsChanged = true
}
// neither UpdatePrefs or reconciliation should change Persist
newPrefs.Persist = b.pm.CurrentPrefs().Persist().AsStruct()
if buildfeatures.HasTPM {
if genKey, ok := feature.HookGenerateAttestationKeyIfEmpty.GetOk(); ok {
newKey, err := genKey(newPrefs.Persist, b.logf)
if err != nil {
b.logf("failed to populate attestation key from TPM: %v", err)
}
if newKey {
prefsChanged = true
}
}
}
if prefsChanged {
// Neither opts.UpdatePrefs nor prefs reconciliation
// is allowed to modify Persist; retain the old value.
newPrefs.Persist = b.pm.CurrentPrefs().Persist().AsStruct()
if err := b.pm.SetPrefs(newPrefs.View(), cn.NetworkProfile()); err != nil {
b.logf("failed to save updated and reconciled prefs: %v", err)
}
@@ -2491,8 +2521,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
discoPublic := b.MagicConn().DiscoPublicKey()
var err error
isNetstack := b.sys.IsNetstackRouter()
debugFlags := controlDebugFlags
if isNetstack {
+21
View File
@@ -7030,6 +7030,27 @@ func TestDisplayMessageIPNBus(t *testing.T) {
}
}
func TestHardwareAttested(t *testing.T) {
b := new(LocalBackend)
// default false
if got := b.HardwareAttested(); got != false {
t.Errorf("HardwareAttested() = %v, want false", got)
}
// set true
b.SetHardwareAttested()
if got := b.HardwareAttested(); got != true {
t.Errorf("HardwareAttested() = %v, want true after SetHardwareAttested()", got)
}
// repeat calls are safe; still true
b.SetHardwareAttested()
if got := b.HardwareAttested(); got != true {
t.Errorf("HardwareAttested() = %v, want true after second SetHardwareAttested()", got)
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
OnImport: func(pkg string) {
+13 -3
View File
@@ -19,7 +19,9 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
)
@@ -645,8 +647,8 @@ func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView)
return pm.WriteState(k, []byte(profile.Key()))
}
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
bs, err := pm.store.ReadState(key)
func (pm *profileManager) loadSavedPrefs(k ipn.StateKey) (ipn.PrefsView, error) {
bs, err := pm.store.ReadState(k)
if err == ipn.ErrStateNotExist || len(bs) == 0 {
return defaultPrefs, nil
}
@@ -654,10 +656,18 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
return ipn.PrefsView{}, err
}
savedPrefs := ipn.NewPrefs()
// if supported by the platform, create an empty hardware attestation key to use when deserializing
// to avoid type exceptions from json.Unmarshaling into an interface{}.
hw, _ := key.NewEmptyHardwareAttestationKey()
savedPrefs.Persist = &persist.Persist{
AttestationKey: hw,
}
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %v", err)
}
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty())
pm.logf("using backend prefs for %q: %v", k, savedPrefs.Pretty())
// Ignore any old stored preferences for https://login.tailscale.com
// as the control server that would override the new default of
+1
View File
@@ -151,6 +151,7 @@ func TestProfileDupe(t *testing.T) {
ID: tailcfg.UserID(user),
LoginName: fmt.Sprintf("user%d@example.com", user),
},
AttestationKey: nil,
}
}
user1Node1 := newPersist(1, 1)
+1
View File
@@ -709,6 +709,7 @@ func NewPrefs() *Prefs {
// Provide default values for options which might be missing
// from the json data for any reason. The json can still
// override them to false.
p := &Prefs{
// ControlURL is explicitly not set to signal that
// it's not yet configured, which relaxes the CLI "up"
+1 -1
View File
@@ -501,7 +501,7 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u=""}}`,
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u="" ak=-}}`,
},
{
Prefs{