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 <patrick@tailscale.com>
This commit is contained in:
Patrick O'Doherty 2025-09-15 10:11:38 -07:00 committed by GitHub
parent 17ffa80138
commit 510830ca7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 102 additions and 2 deletions

View File

@ -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.
//

View File

@ -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