Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions integration-tests/data/results/enacttrust.freshness-fail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"TPM_ENACTTRUST": {
"ear.status": "contraindicated",
"ear.trustworthiness-vector": {
"configuration": 99,
"executables": 99,
"file-system": 99,
"hardware": 99,
"instance-identity": 99,
"runtime-opaque": 99,
"sourced-data": 99,
"storage-opaque": 99
},
"ear.appraisal-policy-id": "policy:TPM_ENACTTRUST",
"ear.veraison.policy-claims": {
"problem": "integrity validation failed: bad evidence: freshness: quote nonce (414a7c174141b3d0e9a1d28af31520f0d42299feac4007ded89d68ae6cd92f19) does not match session nonce (75e69d6de79f75e69d6de79f75e69d6de79f75e69d6de79f75e69d6de79f75e6)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ marks:
vals:
- [psa, p1, good, full, ec.p256, freshness-fail, nonce32]
- [cca, _, good, full, ccakeys, freshness-fail, nonce64]
- [enacttrust, _, good, mini, ec.p256.enacttrust, freshness-fail, nonce32]

includes:
- !include common.yaml
Expand Down
8 changes: 6 additions & 2 deletions integration-tests/utils/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def generate_evidence(scheme, evidence, nonce, signing, outname):
f'data/keys/{key}.pem',
f'{GENDIR}/evidence/{outname}.cbor',
badnode,
nonce,
)
else:
raise ValueError(f'Unexpected scheme: {scheme}')
Expand Down Expand Up @@ -288,9 +289,12 @@ def generate_cca_evidence_token(claims_file, iak_file, rak_file, token_file):
run_command(evcli_command, 'generate CCA token')


def generate_enacttrust_evidence_token(claims_file, key_file, token_file, badnode):
def generate_enacttrust_evidence_token(claims_file, key_file, token_file, badnode=False, nonce=None):
bn_flag = '-bad-node' if badnode else ''
gentoken_command = f"gen-enacttrust-token {bn_flag} -key {key_file} -out {token_file} {claims_file}"
# Bind the session nonce into TPMS_ATTEST.ExtraData so the scheme's freshness
# check passes. Left out when no nonce is used.
nonce_flag = f'-nonce {nonce}' if nonce else ''
gentoken_command = f"gen-enacttrust-token {bn_flag} {nonce_flag} -key {key_file} -out {token_file} {claims_file}"
run_command(gentoken_command, 'generate EnactTrust token')


Expand Down
12 changes: 12 additions & 0 deletions scheme/tpm-enacttrust/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package tpm_enacttrust

import (
"bytes"
"encoding/hex"
"fmt"

"github.com/google/go-tpm/tpm2"
Expand Down Expand Up @@ -98,6 +99,17 @@ func (o *Implementation) ValidateEvidenceIntegrity(
return handler.BadEvidence("could not verify evidence signature: %w", err)
}

// Freshness: the attester binds the session nonce into the quote's qualifying
// data (TPMS_ATTEST.ExtraData). A quote whose ExtraData does not match the
// session nonce is stale or replayed, so reject it here. See issue #427.
if !bytes.Equal([]byte(decoded.AttestationData.ExtraData), evidence.Nonce) {
return handler.BadEvidence(
"freshness: quote nonce (%s) does not match session nonce (%s)",
hex.EncodeToString(decoded.AttestationData.ExtraData),
hex.EncodeToString(evidence.Nonce),
)
}

return nil
}

Expand Down
108 changes: 108 additions & 0 deletions scheme/tpm-enacttrust/scheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2026 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package tpm_enacttrust

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/binary"
"encoding/pem"
"testing"

"github.com/google/go-tpm/tpm2"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/veraison/corim/comid"
"github.com/veraison/services/vts/appraisal"
)

// makeToken assembles an EnactTrust token (NODE_ID||SIZE||TPMS_ATTEST||
// TPMT_SIGNATURE) that binds the given nonce into TPMS_ATTEST.ExtraData and
// signs the TPMS_ATTEST bytes with the given key.
func makeToken(t *testing.T, key *ecdsa.PrivateKey, nonce []byte) []byte {
t.Helper()

attest := tpm2.AttestationData{
Magic: 0xff544347,
Type: tpm2.TagAttestQuote,
ExtraData: nonce,
AttestedQuoteInfo: &tpm2.QuoteInfo{
PCRSelection: tpm2.PCRSelection{Hash: tpm2.AlgSHA256, PCRs: []int{1, 2, 3, 4}},
PCRDigest: make([]byte, 32),
},
}
attestBytes, err := attest.Encode()
require.NoError(t, err)

digest := sha256.Sum256(attestBytes)
r, s, err := ecdsa.Sign(rand.Reader, key, digest[:])
require.NoError(t, err)

sig := tpm2.Signature{
Alg: tpm2.AlgECDSA,
ECC: &tpm2.SignatureECC{HashAlg: tpm2.AlgSHA256, R: r, S: s},
}
sigBytes, err := sig.Encode()
require.NoError(t, err)

nodeID, err := uuid.Parse("7df7714e-aa04-4638-bcbf-434b1dd720f1")
require.NoError(t, err)
nodeBytes, err := nodeID.MarshalBinary()
require.NoError(t, err)

buf := new(bytes.Buffer)
require.NoError(t, binary.Write(buf, binary.BigEndian, nodeBytes))
require.NoError(t, binary.Write(buf, binary.BigEndian, uint16(len(attestBytes))))
require.NoError(t, binary.Write(buf, binary.BigEndian, attestBytes))
require.NoError(t, binary.Write(buf, binary.BigEndian, sigBytes))

return buf.Bytes()
}

