mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Added integration test for TPM. (#31315)
For #31048 This change includes some refactoring of orbit code. No functional changes. Moved non-Linux-specific code from `securehw_linux.go` to `securehw_tpm.go` so that tests on any platform can use it. There are no server changes impacting the upcoming 4.72 release. Just tests. # Checklist for submitter ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified that fleetd runs on macOS, Linux and Windows <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new TPM 2.0-based secure hardware interface, enabling creation, loading, and management of ECC keys within a TPM device. * Added support for both standard and RFC 9421-compatible HTTP signatures using TPM-backed keys. * **Bug Fixes** * Improved error handling and resource management for TPM operations. * **Tests** * Added comprehensive unit tests for TPM key file loading scenarios. * Introduced integration tests using a simulated TPM device to validate end-to-end secure hardware and SCEP workflows. * **Chores** * Updated dependencies for enhanced compatibility and security. * Modernized build constraints for improved maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
cd82898b79
commit
d1992aa983
10 changed files with 874 additions and 530 deletions
|
|
@ -3,35 +3,17 @@
|
|||
package securehw
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
keyfile "github.com/foxboron/go-tpm-keyfiles"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
"github.com/google/go-tpm/tpm2/transport/linuxtpm"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// tpm2SecureHW implements the SecureHW interface using TPM 2.0.
|
||||
type tpm2SecureHW struct {
|
||||
device transport.TPMCloser
|
||||
|
||||
logger zerolog.Logger
|
||||
keyFilePath string
|
||||
}
|
||||
|
||||
const tpm20DevicePath = "/dev/tpmrm0"
|
||||
|
||||
// Creates a new SecureHW instance using TPM 2.0.
|
||||
// Creates a new SecureHW instance using TPM 2.0 for Linux.
|
||||
// It attempts to open the TPM device using the provided configuration.
|
||||
func newSecureHW(metadataDir string, logger zerolog.Logger) (SecureHW, error) {
|
||||
if metadataDir == "" {
|
||||
|
|
@ -53,498 +35,8 @@ func newSecureHW(metadataDir string, logger zerolog.Logger) (SecureHW, error) {
|
|||
logger.Info().Str("device_path", tpm20DevicePath).Msg("successfully opened TPM 2.0 device")
|
||||
|
||||
return &tpm2SecureHW{
|
||||
device: device,
|
||||
|
||||
logger: zerolog.Nop(),
|
||||
device: device,
|
||||
logger: logger.With().Str("component", "securehw-tpm").Logger(),
|
||||
keyFilePath: filepath.Join(metadataDir, "host_identity_tpm.pem"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateKey partially implements SecureHW.
|
||||
func (t *tpm2SecureHW) CreateKey() (Key, error) {
|
||||
t.logger.Info().Msg("creating new ECC key in TPM")
|
||||
|
||||
parentKeyHandle, err := t.createParentKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get or create TPM parent key: %w", err)
|
||||
}
|
||||
|
||||
curveID, curveName := t.selectBestECCCurve()
|
||||
t.logger.Info().Str("curve", curveName).Msg("selected ECC curve for key creation")
|
||||
|
||||
// Create an ECC key template for the child key
|
||||
t.logger.Debug().Str("curve", curveName).Msg("creating ECC key template")
|
||||
eccTemplate := tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgECC,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true,
|
||||
FixedParent: true,
|
||||
SensitiveDataOrigin: true,
|
||||
UserWithAuth: true, // Required even if password is nil
|
||||
SignEncrypt: true,
|
||||
// We will just use this child key for signing.
|
||||
// If we need encryption in the future we can create a separate key for it.
|
||||
// It's usually recommended to have separate keys for signing and encryption.
|
||||
Decrypt: false,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(
|
||||
tpm2.TPMAlgECC,
|
||||
&tpm2.TPMSECCParms{
|
||||
CurveID: curveID,
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
// Create the key under the transient parent
|
||||
t.logger.Debug().Msg("creating child key")
|
||||
createKey, err := tpm2.Create{
|
||||
ParentHandle: parentKeyHandle,
|
||||
InPublic: eccTemplate,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create child key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Debug().Msg("Loading created key")
|
||||
loadedKey, err := tpm2.Load{
|
||||
ParentHandle: parentKeyHandle,
|
||||
InPrivate: createKey.OutPrivate,
|
||||
InPublic: createKey.OutPublic,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Debug().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key loaded successfully")
|
||||
|
||||
cleanUpOnError := func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: loadedKey.ObjectHandle,
|
||||
}
|
||||
_, _ = flush.Execute(t.device)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cleanUpOnError()
|
||||
return nil, fmt.Errorf("save key context: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Info().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key created and context saved successfully")
|
||||
|
||||
if err := t.saveTPMKeyFile(createKey.OutPrivate, createKey.OutPublic); err != nil {
|
||||
cleanUpOnError()
|
||||
return nil, fmt.Errorf("write TPM keyfile to file: %w", err)
|
||||
}
|
||||
|
||||
// Create and return the key
|
||||
return &tpm2Key{
|
||||
tpm: t.device,
|
||||
handle: tpm2.NamedHandle{Handle: loadedKey.ObjectHandle, Name: loadedKey.Name},
|
||||
public: createKey.OutPublic,
|
||||
logger: t.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createParentKey creates a transient Storage Root Key for use as a parent key
|
||||
//
|
||||
// NOTE: It creates the parent key deterministically so this can be called when loading a child key.
|
||||
func (t *tpm2SecureHW) createParentKey() (tpm2.NamedHandle, error) {
|
||||
t.logger.Debug().Msg("creating transient RSA 2048-bit parent key")
|
||||
|
||||
// Create a parent key template with required attributes
|
||||
parentTemplate := tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgRSA,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true, // bound to TPM that created it
|
||||
FixedParent: true, // Required, based on manual testing
|
||||
SensitiveDataOrigin: true, // key material generated internally
|
||||
UserWithAuth: true, // Required, even if we use nil password
|
||||
Decrypt: true, // Allows key to be used for decryption/unwrapping
|
||||
Restricted: true, // Limits use to decryption of child keys
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(
|
||||
tpm2.TPMAlgRSA,
|
||||
&tpm2.TPMSRSAParms{
|
||||
KeyBits: 2048,
|
||||
Symmetric: tpm2.TPMTSymDefObject{
|
||||
Algorithm: tpm2.TPMAlgAES,
|
||||
KeyBits: tpm2.NewTPMUSymKeyBits(
|
||||
tpm2.TPMAlgAES,
|
||||
tpm2.TPMKeyBits(128),
|
||||
),
|
||||
Mode: tpm2.NewTPMUSymMode(
|
||||
tpm2.TPMAlgAES,
|
||||
tpm2.TPMAlgCFB,
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
// If this command is called multiple times with the same inPublic parameter,
|
||||
// inSensitive.data, and Primary Seed, the TPM shall produce the same Primary Object.
|
||||
primaryKey, err := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: parentTemplate,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return tpm2.NamedHandle{}, fmt.Errorf("create transient parent key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Info().
|
||||
Str("handle", fmt.Sprintf("0x%x", primaryKey.ObjectHandle)).
|
||||
Msg("created transient parent key successfully")
|
||||
|
||||
// Return the transient key as a NamedHandle
|
||||
return tpm2.NamedHandle{
|
||||
Handle: primaryKey.ObjectHandle,
|
||||
Name: primaryKey.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// selectBestECCCurve checks if the TPM supports ECC P-384, otherwise returns P-256
|
||||
func (t *tpm2SecureHW) selectBestECCCurve() (tpm2.TPMECCCurve, string) {
|
||||
t.logger.Debug().Msg("checking TPM ECC curve support")
|
||||
|
||||
// Try to create a test key with P-384 to check support
|
||||
// This is a more reliable method than querying capabilities
|
||||
testTemplate := tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgECC,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true,
|
||||
FixedParent: true,
|
||||
UserWithAuth: true, // Required even if password is nil
|
||||
SensitiveDataOrigin: true,
|
||||
SignEncrypt: true,
|
||||
Decrypt: true,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(
|
||||
tpm2.TPMAlgECC,
|
||||
&tpm2.TPMSECCParms{
|
||||
CurveID: tpm2.TPMECCNistP384,
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
// Try to create a primary key with P-384 to test support
|
||||
testKey, err := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: testTemplate,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
t.logger.Debug().Err(err).Msg("TPM does not support P-384, using P-256")
|
||||
return tpm2.TPMECCNistP256, "P-256"
|
||||
}
|
||||
|
||||
// Clean up the test key
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: testKey.ObjectHandle,
|
||||
}
|
||||
_, _ = flush.Execute(t.device)
|
||||
|
||||
t.logger.Debug().Msg("TPM supports P-384")
|
||||
return tpm2.TPMECCNistP384, "P-384"
|
||||
}
|
||||
|
||||
func (t *tpm2SecureHW) saveTPMKeyFile(privateKey tpm2.TPM2BPrivate, publicKey tpm2.TPM2BPublic) error {
|
||||
k := keyfile.NewTPMKey(
|
||||
keyfile.OIDLoadableKey,
|
||||
publicKey,
|
||||
privateKey,
|
||||
keyfile.WithDescription("fleetd httpsig key"),
|
||||
)
|
||||
if err := os.WriteFile(t.keyFilePath, k.Bytes(), 0o600); err != nil {
|
||||
return fmt.Errorf("failed to save keyfile: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tpm2SecureHW) loadTPMKeyFile() (privateKey *tpm2.TPM2BPrivate, publicKey *tpm2.TPM2BPublic, err error) {
|
||||
keyfileBytes, err := os.ReadFile(t.keyFilePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, ErrKeyNotFound{}
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to read keyfile path: %w", err)
|
||||
}
|
||||
k, err := keyfile.Decode(keyfileBytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to decode keyfile: %w", err)
|
||||
}
|
||||
return &k.Privkey, &k.Pubkey, nil
|
||||
}
|
||||
|
||||
// LoadKey partially implements SecureHW.
|
||||
func (t *tpm2SecureHW) LoadKey() (Key, error) {
|
||||
private, public, err := t.loadTPMKeyFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the parent key handle.
|
||||
//
|
||||
// NOTE: createParentKey calls CreatePrimary which creates the parent key
|
||||
// deterministically so this can be called when loadind a child key.
|
||||
parentKeyHandle, err := t.createParentKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get parent key: %w", err)
|
||||
}
|
||||
|
||||
// Load the key using the parent handle.
|
||||
t.logger.Debug().Uint32("parent_handle", uint32(parentKeyHandle.Handle)).Msg("loading parent key")
|
||||
loadedKey, err := tpm2.Load{
|
||||
ParentHandle: parentKeyHandle,
|
||||
InPrivate: *private,
|
||||
InPublic: *public,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load parent key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Debug().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key loaded successfully")
|
||||
|
||||
t.logger.Info().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key loaded successfully")
|
||||
|
||||
return &tpm2Key{
|
||||
tpm: t.device,
|
||||
handle: tpm2.NamedHandle{
|
||||
Handle: loadedKey.ObjectHandle,
|
||||
Name: loadedKey.Name,
|
||||
},
|
||||
public: *public,
|
||||
logger: t.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close partially implements SecureHW.
|
||||
func (t *tpm2SecureHW) Close() error {
|
||||
t.logger.Info().Msg("closing TPM device")
|
||||
if t.device != nil {
|
||||
err := t.device.Close()
|
||||
if err != nil {
|
||||
t.logger.Error().Err(err).Msg("error closing TPM device")
|
||||
return err
|
||||
}
|
||||
t.device = nil
|
||||
t.logger.Debug().Msg("TPM device closed successfully")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tpm2Key implements the Key interface using TPM 2.0.
|
||||
type tpm2Key struct {
|
||||
tpm transport.TPMCloser
|
||||
handle tpm2.NamedHandle
|
||||
public tpm2.TPM2BPublic
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (k *tpm2Key) Signer() (crypto.Signer, error) {
|
||||
signer, _, err := k.createSigner(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
func (k *tpm2Key) HTTPSigner() (HTTPSigner, error) {
|
||||
signer, algo, err := k.createSigner(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpSigner{
|
||||
Signer: signer,
|
||||
algo: algo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type httpSigner struct {
|
||||
crypto.Signer
|
||||
algo ECCAlgorithm
|
||||
}
|
||||
|
||||
func (h *httpSigner) ECCAlgorithm() ECCAlgorithm {
|
||||
return h.algo
|
||||
}
|
||||
|
||||
func (k *tpm2Key) createSigner(httpsign bool) (s crypto.Signer, algo ECCAlgorithm, err error) {
|
||||
pub, err := k.public.Contents()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get public key contents: %w", err)
|
||||
}
|
||||
|
||||
if pub.Type != tpm2.TPMAlgECC {
|
||||
return nil, 0, errors.New("not an ECC key")
|
||||
}
|
||||
|
||||
eccDetail, err := pub.Parameters.ECCDetail()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get ECC details: %w", err)
|
||||
}
|
||||
|
||||
eccUnique, err := pub.Unique.ECC()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get ECC unique: %w", err)
|
||||
}
|
||||
|
||||
// Create crypto.PublicKey based on curve
|
||||
var publicKey *ecdsa.PublicKey
|
||||
switch eccDetail.CurveID {
|
||||
case tpm2.TPMECCNistP256:
|
||||
publicKey = &ecdsa.PublicKey{
|
||||
Curve: elliptic.P256(),
|
||||
X: new(big.Int).SetBytes(eccUnique.X.Buffer),
|
||||
Y: new(big.Int).SetBytes(eccUnique.Y.Buffer),
|
||||
}
|
||||
algo = ECCAlgorithmP256
|
||||
case tpm2.TPMECCNistP384:
|
||||
publicKey = &ecdsa.PublicKey{
|
||||
Curve: elliptic.P384(),
|
||||
X: new(big.Int).SetBytes(eccUnique.X.Buffer),
|
||||
Y: new(big.Int).SetBytes(eccUnique.Y.Buffer),
|
||||
}
|
||||
algo = ECCAlgorithmP384
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("unsupported ECC curve: %v", eccDetail.CurveID)
|
||||
}
|
||||
|
||||
return &tpm2Signer{
|
||||
tpm: k.tpm,
|
||||
handle: k.handle,
|
||||
publicKey: publicKey,
|
||||
httpsign: httpsign,
|
||||
}, algo, nil
|
||||
}
|
||||
|
||||
func (k *tpm2Key) Public() (crypto.PublicKey, error) {
|
||||
signer, err := k.Signer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer.Public(), nil
|
||||
}
|
||||
|
||||
func (k *tpm2Key) Close() error {
|
||||
if k.handle.Handle != 0 {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: k.handle.Handle,
|
||||
}
|
||||
_, err := flush.Execute(k.tpm)
|
||||
k.handle = tpm2.NamedHandle{}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tpm2Signer implements crypto.Signer using TPM 2.0.
|
||||
type tpm2Signer struct {
|
||||
tpm transport.TPM
|
||||
handle tpm2.NamedHandle
|
||||
publicKey *ecdsa.PublicKey
|
||||
httpsign bool // true for RFC 9421-compatible HTTP signatures, false for standard ECDSA
|
||||
}
|
||||
|
||||
// _ ensures tpm2Signer satisfies the crypto.Signer interface at compile time.
|
||||
var _ crypto.Signer = (*tpm2Signer)(nil)
|
||||
|
||||
func (s *tpm2Signer) Public() crypto.PublicKey {
|
||||
return s.publicKey
|
||||
}
|
||||
|
||||
func (s *tpm2Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||
// Determine hash algorithm
|
||||
var hashAlg tpm2.TPMAlgID
|
||||
switch opts.HashFunc() {
|
||||
case crypto.SHA256:
|
||||
hashAlg = tpm2.TPMAlgSHA256
|
||||
case crypto.SHA384:
|
||||
hashAlg = tpm2.TPMAlgSHA384
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported hash function: %v", opts.HashFunc())
|
||||
}
|
||||
|
||||
// Sign with TPM using ECDSA.
|
||||
// ECC keys are used with ECDSA (Elliptic Curve Digital Signature Algorithm) for signing
|
||||
sign := tpm2.Sign{
|
||||
KeyHandle: s.handle,
|
||||
Digest: tpm2.TPM2BDigest{
|
||||
Buffer: digest,
|
||||
},
|
||||
InScheme: tpm2.TPMTSigScheme{
|
||||
Scheme: tpm2.TPMAlgECDSA,
|
||||
Details: tpm2.NewTPMUSigScheme(
|
||||
tpm2.TPMAlgECDSA,
|
||||
&tpm2.TPMSSchemeHash{
|
||||
HashAlg: hashAlg,
|
||||
},
|
||||
),
|
||||
},
|
||||
Validation: tpm2.TPMTTKHashCheck{
|
||||
Tag: tpm2.TPMSTHashCheck,
|
||||
},
|
||||
}
|
||||
|
||||
rsp, err := sign.Execute(s.tpm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TPM sign: %w", err)
|
||||
}
|
||||
|
||||
// Check signature type and extract ECDSA signature
|
||||
if rsp.Signature.SigAlg != tpm2.TPMAlgECDSA {
|
||||
return nil, fmt.Errorf("unexpected signature algorithm: %v", rsp.Signature.SigAlg)
|
||||
}
|
||||
|
||||
// Get the ECDSA signature
|
||||
ecdsaSig, err := rsp.Signature.Signature.ECDSA()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get ECDSA signature: %w", err)
|
||||
}
|
||||
|
||||
if s.httpsign {
|
||||
// RFC 9421-compatible HTTP signature format: fixed-width r||s
|
||||
curveBits := s.publicKey.Curve.Params().BitSize
|
||||
coordSize := (curveBits + 7) / 8 // bytes per coordinate
|
||||
|
||||
// Allocate the output buffer
|
||||
sig := make([]byte, 2*coordSize)
|
||||
|
||||
// Copy R, left-padded
|
||||
sigR := ecdsaSig.SignatureR.Buffer
|
||||
if len(sigR) > coordSize {
|
||||
return nil, fmt.Errorf("TPM ECDSA signature R too long: got %d bytes, expected max %d", len(sigR), coordSize)
|
||||
}
|
||||
copy(sig[coordSize-len(sigR):coordSize], sigR)
|
||||
|
||||
// Copy S, left-padded
|
||||
sigS := ecdsaSig.SignatureS.Buffer
|
||||
if len(sigS) > coordSize {
|
||||
return nil, fmt.Errorf("TPM ECDSA signature S too long: got %d bytes, expected max %d", len(sigS), coordSize)
|
||||
}
|
||||
copy(sig[2*coordSize-len(sigS):], sigS)
|
||||
|
||||
// The final signature contains r||s, fixed-width, RFC 9421–compatible
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// Standard ECDSA signature format for certificate signing requests
|
||||
// Convert TPM signature components to ASN.1 DER format
|
||||
sigR := new(big.Int).SetBytes(ecdsaSig.SignatureR.Buffer)
|
||||
sigS := new(big.Int).SetBytes(ecdsaSig.SignatureS.Buffer)
|
||||
|
||||
// Encode as ASN.1 DER sequence manually
|
||||
type ecdsaSignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
return asn1.Marshal(ecdsaSignature{R: sigR, S: sigS})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package securehw
|
||||
|
||||
|
|
|
|||
119
ee/orbit/pkg/securehw/securehw_test.go
Normal file
119
ee/orbit/pkg/securehw/securehw_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//go:build !windows
|
||||
|
||||
// Windows is disabled because the TPM simulator requires CGO, which causes lint failures on Windows.
|
||||
|
||||
package securehw
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-tpm/tpm2/transport/simulator"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadTPMKeyFile(t *testing.T) {
|
||||
// Create a temporary directory for test files
|
||||
tempDir := t.TempDir()
|
||||
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
// Create a TPM simulator
|
||||
sim, err := simulator.OpenSimulator()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, sim.Close())
|
||||
})
|
||||
|
||||
t.Run("missing key file", func(t *testing.T) {
|
||||
// Create tpm2SecureHW instance with non-existent key file path
|
||||
hw := &tpm2SecureHW{
|
||||
device: sim,
|
||||
logger: logger,
|
||||
keyFilePath: filepath.Join(tempDir, "non_existent_key.pem"),
|
||||
}
|
||||
|
||||
// Try to load the key file
|
||||
privateKey, publicKey, err := hw.loadTPMKeyFile()
|
||||
|
||||
// Should return ErrKeyNotFound
|
||||
require.Error(t, err)
|
||||
var keyNotFoundErr ErrKeyNotFound
|
||||
require.ErrorAs(t, err, &keyNotFoundErr)
|
||||
require.Nil(t, privateKey)
|
||||
require.Nil(t, publicKey)
|
||||
})
|
||||
|
||||
t.Run("invalid key file format", func(t *testing.T) {
|
||||
// Create a file with invalid content
|
||||
invalidKeyPath := filepath.Join(tempDir, "invalid_key.pem")
|
||||
err = os.WriteFile(invalidKeyPath, []byte("this is not a valid TPM key file"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create tpm2SecureHW instance
|
||||
hw := &tpm2SecureHW{
|
||||
device: sim,
|
||||
logger: logger,
|
||||
keyFilePath: invalidKeyPath,
|
||||
}
|
||||
|
||||
// Try to load the invalid key file
|
||||
privateKey, publicKey, err := hw.loadTPMKeyFile()
|
||||
|
||||
// Should return an error about decoding
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to decode keyfile")
|
||||
require.Nil(t, privateKey)
|
||||
require.Nil(t, publicKey)
|
||||
})
|
||||
|
||||
t.Run("empty key file", func(t *testing.T) {
|
||||
// Create an empty file
|
||||
emptyKeyPath := filepath.Join(tempDir, "empty_key.pem")
|
||||
err = os.WriteFile(emptyKeyPath, []byte{}, 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create tpm2SecureHW instance
|
||||
hw := &tpm2SecureHW{
|
||||
device: sim,
|
||||
logger: logger,
|
||||
keyFilePath: emptyKeyPath,
|
||||
}
|
||||
|
||||
// Try to load the empty key file
|
||||
privateKey, publicKey, err := hw.loadTPMKeyFile()
|
||||
|
||||
// Should return an error about decoding
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to decode keyfile")
|
||||
require.Nil(t, privateKey)
|
||||
require.Nil(t, publicKey)
|
||||
})
|
||||
|
||||
t.Run("PEM formatted but not TPM key", func(t *testing.T) {
|
||||
// Create a file with valid PEM but not a TPM key
|
||||
pemKeyPath := filepath.Join(tempDir, "not_tpm_key.pem")
|
||||
pemContent := `-----BEGIN CERTIFICATE-----
|
||||
MIIBkTCB+wIJAKHHIG...
|
||||
-----END CERTIFICATE-----`
|
||||
err = os.WriteFile(pemKeyPath, []byte(pemContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create tpm2SecureHW instance
|
||||
hw := &tpm2SecureHW{
|
||||
device: sim,
|
||||
logger: logger,
|
||||
keyFilePath: pemKeyPath,
|
||||
}
|
||||
|
||||
// Try to load the PEM file that's not a TPM key
|
||||
privateKey, publicKey, err := hw.loadTPMKeyFile()
|
||||
|
||||
// Should return an error about decoding
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to decode keyfile")
|
||||
require.Nil(t, privateKey)
|
||||
require.Nil(t, publicKey)
|
||||
})
|
||||
}
|
||||
535
ee/orbit/pkg/securehw/securehw_tpm.go
Normal file
535
ee/orbit/pkg/securehw/securehw_tpm.go
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
package securehw
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
keyfile "github.com/foxboron/go-tpm-keyfiles"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// tpm2SecureHW implements the SecureHW interface using TPM 2.0.
|
||||
type tpm2SecureHW struct {
|
||||
device transport.TPMCloser
|
||||
|
||||
logger zerolog.Logger
|
||||
keyFilePath string
|
||||
}
|
||||
|
||||
// NewTestSecureHW creates a new SecureHW instance for testing with a custom TPM transport.
|
||||
// This allows injecting a TPM simulator for integration tests.
|
||||
// Note: This function is only meant for testing with TPM simulators.
|
||||
func NewTestSecureHW(device transport.TPMCloser, metadataDir string, logger zerolog.Logger) (SecureHW, error) {
|
||||
if metadataDir == "" {
|
||||
return nil, ErrKeyNotFound{Message: "required metadata directory not set"}
|
||||
}
|
||||
|
||||
if device == nil {
|
||||
return nil, errors.New("TPM device is required")
|
||||
}
|
||||
|
||||
logger.Info().Msg("initializing test TPM connection")
|
||||
|
||||
// On non-Linux platforms where tpm2SecureHW is not available,
|
||||
// we create a minimal test implementation that uses the same interface
|
||||
// This allows the integration tests to run with a TPM simulator
|
||||
return &tpm2SecureHW{
|
||||
device: device,
|
||||
logger: logger.With().Str("component", "securehw-test").Logger(),
|
||||
keyFilePath: filepath.Join(metadataDir, "host_identity_tpm_test.pem"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateKey partially implements SecureHW.
|
||||
func (t *tpm2SecureHW) CreateKey() (Key, error) {
|
||||
t.logger.Info().Msg("creating new ECC key in TPM")
|
||||
|
||||
parentKeyHandle, err := t.createParentKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get or create TPM parent key: %w", err)
|
||||
}
|
||||
|
||||
curveID, curveName := t.selectBestECCCurve()
|
||||
t.logger.Info().Str("curve", curveName).Msg("selected ECC curve for key creation")
|
||||
|
||||
// Create an ECC key template for the child key
|
||||
t.logger.Debug().Str("curve", curveName).Msg("creating ECC key template")
|
||||
eccTemplate := tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgECC,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true,
|
||||
FixedParent: true,
|
||||
SensitiveDataOrigin: true,
|
||||
UserWithAuth: true, // Required even if password is nil
|
||||
SignEncrypt: true,
|
||||
// We will just use this child key for signing.
|
||||
// If we need encryption in the future we can create a separate key for it.
|
||||
// It's usually recommended to have separate keys for signing and encryption.
|
||||
Decrypt: false,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(
|
||||
tpm2.TPMAlgECC,
|
||||
&tpm2.TPMSECCParms{
|
||||
CurveID: curveID,
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
// Create the key under the transient parent
|
||||
t.logger.Debug().Msg("creating child key")
|
||||
createKey, err := tpm2.Create{
|
||||
ParentHandle: parentKeyHandle,
|
||||
InPublic: eccTemplate,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create child key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Debug().Msg("Loading created key")
|
||||
loadedKey, err := tpm2.Load{
|
||||
ParentHandle: parentKeyHandle,
|
||||
InPrivate: createKey.OutPrivate,
|
||||
InPublic: createKey.OutPublic,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Debug().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key loaded successfully")
|
||||
|
||||
cleanUpOnError := func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: loadedKey.ObjectHandle,
|
||||
}
|
||||
_, _ = flush.Execute(t.device)
|
||||
}
|
||||
|
||||
t.logger.Info().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key created and context saved successfully")
|
||||
|
||||
if err := t.saveTPMKeyFile(createKey.OutPrivate, createKey.OutPublic); err != nil {
|
||||
cleanUpOnError()
|
||||
return nil, fmt.Errorf("write TPM keyfile to file: %w", err)
|
||||
}
|
||||
|
||||
// Create and return the key
|
||||
return &tpm2Key{
|
||||
tpm: t.device,
|
||||
handle: tpm2.NamedHandle{Handle: loadedKey.ObjectHandle, Name: loadedKey.Name},
|
||||
public: createKey.OutPublic,
|
||||
logger: t.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createParentKey creates a transient Storage Root Key for use as a parent key
|
||||
//
|
||||
// NOTE: It creates the parent key deterministically so this can be called when loading a child key.
|
||||
func (t *tpm2SecureHW) createParentKey() (tpm2.NamedHandle, error) {
|
||||
t.logger.Debug().Msg("creating transient RSA 2048-bit parent key")
|
||||
|
||||
// Create a parent key template with required attributes
|
||||
parentTemplate := tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgRSA,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true, // bound to TPM that created it
|
||||
FixedParent: true, // Required, based on manual testing
|
||||
SensitiveDataOrigin: true, // key material generated internally
|
||||
UserWithAuth: true, // Required, even if we use nil password
|
||||
Decrypt: true, // Allows key to be used for decryption/unwrapping
|
||||
Restricted: true, // Limits use to decryption of child keys
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(
|
||||
tpm2.TPMAlgRSA,
|
||||
&tpm2.TPMSRSAParms{
|
||||
KeyBits: 2048,
|
||||
Symmetric: tpm2.TPMTSymDefObject{
|
||||
Algorithm: tpm2.TPMAlgAES,
|
||||
KeyBits: tpm2.NewTPMUSymKeyBits(
|
||||
tpm2.TPMAlgAES,
|
||||
tpm2.TPMKeyBits(128),
|
||||
),
|
||||
Mode: tpm2.NewTPMUSymMode(
|
||||
tpm2.TPMAlgAES,
|
||||
tpm2.TPMAlgCFB,
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
// If this command is called multiple times with the same inPublic parameter,
|
||||
// inSensitive.data, and Primary Seed, the TPM shall produce the same Primary Object.
|
||||
primaryKey, err := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: parentTemplate,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return tpm2.NamedHandle{}, fmt.Errorf("create transient parent key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Info().
|
||||
Str("handle", fmt.Sprintf("0x%x", primaryKey.ObjectHandle)).
|
||||
Msg("created transient parent key successfully")
|
||||
|
||||
// Return the transient key as a NamedHandle
|
||||
return tpm2.NamedHandle{
|
||||
Handle: primaryKey.ObjectHandle,
|
||||
Name: primaryKey.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// selectBestECCCurve checks if the TPM supports ECC P-384, otherwise returns P-256
|
||||
func (t *tpm2SecureHW) selectBestECCCurve() (tpm2.TPMECCCurve, string) {
|
||||
t.logger.Debug().Msg("checking TPM ECC curve support")
|
||||
|
||||
// Try to create a test key with P-384 to check support
|
||||
// This is a more reliable method than querying capabilities
|
||||
testTemplate := tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgECC,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true,
|
||||
FixedParent: true,
|
||||
UserWithAuth: true, // Required even if password is nil
|
||||
SensitiveDataOrigin: true,
|
||||
SignEncrypt: true,
|
||||
Decrypt: true,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(
|
||||
tpm2.TPMAlgECC,
|
||||
&tpm2.TPMSECCParms{
|
||||
CurveID: tpm2.TPMECCNistP384,
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
// Try to create a primary key with P-384 to test support
|
||||
testKey, err := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: testTemplate,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
t.logger.Debug().Err(err).Msg("TPM does not support P-384, using P-256")
|
||||
return tpm2.TPMECCNistP256, "P-256"
|
||||
}
|
||||
|
||||
// Clean up the test key
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: testKey.ObjectHandle,
|
||||
}
|
||||
_, _ = flush.Execute(t.device)
|
||||
|
||||
t.logger.Debug().Msg("TPM supports P-384")
|
||||
return tpm2.TPMECCNistP384, "P-384"
|
||||
}
|
||||
|
||||
func (t *tpm2SecureHW) saveTPMKeyFile(privateKey tpm2.TPM2BPrivate, publicKey tpm2.TPM2BPublic) error {
|
||||
k := keyfile.NewTPMKey(
|
||||
keyfile.OIDLoadableKey,
|
||||
publicKey,
|
||||
privateKey,
|
||||
keyfile.WithDescription("fleetd httpsig key"),
|
||||
)
|
||||
if err := os.WriteFile(t.keyFilePath, k.Bytes(), 0o600); err != nil {
|
||||
return fmt.Errorf("failed to save keyfile: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tpm2SecureHW) loadTPMKeyFile() (privateKey *tpm2.TPM2BPrivate, publicKey *tpm2.TPM2BPublic, err error) {
|
||||
keyfileBytes, err := os.ReadFile(t.keyFilePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, ErrKeyNotFound{}
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to read keyfile path: %w", err)
|
||||
}
|
||||
k, err := keyfile.Decode(keyfileBytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to decode keyfile: %w", err)
|
||||
}
|
||||
return &k.Privkey, &k.Pubkey, nil
|
||||
}
|
||||
|
||||
// LoadKey partially implements SecureHW.
|
||||
func (t *tpm2SecureHW) LoadKey() (Key, error) {
|
||||
private, public, err := t.loadTPMKeyFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the parent key handle.
|
||||
//
|
||||
// NOTE: createParentKey calls CreatePrimary which creates the parent key
|
||||
// deterministically so this can be called when loadind a child key.
|
||||
parentKeyHandle, err := t.createParentKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get parent key: %w", err)
|
||||
}
|
||||
|
||||
// Load the key using the parent handle.
|
||||
t.logger.Debug().Uint32("parent_handle", uint32(parentKeyHandle.Handle)).Msg("loading parent key")
|
||||
loadedKey, err := tpm2.Load{
|
||||
ParentHandle: parentKeyHandle,
|
||||
InPrivate: *private,
|
||||
InPublic: *public,
|
||||
}.Execute(t.device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load parent key: %w", err)
|
||||
}
|
||||
|
||||
t.logger.Debug().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key loaded successfully")
|
||||
|
||||
t.logger.Info().
|
||||
Str("handle", fmt.Sprintf("0x%x", loadedKey.ObjectHandle)).
|
||||
Msg("key loaded successfully")
|
||||
|
||||
return &tpm2Key{
|
||||
tpm: t.device,
|
||||
handle: tpm2.NamedHandle{
|
||||
Handle: loadedKey.ObjectHandle,
|
||||
Name: loadedKey.Name,
|
||||
},
|
||||
public: *public,
|
||||
logger: t.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close partially implements SecureHW.
|
||||
func (t *tpm2SecureHW) Close() error {
|
||||
t.logger.Info().Msg("closing TPM device")
|
||||
if t.device != nil {
|
||||
err := t.device.Close()
|
||||
if err != nil {
|
||||
t.logger.Error().Err(err).Msg("error closing TPM device")
|
||||
return err
|
||||
}
|
||||
t.device = nil
|
||||
t.logger.Debug().Msg("TPM device closed successfully")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tpm2Key implements the Key interface using TPM 2.0.
|
||||
type tpm2Key struct {
|
||||
tpm transport.TPMCloser
|
||||
handle tpm2.NamedHandle
|
||||
public tpm2.TPM2BPublic
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (k *tpm2Key) Signer() (crypto.Signer, error) {
|
||||
signer, _, err := k.createSigner(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
func (k *tpm2Key) HTTPSigner() (HTTPSigner, error) {
|
||||
signer, algo, err := k.createSigner(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpSigner{
|
||||
Signer: signer,
|
||||
algo: algo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type httpSigner struct {
|
||||
crypto.Signer
|
||||
algo ECCAlgorithm
|
||||
}
|
||||
|
||||
func (h *httpSigner) ECCAlgorithm() ECCAlgorithm {
|
||||
return h.algo
|
||||
}
|
||||
|
||||
func (k *tpm2Key) createSigner(httpsign bool) (s crypto.Signer, algo ECCAlgorithm, err error) {
|
||||
pub, err := k.public.Contents()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get public key contents: %w", err)
|
||||
}
|
||||
|
||||
if pub.Type != tpm2.TPMAlgECC {
|
||||
return nil, 0, errors.New("not an ECC key")
|
||||
}
|
||||
|
||||
eccDetail, err := pub.Parameters.ECCDetail()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get ECC details: %w", err)
|
||||
}
|
||||
|
||||
eccUnique, err := pub.Unique.ECC()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get ECC unique: %w", err)
|
||||
}
|
||||
|
||||
// Create crypto.PublicKey based on curve
|
||||
var publicKey *ecdsa.PublicKey
|
||||
switch eccDetail.CurveID {
|
||||
case tpm2.TPMECCNistP256:
|
||||
publicKey = &ecdsa.PublicKey{
|
||||
Curve: elliptic.P256(),
|
||||
X: new(big.Int).SetBytes(eccUnique.X.Buffer),
|
||||
Y: new(big.Int).SetBytes(eccUnique.Y.Buffer),
|
||||
}
|
||||
algo = ECCAlgorithmP256
|
||||
case tpm2.TPMECCNistP384:
|
||||
publicKey = &ecdsa.PublicKey{
|
||||
Curve: elliptic.P384(),
|
||||
X: new(big.Int).SetBytes(eccUnique.X.Buffer),
|
||||
Y: new(big.Int).SetBytes(eccUnique.Y.Buffer),
|
||||
}
|
||||
algo = ECCAlgorithmP384
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("unsupported ECC curve: %v", eccDetail.CurveID)
|
||||
}
|
||||
|
||||
return &tpm2Signer{
|
||||
tpm: k.tpm,
|
||||
handle: k.handle,
|
||||
publicKey: publicKey,
|
||||
httpsign: httpsign,
|
||||
}, algo, nil
|
||||
}
|
||||
|
||||
func (k *tpm2Key) Public() (crypto.PublicKey, error) {
|
||||
signer, err := k.Signer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer.Public(), nil
|
||||
}
|
||||
|
||||
func (k *tpm2Key) Close() error {
|
||||
if k.handle.Handle != 0 {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: k.handle.Handle,
|
||||
}
|
||||
_, err := flush.Execute(k.tpm)
|
||||
k.handle = tpm2.NamedHandle{}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tpm2Signer implements crypto.Signer using TPM 2.0.
|
||||
type tpm2Signer struct {
|
||||
tpm transport.TPM
|
||||
handle tpm2.NamedHandle
|
||||
publicKey *ecdsa.PublicKey
|
||||
httpsign bool // true for RFC 9421-compatible HTTP signatures, false for standard ECDSA
|
||||
}
|
||||
|
||||
// _ ensures tpm2Signer satisfies the crypto.Signer interface at compile time.
|
||||
var _ crypto.Signer = (*tpm2Signer)(nil)
|
||||
|
||||
func (s *tpm2Signer) Public() crypto.PublicKey {
|
||||
return s.publicKey
|
||||
}
|
||||
|
||||
func (s *tpm2Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||
// Determine hash algorithm
|
||||
var hashAlg tpm2.TPMAlgID
|
||||
switch opts.HashFunc() {
|
||||
case crypto.SHA256:
|
||||
hashAlg = tpm2.TPMAlgSHA256
|
||||
case crypto.SHA384:
|
||||
hashAlg = tpm2.TPMAlgSHA384
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported hash function: %v", opts.HashFunc())
|
||||
}
|
||||
|
||||
// Sign with TPM using ECDSA.
|
||||
// ECC keys are used with ECDSA (Elliptic Curve Digital Signature Algorithm) for signing
|
||||
sign := tpm2.Sign{
|
||||
KeyHandle: s.handle,
|
||||
Digest: tpm2.TPM2BDigest{
|
||||
Buffer: digest,
|
||||
},
|
||||
InScheme: tpm2.TPMTSigScheme{
|
||||
Scheme: tpm2.TPMAlgECDSA,
|
||||
Details: tpm2.NewTPMUSigScheme(
|
||||
tpm2.TPMAlgECDSA,
|
||||
&tpm2.TPMSSchemeHash{
|
||||
HashAlg: hashAlg,
|
||||
},
|
||||
),
|
||||
},
|
||||
Validation: tpm2.TPMTTKHashCheck{
|
||||
Tag: tpm2.TPMSTHashCheck,
|
||||
},
|
||||
}
|
||||
|
||||
rsp, err := sign.Execute(s.tpm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TPM sign: %w", err)
|
||||
}
|
||||
|
||||
// Check signature type and extract ECDSA signature
|
||||
if rsp.Signature.SigAlg != tpm2.TPMAlgECDSA {
|
||||
return nil, fmt.Errorf("unexpected signature algorithm: %v", rsp.Signature.SigAlg)
|
||||
}
|
||||
|
||||
// Get the ECDSA signature
|
||||
ecdsaSig, err := rsp.Signature.Signature.ECDSA()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get ECDSA signature: %w", err)
|
||||
}
|
||||
|
||||
if s.httpsign {
|
||||
// RFC 9421-compatible HTTP signature format: fixed-width r||s
|
||||
curveBits := s.publicKey.Curve.Params().BitSize
|
||||
coordSize := (curveBits + 7) / 8 // bytes per coordinate
|
||||
|
||||
// Allocate the output buffer
|
||||
sig := make([]byte, 2*coordSize)
|
||||
|
||||
// Copy R, left-padded
|
||||
sigR := ecdsaSig.SignatureR.Buffer
|
||||
if len(sigR) > coordSize {
|
||||
return nil, fmt.Errorf("TPM ECDSA signature R too long: got %d bytes, expected max %d", len(sigR), coordSize)
|
||||
}
|
||||
copy(sig[coordSize-len(sigR):coordSize], sigR)
|
||||
|
||||
// Copy S, left-padded
|
||||
sigS := ecdsaSig.SignatureS.Buffer
|
||||
if len(sigS) > coordSize {
|
||||
return nil, fmt.Errorf("TPM ECDSA signature S too long: got %d bytes, expected max %d", len(sigS), coordSize)
|
||||
}
|
||||
copy(sig[2*coordSize-len(sigS):], sigS)
|
||||
|
||||
// The final signature contains r||s, fixed-width, RFC 9421–compatible
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// Standard ECDSA signature format for certificate signing requests
|
||||
// Convert TPM signature components to ASN.1 DER format
|
||||
sigR := new(big.Int).SetBytes(ecdsaSig.SignatureR.Buffer)
|
||||
sigS := new(big.Int).SetBytes(ecdsaSig.SignatureS.Buffer)
|
||||
|
||||
// Encode as ASN.1 DER sequence manually
|
||||
type ecdsaSignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
return asn1.Marshal(ecdsaSignature{R: sigR, S: sigS})
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
//go:build !windows
|
||||
|
||||
// Windows is disabled because the TPM simulator requires CGO, which causes lint failures on Windows.
|
||||
|
||||
package hostidentity
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
//go:build !windows
|
||||
|
||||
// Windows is disabled because the TPM simulator requires CGO, which causes lint failures on Windows.
|
||||
|
||||
package hostidentity
|
||||
|
||||
import (
|
||||
|
|
@ -12,17 +16,23 @@ import (
|
|||
"fmt"
|
||||
mathrand "math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
orbitscep "github.com/fleetdm/fleet/v4/ee/orbit/pkg/scep"
|
||||
"github.com/fleetdm/fleet/v4/ee/orbit/pkg/securehw"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttpsig"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/scep/x509util"
|
||||
"github.com/fleetdm/fleet/v4/server/service/contract"
|
||||
"github.com/google/go-tpm/tpm2/transport/simulator"
|
||||
"github.com/remitly-oss/httpsig-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/smallstep/scep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -40,6 +50,7 @@ func TestHostIdentity(t *testing.T) {
|
|||
{"GetCertAndSignReq", testGetCertAndSignReq},
|
||||
{"GetCertFailures", testGetCertFailures},
|
||||
{"WrongCertAuthentication", testWrongCertAuthentication},
|
||||
{"RealSecureHWAndSCEP", testRealSecureHWAndSCEP},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -201,15 +212,11 @@ func createHTTPSigner(t *testing.T, eccPrivateKey *ecdsa.PrivateKey, cert *x509.
|
|||
}
|
||||
|
||||
// Create signer
|
||||
signer, err := httpsig.NewSigner(httpsig.SigningProfile{
|
||||
Algorithm: algo,
|
||||
// We are not using @target-uri in the signature so that we don't run into issues with HTTPS forwarding and proxies (http vs https).
|
||||
Fields: httpsig.Fields("@method", "@authority", "@path", "@query", "content-digest"),
|
||||
Metadata: []httpsig.Metadata{httpsig.MetaKeyID, httpsig.MetaCreated, httpsig.MetaNonce},
|
||||
}, httpsig.SigningKey{
|
||||
Key: eccPrivateKey,
|
||||
MetaKeyID: fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
||||
})
|
||||
signer, err := fleethttpsig.Signer(
|
||||
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
||||
eccPrivateKey,
|
||||
algo,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return signer
|
||||
}
|
||||
|
|
@ -749,14 +756,11 @@ func testWrongCertAuthentication(t *testing.T, s *Suite) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Create a signer using the local private key with a fake certificate serial
|
||||
localSigner, err := httpsig.NewSigner(httpsig.SigningProfile{
|
||||
Algorithm: httpsig.Algo_ECDSA_P384_SHA384,
|
||||
Fields: httpsig.Fields("@method", "@authority", "@path", "@query", "content-digest"),
|
||||
Metadata: []httpsig.Metadata{httpsig.MetaKeyID, httpsig.MetaCreated, httpsig.MetaNonce},
|
||||
}, httpsig.SigningKey{
|
||||
Key: localPrivateKey,
|
||||
MetaKeyID: "999999", // Fake certificate serial number
|
||||
})
|
||||
localSigner, err := fleethttpsig.Signer(
|
||||
"999999", // Fake certificate serial number
|
||||
localPrivateKey,
|
||||
httpsig.Algo_ECDSA_P384_SHA384,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
enrollRequest := contract.EnrollOrbitRequest{
|
||||
|
|
@ -985,3 +989,181 @@ func testWrongCertAuthentication(t *testing.T, s *Suite) {
|
|||
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with wrong certificate should fail even after other hosts are enrolled")
|
||||
})
|
||||
}
|
||||
|
||||
// testRealSecureHWAndSCEP uses the SecureHW and SCEP packages that are used by Orbit. Only the TPM device is fake/simulated.
|
||||
func testRealSecureHWAndSCEP(t *testing.T, s *Suite) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create TPM simulator
|
||||
sim, err := simulator.OpenSimulator()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a temporary directory for metadata
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create a zerolog logger for the test
|
||||
zerologLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
// Create SecureHW instance with TPM simulator
|
||||
tpmHW, err := securehw.NewTestSecureHW(sim, tempDir, zerologLogger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new key in the TPM
|
||||
tpmKey, err := tpmHW.CreateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set up cleanup in reverse order - keys first, then hardware, then simulator
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, tpmHW.Close())
|
||||
})
|
||||
|
||||
// Verify we can get the public key
|
||||
pubKey, err := tpmKey.Public()
|
||||
require.NoError(t, err)
|
||||
eccPubKey, ok := pubKey.(*ecdsa.PublicKey)
|
||||
require.True(t, ok, "Expected ECC public key")
|
||||
|
||||
// Create enrollment secret
|
||||
err = s.DS.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{
|
||||
{
|
||||
Secret: testEnrollmentSecret,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate a unique common name
|
||||
commonName := generateRandomString(16)
|
||||
|
||||
// Create SCEP client with the TPM key
|
||||
scepClient, err := orbitscep.NewClient(
|
||||
orbitscep.WithSigningKey(tpmKey),
|
||||
orbitscep.WithURL(fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", s.Server.URL)),
|
||||
orbitscep.WithCommonName(commonName),
|
||||
orbitscep.WithChallenge(testEnrollmentSecret),
|
||||
orbitscep.WithLogger(zerologLogger),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fetch certificate using SCEP
|
||||
cert, err := scepClient.FetchCert(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cert)
|
||||
|
||||
// Verify certificate properties
|
||||
assert.Equal(t, commonName, cert.Subject.CommonName)
|
||||
assert.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm)
|
||||
|
||||
// Verify the certificate's public key matches our TPM key
|
||||
certPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||
require.True(t, ok, "Certificate should contain ECC public key")
|
||||
assert.True(t, eccPubKey.Equal(certPubKey), "Certificate public key should match TPM key")
|
||||
|
||||
// Test enrollment with HTTP signature using TPM key
|
||||
enrollRequest := contract.EnrollOrbitRequest{
|
||||
EnrollSecret: testEnrollmentSecret,
|
||||
HardwareUUID: "test-uuid-" + commonName,
|
||||
HardwareSerial: "test-serial-" + commonName,
|
||||
Hostname: "test-hostname-" + commonName,
|
||||
OsqueryIdentifier: commonName,
|
||||
}
|
||||
|
||||
reqBody, err := json.Marshal(enrollRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Get HTTP signer from TPM key
|
||||
httpSigner, err := tpmKey.HTTPSigner()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Determine algorithm based on the curve
|
||||
var algo httpsig.Algorithm
|
||||
switch httpSigner.ECCAlgorithm() {
|
||||
case securehw.ECCAlgorithmP256:
|
||||
algo = httpsig.Algo_ECDSA_P256_SHA256
|
||||
case securehw.ECCAlgorithmP384:
|
||||
algo = httpsig.Algo_ECDSA_P384_SHA384
|
||||
default:
|
||||
t.Fatalf("Unsupported ECC algorithm from TPM")
|
||||
}
|
||||
|
||||
// Create HTTP signature signer
|
||||
signer, err := fleethttpsig.Signer(
|
||||
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
||||
httpSigner,
|
||||
algo,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Sign the request
|
||||
err = signer.Sign(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send the signed request
|
||||
client := fleethttp.NewClient()
|
||||
httpResp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
// The request with a valid HTTP signature should succeed
|
||||
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Request with TPM-based HTTP signature should succeed")
|
||||
|
||||
// Parse the response
|
||||
var enrollResp enrollOrbitResponse
|
||||
err = json.NewDecoder(httpResp.Body).Decode(&enrollResp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, enrollResp.OrbitNodeKey, "Should receive orbit node key")
|
||||
require.NoError(t, enrollResp.Err)
|
||||
|
||||
// Test that we can load the key from storage
|
||||
require.NoError(t, tpmKey.Close()) // Close the original key
|
||||
|
||||
loadedKey, err := tpmHW.LoadKey()
|
||||
require.NoError(t, err)
|
||||
// Close the loaded key at the end of this section
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, loadedKey.Close())
|
||||
})
|
||||
|
||||
// Verify loaded key has same public key
|
||||
loadedPubKey, err := loadedKey.Public()
|
||||
require.NoError(t, err)
|
||||
loadedECCPubKey, ok := loadedPubKey.(*ecdsa.PublicKey)
|
||||
require.True(t, ok, "Loaded key should be ECC")
|
||||
assert.True(t, eccPubKey.Equal(loadedECCPubKey), "Loaded key should match original")
|
||||
|
||||
// Test config endpoint with loaded key
|
||||
configRequest := orbitConfigRequest{
|
||||
OrbitNodeKey: enrollResp.OrbitNodeKey,
|
||||
}
|
||||
|
||||
configReqBody, err := json.Marshal(configRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
configReq, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(configReqBody))
|
||||
require.NoError(t, err)
|
||||
configReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Sign with loaded key
|
||||
loadedHTTPSigner, err := loadedKey.HTTPSigner()
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedSigner, err := fleethttpsig.Signer(
|
||||
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
||||
loadedHTTPSigner,
|
||||
algo,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = loadedSigner.Sign(configReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpResp, err = client.Do(configReq)
|
||||
require.NoError(t, err)
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Config request with loaded TPM key should succeed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
//go:build !windows
|
||||
|
||||
// Windows is disabled because the TPM simulator requires CGO, which causes lint failures on Windows.
|
||||
|
||||
package hostidentity
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
//go:build !windows
|
||||
|
||||
// Windows is disabled because the TPM simulator requires CGO, which causes lint failures on Windows.
|
||||
|
||||
package hostidentity
|
||||
|
||||
import (
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -248,6 +248,7 @@ require (
|
|||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
github.com/google/go-tpm-tools v0.4.5 // indirect
|
||||
github.com/google/rpmpack v0.0.0-20210518075352-dc539ef4f2ea // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
|
|
@ -331,7 +332,7 @@ require (
|
|||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -496,6 +496,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
|||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k=
|
||||
github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
|
|
@ -1063,6 +1065,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
|
|
|
|||
Loading…
Reference in a new issue