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:
Victor Lyuboslavsky 2025-07-29 20:21:09 +02:00 committed by GitHub
parent cd82898b79
commit d1992aa983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 874 additions and 530 deletions

View file

@ -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 9421compatible
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})
}

View file

@ -1,5 +1,4 @@
//go:build !linux
// +build !linux
package securehw

View 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)
})
}

View 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 9421compatible
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})
}

View file

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

View file

@ -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")
}

View file

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

View file

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

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

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