mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-05 17:32:02 +01:00
DA protection is not super helpful because we don't set an authorization password on the key. But if authorization fails for other reasons (like TPM being reset), we will eventually cause DA lockout with tailscaled trying to load the key. DA lockout then leads to (1) issues for other processes using the TPM and (2) the underlying authorization error being masked in logs. Updates #17654 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
310 lines
7.3 KiB
Go
310 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,
|
|
// We don't set an authorization policy on this key, so
|
|
// DA isn't helpful.
|
|
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()
|
|
}
|