mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-11 07:59:08 +02:00
The TSS2 parser was the only reason this repository depended on a crypto suite whose own build tooling it inherits. The replacement sits on go-tpm, which was already a direct dependency and is in fact what that suite calls underneath, so this removes a wrapper rather than porting onto a different library: the load, the derived storage root key and the signing commands are the same calls. Swapping a parser on the one path a customer actually runs is not something to assert, so the two are held side by side for this commit. One test feeds the replacement bytes the old library wrote and requires the same key type, empty auth flag, parent handle, blobs and decoded public key; the other feeds both the fixtures the tests are built on, so those are the shape the format calls for and not merely the shape the new parser reads. The scaffolding goes away with the dependency in the commit that follows. The encoder behind the fixtures is written out separately from the parser under test, so an encoder bug and a decoder bug cannot cancel each other out.
164 lines
5.3 KiB
Go
164 lines
5.3 KiB
Go
package tpm
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rsa"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
|
|
legacy "github.com/google/go-tpm/legacy/tpm2"
|
|
"github.com/google/go-tpm/tpmutil"
|
|
)
|
|
|
|
// KeyPEMType is the PEM block type of a TPM 2.0 key file as defined by
|
|
// draft-bottomley-tpm2-keys and written by tpm2-openssl and tpm2-tss-engine.
|
|
const KeyPEMType = "TSS2 PRIVATE KEY"
|
|
|
|
var ErrKeyNeedsAuth = errors.New("TPM key requires an authorization value")
|
|
|
|
// eccSRKTemplate is the TCG reference ECC-P256 storage root key. A key whose parent is
|
|
// a hierarchy rather than a persistent handle was wrapped by the primary this template
|
|
// derives, and tpm2-openssl and tpm2-tss-engine derive the same one, so the TPM
|
|
// reproduces the identical parent from the hierarchy seed without anything being stored.
|
|
var eccSRKTemplate = legacy.Public{
|
|
Type: legacy.AlgECC,
|
|
NameAlg: legacy.AlgSHA256,
|
|
Attributes: legacy.FlagStorageDefault | legacy.FlagNoDA,
|
|
ECCParameters: &legacy.ECCParams{
|
|
Symmetric: &legacy.SymScheme{Alg: legacy.AlgAES, KeyBits: 128, Mode: legacy.AlgCFB},
|
|
Sign: &legacy.SigScheme{Alg: legacy.AlgNull},
|
|
CurveID: legacy.CurveNISTP256,
|
|
},
|
|
}
|
|
|
|
// ParseKey reads a TSS2 key file and returns a signer that produces every signature
|
|
// inside the TPM; only the digest goes in and only the signature comes out. Keys guarded
|
|
// by an authorization value are rejected, since nothing can supply it without prompting.
|
|
func ParseKey(der []byte) (crypto.Signer, error) {
|
|
key, err := parseTSS2(der)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !key.EmptyAuth {
|
|
return nil, ErrKeyNeedsAuth
|
|
}
|
|
|
|
public, err := legacy.DecodePublic(key.PublicKey[2:])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode TSS2 public area: %w", err)
|
|
}
|
|
pub, err := public.Key()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode TSS2 public key: %w", err)
|
|
}
|
|
return &keySigner{key: key, public: pub}, nil
|
|
}
|
|
|
|
type keySigner struct {
|
|
key *tss2Key
|
|
public crypto.PublicKey
|
|
}
|
|
|
|
func (s *keySigner) Public() crypto.PublicKey {
|
|
return s.public
|
|
}
|
|
|
|
// Sign loads the key under its parent, signs, and releases both handles. The TPM is
|
|
// opened per signature so no handle outlives the call, which matters on a device whose
|
|
// transient object slots are few and shared with everything else on the host.
|
|
func (s *keySigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
|
rwc, err := Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rwc.Close() }()
|
|
|
|
parent := tpmutil.Handle(uint32(s.key.Parent)) //nolint:gosec // validParent bounds it
|
|
if !persistentHandle(s.key.Parent) {
|
|
parent, _, err = legacy.CreatePrimary(rwc, parent, legacy.PCRSelection{}, "", "", eccSRKTemplate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create TPM primary: %w", err)
|
|
}
|
|
defer func() { _ = legacy.FlushContext(rwc, parent) }()
|
|
}
|
|
|
|
public, private := s.key.blobs()
|
|
handle, _, err := legacy.Load(rwc, parent, "", public, private)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load TPM key: %w", err)
|
|
}
|
|
defer func() { _ = legacy.FlushContext(rwc, handle) }()
|
|
|
|
switch pub := s.public.(type) {
|
|
case *ecdsa.PublicKey:
|
|
return signECDSA(rwc, handle, digest, pub.Curve)
|
|
case *rsa.PublicKey:
|
|
return signRSA(rwc, handle, digest, opts)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported TPM key type %T", s.public)
|
|
}
|
|
}
|
|
|
|
// signECDSA returns the signature as the ASN.1 sequence crypto.Signer is defined to
|
|
// return; the TPM hands back the two integers on their own.
|
|
func signECDSA(rw io.ReadWriter, handle tpmutil.Handle, digest []byte, curve elliptic.Curve) ([]byte, error) {
|
|
hash, err := eccHash(curve)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sig, err := legacy.Sign(rw, handle, "", digest, nil, &legacy.SigScheme{Alg: legacy.AlgECDSA, Hash: hash})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("TPM ECDSA signature: %w", err)
|
|
}
|
|
if sig.ECC == nil {
|
|
return nil, fmt.Errorf("TPM returned a %v signature for an ECDSA key", sig.Alg)
|
|
}
|
|
return asn1.Marshal(struct{ R, S *big.Int }{sig.ECC.R, sig.ECC.S})
|
|
}
|
|
|
|
func signRSA(rw io.ReadWriter, handle tpmutil.Handle, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
|
hash, err := legacy.HashToAlgorithm(opts.HashFunc())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("TPM hash algorithm: %w", err)
|
|
}
|
|
|
|
scheme := &legacy.SigScheme{Alg: legacy.AlgRSASSA, Hash: hash}
|
|
if pss, ok := opts.(*rsa.PSSOptions); ok {
|
|
// The TPM always salts to the hash length, so a caller asking for anything
|
|
// else would get a signature it did not ask for.
|
|
if pss.SaltLength != rsa.PSSSaltLengthAuto &&
|
|
pss.SaltLength != rsa.PSSSaltLengthEqualsHash &&
|
|
pss.SaltLength != len(digest) {
|
|
return nil, fmt.Errorf("TPM cannot produce a PSS signature with salt length %d", pss.SaltLength)
|
|
}
|
|
scheme.Alg = legacy.AlgRSAPSS
|
|
}
|
|
|
|
sig, err := legacy.Sign(rw, handle, "", digest, nil, scheme)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("TPM RSA signature: %w", err)
|
|
}
|
|
if sig.RSA == nil {
|
|
return nil, fmt.Errorf("TPM returned a %v signature for an RSA key", sig.Alg)
|
|
}
|
|
return sig.RSA.Signature, nil
|
|
}
|
|
|
|
func eccHash(curve elliptic.Curve) (legacy.Algorithm, error) {
|
|
switch curve {
|
|
case elliptic.P256():
|
|
return legacy.AlgSHA256, nil
|
|
case elliptic.P384():
|
|
return legacy.AlgSHA384, nil
|
|
case elliptic.P521():
|
|
return legacy.AlgSHA512, nil
|
|
default:
|
|
return 0, fmt.Errorf("unsupported curve %s", curve.Params().Name)
|
|
}
|
|
}
|