From 510830ca7aa2987cce1e76b92efbe5fede6eff8b Mon Sep 17 00:00:00 2001 From: Patrick O'Doherty Date: Mon, 15 Sep 2025 10:11:38 -0700 Subject: [PATCH] tailcfg: add HardwareAttestationKey to MapRequest (#17102) Extend the client state management to generate a hardware attestation key if none exists. Extend MapRequest with HardwareAttestationKey{,Signature} fields that optionally contain the public component of the hardware attestation key and a signature of the node's node key using it. This will be used by control to associate hardware attesation keys with node identities on a TOFU basis. Updates tailscale/corp#31269 Signed-off-by: Patrick O'Doherty --- tailcfg/tailcfg.go | 7 +++ types/key/hardware_attestation.go | 97 ++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 94d0b19d5..6c1357a63 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -1360,6 +1360,13 @@ type MapRequest struct { NodeKey key.NodePublic DiscoKey key.DiscoPublic + // HardwareAttestationKey is the public key of the node's hardware-backed + // identity attestation key, if any. + HardwareAttestationKey key.HardwareAttestationPublic `json:",omitzero"` + // HardwareAttestationKeySignature is the signature of the NodeKey + // serialized using MarshalText using its hardware attestation key, if any. + HardwareAttestationKeySignature []byte `json:",omitempty"` + // Stream is whether the client wants to receive multiple MapResponses over // the same HTTP connection. // diff --git a/types/key/hardware_attestation.go b/types/key/hardware_attestation.go index be2eefb78..ead077a5d 100644 --- a/types/key/hardware_attestation.go +++ b/types/key/hardware_attestation.go @@ -5,12 +5,19 @@ package key import ( "crypto" + "crypto/ecdsa" + "crypto/elliptic" "encoding/json" "fmt" + "io" + + "go4.org/mem" ) var ErrUnsupported = fmt.Errorf("key type not supported on this platform") +const hardwareAttestPublicHexPrefix = "hwattestpub:" + // HardwareAttestationKey describes a hardware-backed key that is used to // identify a node. Implementation details will // vary based on the platform in use (SecureEnclave for Apple, TPM for @@ -20,10 +27,96 @@ type HardwareAttestationKey interface { crypto.Signer json.Marshaler json.Unmarshaler + io.Closer + Clone() HardwareAttestationKey +} + +// HardwareAttestationPublicFromPlatformKey creates a HardwareAttestationPublic +// for communicating the public component of the hardware attestation key +// with control and other nodes. +func HardwareAttestationPublicFromPlatformKey(k HardwareAttestationKey) HardwareAttestationPublic { + if k == nil { + return HardwareAttestationPublic{} + } + pub := k.Public() + ecdsaPub, ok := pub.(*ecdsa.PublicKey) + if !ok { + panic("hardware attestation key is not ECDSA") + } + return HardwareAttestationPublic{k: ecdsaPub} +} + +// HardwareAttestationPublic is the public key counterpart to +// HardwareAttestationKey. +type HardwareAttestationPublic struct { + k *ecdsa.PublicKey +} + +func (k HardwareAttestationPublic) Equal(o HardwareAttestationPublic) bool { + if k.k == nil || o.k == nil { + return k.k == o.k + } + return k.k.X.Cmp(o.k.X) == 0 && k.k.Y.Cmp(o.k.Y) == 0 && k.k.Curve == o.k.Curve +} + +// IsZero reports whether k is the zero value. +func (k HardwareAttestationPublic) IsZero() bool { + return k.k == nil +} + +// String returns the hex-encoded public key with a type prefix. +func (k HardwareAttestationPublic) String() string { + bs, err := k.MarshalText() + if err != nil { + panic(err) + } + return string(bs) +} + +// MarshalText implements encoding.TextMarshaler. +func (k HardwareAttestationPublic) MarshalText() ([]byte, error) { + if k.k == nil { + return nil, nil + } + return k.AppendText(nil) +} + +// UnmarshalText implements encoding.TextUnmarshaler. It expects a typed prefix +// followed by a hex encoded representation of k. +func (k *HardwareAttestationPublic) UnmarshalText(b []byte) error { + if len(b) == 0 { + *k = HardwareAttestationPublic{} + return nil + } + + kb := make([]byte, 65) + if err := parseHex(kb, mem.B(b), mem.S(hardwareAttestPublicHexPrefix)); err != nil { + return err + } + + pk, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), kb) + if err != nil { + return err + } + k.k = pk + return nil +} + +func (k HardwareAttestationPublic) AppendText(dst []byte) ([]byte, error) { + b, err := k.k.Bytes() + if err != nil { + return nil, err + } + return appendHexKey(dst, hardwareAttestPublicHexPrefix, b), nil +} + +// Verifier returns the ECDSA public key for verifying signatures made by k. +func (k HardwareAttestationPublic) Verifier() *ecdsa.PublicKey { + return k.k } // emptyHardwareAttestationKey is a function that returns an empty -// HardwareAttestationKey suitable for use with JSON unmarshalling. +// HardwareAttestationKey suitable for use with JSON unmarshaling. var emptyHardwareAttestationKey func() HardwareAttestationKey // createHardwareAttestationKey is a function that creates a new @@ -50,7 +143,7 @@ func RegisterHardwareAttestationKeyFns(emptyFn func() HardwareAttestationKey, cr } // NewEmptyHardwareAttestationKey returns an empty HardwareAttestationKey -// suitable for JSON unmarshalling. +// suitable for JSON unmarshaling. func NewEmptyHardwareAttestationKey() (HardwareAttestationKey, error) { if emptyHardwareAttestationKey == nil { return nil, ErrUnsupported