// trustAnchor wraps an ECDSA public key as the single verification key of a
// trust anchor, matching what the scheme expects from a CoRIM.
func trustAnchor(t *testing.T, pub *ecdsa.PublicKey) []*comid.KeyTriple {
t.Helper()

der, err := x509.MarshalPKIXPublicKey(pub)
require.NoError(t, err)
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})

key, err := comid.NewPKIXBase64Key(string(pemBytes))
require.NoError(t, err)

return []*comid.KeyTriple{{VerifKeys: comid.CryptoKeys{key}}}
}

// TestValidateEvidenceIntegrity_NonceMatch checks that a signed quote whose
// bound nonce equals the session nonce passes the integrity check.
func TestValidateEvidenceIntegrity_NonceMatch(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

nonce := []byte{0x80, 0x0c, 0xc8, 0x5c, 0x41, 0xd2, 0xea, 0x83}
evidence := &appraisal.Evidence{Data: makeToken(t, key, nonce), Nonce: nonce}

err = (&Implementation{}).ValidateEvidenceIntegrity(evidence, trustAnchor(t, &key.PublicKey), nil)
require.NoError(t, err)
}

// TestValidateEvidenceIntegrity_NonceMismatch checks that a correctly-signed but
// stale quote (bound nonce differs from the session nonce) is rejected, so a
// captured quote cannot be replayed. See issue #427.
func TestValidateEvidenceIntegrity_NonceMismatch(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

boundNonce := []byte{0x80, 0x0c, 0xc8, 0x5c, 0x41, 0xd2, 0xea, 0x83}
sessionNonce := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}
evidence := &appraisal.Evidence{Data: makeToken(t, key, boundNonce), Nonce: sessionNonce}

err = (&Implementation{}).ValidateEvidenceIntegrity(evidence, trustAnchor(t, &key.PublicKey), nil)
require.Error(t, err)
require.Contains(t, err.Error(), "freshness")
}
23 changes: 22 additions & 1 deletion scheme/tpm-enacttrust/test/cmd/gen-token/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/json"
"encoding/pem"
Expand Down Expand Up @@ -56,6 +57,15 @@ func readTokenDescription(path string) (*TokenDescription, error) {
return &desc, nil
}

// decodeNonce decodes a base64 nonce, accepting standard or URL-safe encoding
// so it matches whatever the verification session used.
func decodeNonce(s string) ([]byte, error) {
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
return b, nil
}
return base64.URLEncoding.DecodeString(s)
}

func readPrivateKey(path string) (*ecdsa.PrivateKey, error) {
buf, err := os.ReadFile(path)
if err != nil {
Expand All @@ -76,11 +86,13 @@ func readPrivateKey(path string) (*ecdsa.PrivateKey, error) {
}

func main() {
var keyPath, outPath string
var keyPath, outPath, noncePath string
var badNode bool
var marshaledNodeID []byte
flag.StringVar(&keyPath, "key", "key.pem", "Path of the ECDSA key used to sign the token data encoded in PEM.")
flag.StringVar(&outPath, "out", "quote.bin", "Output path of the generated token.")
flag.StringVar(&noncePath, "nonce", "",
"Base64 nonce to bind into TPMS_ATTEST.ExtraData. Accepts standard or URL-safe encoding. Left empty when omitted.")
flag.BoolVar(&badNode, "bad-node", false,
"Allow node-id to not be a valid UUID. If this is set, the bytes of the string will be written as-is, rather than attempting to parse UUID out of it. No length check or any other validation will be performed.")
flag.Parse()
Expand Down Expand Up @@ -119,6 +131,15 @@ func main() {
os.Exit(1)
}

if noncePath != "" {
nonce, err := decodeNonce(noncePath)
if err != nil {
fmt.Printf("ERROR: could not decode nonce: %v\n", err)
os.Exit(1)
}
d.ExtraData = nonce
}

attest, err := d.Encode()
if err != nil {
fmt.Printf("ERROR: could not encode attestation data: %v\n", err)
Expand Down
Loading