Skip to content

fix(tpm2): honor gotpm:"check" tag in the unmarshaller#435

Open
evilgensec wants to merge 1 commit into
google:mainfrom
evilgensec:fix/reflect-honor-check-tag
Open

fix(tpm2): honor gotpm:"check" tag in the unmarshaller#435
evilgensec wants to merge 1 commit into
google:mainfrom
evilgensec:fix/reflect-honor-check-tag

Conversation

@evilgensec
Copy link
Copy Markdown

Summary

TPMSAttest.Magic (tpm2/structures.go:1318) is declared with gotpm:"check". The tag is meant to declare that the field's type implements Check() error and that the unmarshaller must reject a blob whose value fails the check. TPMGenerated.Check() (tpm2/structures.go:75-82) enforces g == TPMGeneratedValue (0xff544347).

unmarshalStructField in tpm2/reflect.go processes every other gotpm: tag (skip, list, sized, sized8, tag, nullable, optional, handle, auth, bit, anon) but never inspects check. Result: TPMGenerated.Check() has zero call sites anywhere in the package, the Magic field is the only field carrying the tag, and Unmarshal[TPMSAttest] happily returns a populated struct for any 4-byte Magic value.

A verifier following the pattern in tpm2/test/certify_test.go (lines 138-167) — call .Contents() to unmarshal the attest, marshal it back to compute the digest, verify the signature with the trusted AK, read Attested.Quote().PCRDigest and trust the result — therefore accepts forged "quotes" whose Magic is anything, as long as the surrounding signature verifies. The Magic field is precisely the boundary between "TPM-generated attestation" and "arbitrary bytes signed by any key the verifier trusts". Real-world signing oracles include non-restricted signing keys on the same TPM (TPM2_Sign with a non-restricted scheme does NOT prepend TPM_GENERATED_VALUE), vTPM-as-a-service back-ends, previously-leaked AKs reused for ad-hoc Sign, and cross-tenant signing endpoints.

Compare to the legacy package, which gets this right: legacy/tpm2/structures.go:667-679 (DecodeAttestationData) explicitly rejects ad.Magic != 0xff544347.

Fix

Wire the check tag into unmarshalStructField. After the field's bytes are read, if the field type implements Check() error, call it; return the error.

Test plan

  • go build ./tpm2/ clean.
  • New tpm2/check_tag_test.go adds a regression matrix for Unmarshal[TPMSAttest] that rejects Magic=0x00000000, 0xdeadbeef, and the byte-swapped lookalike 0x47ff5443, and that still accepts the legitimate 0xff544347. The tests don't import the TPM simulator dep.
  • Local PoC against main (commit ab627a9) before the fix: a hand-built 74-byte TPMSAttest with Magic=0, Type=TPM_ST_ATTEST_QUOTE, attacker-chosen 32-byte PCRDigest, attacker ECDSA signature over the blob verifies and the verifier reads parsed.Attested.Quote().PCRDigest as the attacker-chosen value. After this PR, Unmarshal[TPMSAttest] returns check tag failed for field 'Magic' of struct 'TPMSAttest': TPM_GENERATED value should be 0xff544347, was 0x0 and the verifier short-circuits.

`TPMSAttest.Magic` carries `gotpm:"check"` to declare that the field's
`TPMGenerated.Check()` (which enforces `== TPM_GENERATED_VALUE`) must run
during unmarshalling. The reflection-based unmarshaller in tpm2/reflect.go
processes every other tag (`skip`, `list`, `sized`, `sized8`, `tag`,
`nullable`, `optional`, `handle`, `auth`, `bit`, `anon`) but never inspects
`check`, so the tag is silently ignored and `TPMGenerated.Check()` has
zero call sites in the package. A verifier following the pattern in
tpm2/test/certify_test.go therefore returns a populated `TPMSAttest`
for blobs whose Magic is anything -- the integrity boundary between
"TPM-generated attestation" and "arbitrary bytes signed by any key the
verifier trusts" disappears.

Wire `check` into `unmarshalStructField`: after the field's bytes are
read, if the field type implements `Check() error`, call it and return
the error.

Adds tpm2/check_tag_test.go with a regression matrix that asserts
Unmarshal[TPMSAttest] rejects Magic=0x00000000, 0xdeadbeef, and the
byte-swapped lookalike 0x47ff5443, while still accepting the legitimate
0xff544347. The new tests don't require the TPM simulator dep.
@evilgensec evilgensec requested review from a team, alexmwu and jkl73 as code owners May 21, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant