tailscale/feature/tpm/attestation.go
Andrew Lytvynov c5919b4ed1
feature/tpm: check IsZero in clone instead of just nil (#17884)
The key.NewEmptyHardwareAttestationKey hook returns a non-nil empty
attestationKey, which means that the nil check in Clone doesn't trigger
and proceeds to try and clone an empty key. Check IsZero instead to
reduce log spam from Clone.

As a drive-by, make tpmAvailable check a sync.Once because the result
won't change.

Updates #17882

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2025-11-14 13:23:25 -08:00

308 lines
7.3 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tpm
import (
"crypto"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"sync"
"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/tpm2/transport"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
"tailscale.com/types/key"
)
type attestationKey struct {
tpmMu sync.Mutex
tpm transport.TPMCloser
// private and public parts of the TPM key as returned from tpm2.Create.
// These are used for serialization.
tpmPrivate tpm2.TPM2BPrivate
tpmPublic tpm2.TPM2BPublic
// handle of the loaded TPM key.
handle *tpm2.NamedHandle
// pub is the parsed *ecdsa.PublicKey.
pub crypto.PublicKey
}
func newAttestationKey() (ak *attestationKey, retErr error) {
tpm, err := open()
if err != nil {
return nil, key.ErrUnsupported
}
defer func() {
if retErr != nil {
tpm.Close()
}
}()
ak = &attestationKey{tpm: tpm}
// Create a key under the storage hierarchy.
if err := withSRK(log.Printf, ak.tpm, func(srk tpm2.AuthHandle) error {
resp, err := tpm2.Create{
ParentHandle: tpm2.NamedHandle{
Handle: srk.Handle,
Name: srk.Name,
},
InPublic: tpm2.New2B(
tpm2.TPMTPublic{
Type: tpm2.TPMAlgECC,
NameAlg: tpm2.TPMAlgSHA256,
ObjectAttributes: tpm2.TPMAObject{
SensitiveDataOrigin: true,
UserWithAuth: true,
AdminWithPolicy: true,
NoDA: true,
FixedTPM: true,
FixedParent: true,
SignEncrypt: true,
},
Parameters: tpm2.NewTPMUPublicParms(
tpm2.TPMAlgECC,
&tpm2.TPMSECCParms{
CurveID: tpm2.TPMECCNistP256,
Scheme: tpm2.TPMTECCScheme{
Scheme: tpm2.TPMAlgECDSA,
Details: tpm2.NewTPMUAsymScheme(
tpm2.TPMAlgECDSA,
&tpm2.TPMSSigSchemeECDSA{
// Unfortunately, TPMs don't let us use
// TPMAlgNull here to make the hash
// algorithm dynamic higher in the
// stack. We have to hardcode it here.
HashAlg: tpm2.TPMAlgSHA256,
},
),
},
},
),
},
),
}.Execute(ak.tpm)
if err != nil {
return fmt.Errorf("tpm2.Create: %w", err)
}
ak.tpmPrivate = resp.OutPrivate
ak.tpmPublic = resp.OutPublic
return nil
}); err != nil {
return nil, err
}
return ak, ak.load()
}
func (ak *attestationKey) loaded() bool {
return ak.tpm != nil && ak.handle != nil && ak.pub != nil
}
// load the key into the TPM from its public/private components. Must be called
// before Sign or Public.
func (ak *attestationKey) load() error {
if ak.loaded() {
return nil
}
if len(ak.tpmPrivate.Buffer) == 0 || len(ak.tpmPublic.Bytes()) == 0 {
return fmt.Errorf("attestationKey.load called without tpmPrivate or tpmPublic")
}
return withSRK(log.Printf, ak.tpm, func(srk tpm2.AuthHandle) error {
resp, err := tpm2.Load{
ParentHandle: tpm2.NamedHandle{
Handle: srk.Handle,
Name: srk.Name,
},
InPrivate: ak.tpmPrivate,
InPublic: ak.tpmPublic,
}.Execute(ak.tpm)
if err != nil {
return fmt.Errorf("tpm2.Load: %w", err)
}
ak.handle = &tpm2.NamedHandle{
Handle: resp.ObjectHandle,
Name: resp.Name,
}
pub, err := ak.tpmPublic.Contents()
if err != nil {
return err
}
ak.pub, err = tpm2.Pub(*pub)
return err
})
}
// attestationKeySerialized is the JSON-serialized representation of
// attestationKey.
type attestationKeySerialized struct {
TPMPrivate []byte `json:"tpmPrivate"`
TPMPublic []byte `json:"tpmPublic"`
}
// MarshalJSON implements json.Marshaler.
func (ak *attestationKey) MarshalJSON() ([]byte, error) {
if ak == nil || len(ak.tpmPublic.Bytes()) == 0 || len(ak.tpmPrivate.Buffer) == 0 {
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 {
return err
}
ak.tpmPrivate = tpm2.TPM2BPrivate{Buffer: aks.TPMPrivate}
ak.tpmPublic = tpm2.BytesAs2B[tpm2.TPMTPublic, *tpm2.TPMTPublic](aks.TPMPublic)
ak.tpmMu.Lock()
defer ak.tpmMu.Unlock()
if ak.tpm != nil {
ak.tpm.Close()
ak.tpm = nil
}
tpm, err := open()
if err != nil {
return key.ErrUnsupported
}
defer func() {
if retErr != nil {
tpm.Close()
}
}()
ak.tpm = tpm
return ak.load()
}
func (ak *attestationKey) Public() crypto.PublicKey {
return ak.pub
}
func (ak *attestationKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
ak.tpmMu.Lock()
defer ak.tpmMu.Unlock()
if !ak.loaded() {
return nil, errors.New("tpm2 attestation key is not loaded during Sign")
}
// Unfortunately, TPMs don't let us make keys with dynamic hash algorithms.
// The hash algorithm is fixed at key creation time (tpm2.Create).
if opts != crypto.SHA256 {
return nil, fmt.Errorf("tpm2 key is restricted to SHA256, have %q", opts)
}
resp, err := tpm2.Sign{
KeyHandle: ak.handle,
Digest: tpm2.TPM2BDigest{
Buffer: digest,
},
InScheme: tpm2.TPMTSigScheme{
Scheme: tpm2.TPMAlgECDSA,
Details: tpm2.NewTPMUSigScheme(
tpm2.TPMAlgECDSA,
&tpm2.TPMSSchemeHash{
HashAlg: tpm2.TPMAlgSHA256,
},
),
},
Validation: tpm2.TPMTTKHashCheck{
Tag: tpm2.TPMSTHashCheck,
},
}.Execute(ak.tpm)
if err != nil {
return nil, fmt.Errorf("tpm2.Sign: %w", err)
}
sig, err := resp.Signature.Signature.ECDSA()
if err != nil {
return nil, err
}
return encodeSignature(sig.SignatureR.Buffer, sig.SignatureS.Buffer)
}
// Copied from crypto/ecdsa.
func encodeSignature(r, s []byte) ([]byte, error) {
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
addASN1IntBytes(b, r)
addASN1IntBytes(b, s)
})
return b.Bytes()
}
// addASN1IntBytes encodes in ASN.1 a positive integer represented as
// a big-endian byte slice with zero or more leading zeroes.
func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) {
for len(bytes) > 0 && bytes[0] == 0 {
bytes = bytes[1:]
}
if len(bytes) == 0 {
b.SetError(errors.New("invalid integer"))
return
}
b.AddASN1(asn1.INTEGER, func(c *cryptobyte.Builder) {
if bytes[0]&0x80 != 0 {
c.AddUint8(0)
}
c.AddBytes(bytes)
})
}
func (ak *attestationKey) Close() error {
ak.tpmMu.Lock()
defer ak.tpmMu.Unlock()
var errs []error
if ak.handle != nil && ak.tpm != nil {
_, err := tpm2.FlushContext{FlushHandle: ak.handle.Handle}.Execute(ak.tpm)
errs = append(errs, err)
}
if ak.tpm != nil {
errs = append(errs, ak.tpm.Close())
}
return errors.Join(errs...)
}
func (ak *attestationKey) Clone() key.HardwareAttestationKey {
if ak.IsZero() {
return nil
}
tpm, err := open()
if err != nil {
log.Printf("[unexpected] failed to open a TPM connection in feature/tpm.attestationKey.Clone: %v", err)
return nil
}
akc := &attestationKey{
tpm: tpm,
tpmPrivate: ak.tpmPrivate,
tpmPublic: ak.tpmPublic,
}
if err := akc.load(); err != nil {
log.Printf("[unexpected] failed to load TPM key in feature/tpm.attestationKey.Clone: %v", err)
tpm.Close()
return nil
}
return akc
}
func (ak *attestationKey) IsZero() bool {
if ak == nil {
return true
}
ak.tpmMu.Lock()
defer ak.tpmMu.Unlock()
return !ak.loaded()
}