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
+19
View File
@@ -6,6 +6,9 @@ package feature
import (
"net/http"
"net/url"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
)
// HookCanAutoUpdate is a hook for the clientupdate package
@@ -45,6 +48,8 @@ var HookProxySetTransportGetProxyConnectHeader Hook[func(*http.Transport)]
// and available.
var HookTPMAvailable Hook[func() bool]
var HookGenerateAttestationKeyIfEmpty Hook[func(p *persist.Persist, logf logger.Logf) (bool, error)]
// TPMAvailable reports whether a TPM device is supported and available.
func TPMAvailable() bool {
if f, ok := HookTPMAvailable.GetOk(); ok {
@@ -52,3 +57,17 @@ func TPMAvailable() bool {
}
return false
}
// HookHardwareAttestationAvailable is a hook that reports whether hardware
// attestation is supported and available.
var HookHardwareAttestationAvailable Hook[func() bool]
// HardwareAttestationAvailable reports whether hardware attestation is
// supported and available (TPM on Windows/Linux, Secure Enclave on macOS|iOS,
// KeyStore on Android)
func HardwareAttestationAvailable() bool {
if f, ok := HookHardwareAttestationAvailable.GetOk(); ok {
return f()
}
return false
}
+14 -1
View File
@@ -142,13 +142,18 @@ type attestationKeySerialized struct {
TPMPublic []byte `json:"tpmPublic"`
}
// MarshalJSON implements json.Marshaler.
func (ak *attestationKey) MarshalJSON() ([]byte, error) {
if ak == nil || ak.IsZero() {
return []byte("null"), nil
}
return json.Marshal(attestationKeySerialized{
TPMPublic: ak.tpmPublic.Bytes(),
TPMPrivate: ak.tpmPrivate.Buffer,
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (ak *attestationKey) UnmarshalJSON(data []byte) (retErr error) {
var aks attestationKeySerialized
if err := json.Unmarshal(data, &aks); err != nil {
@@ -254,6 +259,9 @@ func (ak *attestationKey) Close() error {
}
func (ak *attestationKey) Clone() key.HardwareAttestationKey {
if ak == nil {
return nil
}
return &attestationKey{
tpm: ak.tpm,
tpmPrivate: ak.tpmPrivate,
@@ -263,4 +271,9 @@ func (ak *attestationKey) Clone() key.HardwareAttestationKey {
}
}
func (ak *attestationKey) IsZero() bool { return !ak.loaded() }
func (ak *attestationKey) IsZero() bool {
if ak == nil {
return true
}
return !ak.loaded()
}
+2
View File
@@ -40,6 +40,8 @@ var infoOnce = sync.OnceValue(info)
func init() {
feature.Register("tpm")
feature.HookTPMAvailable.Set(tpmSupported)
feature.HookHardwareAttestationAvailable.Set(tpmSupported)
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
hi.TPM = infoOnce()
})