20 KiB
@suaveplan/tpm-struct — TPM 2.0 attestation-structure parser
Package: @suaveplan/tpm-struct (NEW)
Status: PROPOSED
Type: new-package (genesis-original; greenfield — no upstream port)
Date: 2026-05-11
Path: packages/security/tpm-struct/
Why
@suaveplan/webauthn's tpm attestation-statement verifier (W3C WebAuthn L3 §8.3 — TPM Attestation Statement Format) is blocked on a universal-tier TPM 2.0 attestation-structure parser. The verifier MUST decode TPMS_ATTEST from the attestationData field (TCG TPM 2.0 Library Part 2 §10.12), MUST decode TPMT_PUBLIC from the pubArea field (TCG Part 2 §12.2), MUST compute the canonical TPM name digest of that public area (TCG Part 1 §16) so it can match it against authData.attestedCredentialData.credentialPublicKey, and MUST verify that extraData from the attest blob equals the attToBeSigned digest the relying party recomputes (WebAuthn L3 §8.3 step 8). External libraries (tpm-js, node-tss, tss.js) either drag in a TPM driver, target Node-only, ship CommonJS with no type information, or implement only a sliver of Part 2 §10/§11/§12. An in-house, spec-cited, zero-dep, buffer-only parser keeps webauthn universal-tier and gives every future TPM 2.0 attestation context (AIK enrollment per TCG Credential Profile EK 2.0, remote attestation, vTPM provisioning) one canonical implementation with one error registry, one OTel surface, and one set of test vectors.
The same parser is webauthn-agnostic. The public surface decodes TPM 2.0 byte strings; it neither imports nor knows about WebAuthn's attStmt map, authData, or COSE keys.
What changes
New package: @suaveplan/tpm-struct
- ADDED:
packages/security/tpm-struct/package scaffold (package.json,tsconfig.lib.json,vite.config.ts,vitest.config.ts,README.md). - ADDED:
src/buffer/buffer.ts—TpmBufferReader(readUint8,readUint16Be,readUint32Be,readUint64Be: bigint,readBytes(n),readTpm2b(): Uint8Array,peek(n),remaining(),assertEnd()) andTpmBufferWriter(matching write surface pluswriteTpm2b(bytes),toUint8Array()) per TCG Part 2 §10.1 octet-stream conventions; every read pastbytes.lengthraisesTPM_STRUCT.TRUNCATEDcarrying cursor + requested-length incause. - ADDED:
src/buffer/buffer.types.ts—TpmReaderOptions,TpmWriterOptions,Tpm2bReadResult. - ADDED:
src/alg/alg.ts—TPM_ALGfrozen registry mirroring TCG Part 2 §11.1 (NULL = 0x0010,RSA = 0x0001,SHA1 = 0x0004,HMAC = 0x0005,AES = 0x0006,KEYEDHASH = 0x0008,SHA256 = 0x000b,SHA384 = 0x000c,SHA512 = 0x000d,XOR = 0x000a,RSASSA = 0x0014,RSAES = 0x0015,RSAPSS = 0x0016,OAEP = 0x0017,ECDSA = 0x0018,ECDH = 0x0019,ECDAA = 0x001a,SM2 = 0x001b,ECSCHNORR = 0x001c,ECMQV = 0x001d,KDF1_SP800_56A = 0x0020,KDF2 = 0x0021,KDF1_SP800_108 = 0x0022,ECC = 0x0023,SYMCIPHER = 0x0025,CTR = 0x0040,OFB = 0x0041,CBC = 0x0042,CFB = 0x0043,ECB = 0x0044);TPM_ECC_CURVEfrozen registry (NIST_P256 = 0x0003,NIST_P384 = 0x0004,NIST_P521 = 0x0005,BN_P256 = 0x0010,BN_P638 = 0x0011,SM2_P256 = 0x0020);algIdToName(id)returns the symbolic name; unknown IDs raiseTPM_STRUCT.ALG_UNSUPPORTED.eccCurveToNamedoes the same for curves; unknown raisesTPM_STRUCT.CURVE_UNSUPPORTED. - ADDED:
src/alg/alg.types.ts—TpmAlgId,TpmEccCurveliteral unions. - ADDED:
src/public/public.ts—parseTpmtPublic(bytes): TpmtPublicper TCG Part 2 §12.2; readstype(TPMI_ALG_PUBLIC),nameAlg(TPMI_ALG_HASH),objectAttributes(32-bit TPMA_OBJECT per §8.3 — exposes rawuint32and named-flag accessor),authPolicy(TPM2B_DIGEST),parameters(TPMS_RSA_PARMSper §12.2.3.5 withsymmetricTPMT_SYM_DEF_OBJECT,schemeTPMT_RSA_SCHEME,keyBits,exponent(uint32,0⇒ default 65537)),parameters(TPMS_ECC_PARMSper §12.2.3.6 withsymmetric,schemeTPMT_ECC_SCHEME,curveIDTPMI_ECC_CURVE,kdfTPMT_KDF_SCHEME),unique(TPM2B_PUBLIC_KEY_RSAfor RSA orTPMS_ECC_POINT{ x, y }of twoTPM2B_ECC_PARAMETERs for ECC). KEYEDHASH and SYMCIPHER branches decode theirTPMS_*_PARMSper §12.2.3.3 / §12.2.3.4 and return the same discriminated-union shape. - ADDED:
src/public/public.types.ts—TpmtPublicdiscriminator ontype;TpmsRsaParms,TpmsEccParms,TpmsKeyedHashParms,TpmsSymCipherParms,TpmsEccPoint,ObjectAttributes(typed flag struct + rawuint32slot). - ADDED:
src/public/serialize.ts—serializeTpmtPublic(pub): Uint8Arrayproduces byte-identical re-serialisation of anyparseTpmtPublicoutput; powerscomputeTpmtPublicName. - ADDED:
src/attest/attest.ts—parseTpmsAttest(bytes): TpmsAttestper TCG Part 2 §10.12; validates 4-byte magic0xff544347(TPM_GENERATED_VALUE, ASCII "TCG\xff" in big-endian); readstype(TPMI_ST_ATTEST—TPM_ST_ATTEST_CERTIFY = 0x8017,TPM_ST_ATTEST_QUOTE = 0x8018,TPM_ST_ATTEST_SESSION_AUDIT = 0x8016,TPM_ST_ATTEST_COMMAND_AUDIT = 0x8015,TPM_ST_ATTEST_TIME = 0x8019,TPM_ST_ATTEST_CREATION = 0x801a,TPM_ST_ATTEST_NV = 0x8014,TPM_ST_ATTEST_NV_DIGEST = 0x801c),qualifiedSigner(TPM2B_NAME),extraData(TPM2B_DATA),clockInfo(17 bytes: uint64 clock, uint32 resetCount, uint32 restartCount, uint8 safe),firmwareVersion(uint64), and dispatchesattestedto the matchingTPMU_ATTESTmember (per §10.11). ForTPM_ST_ATTEST_CERTIFYtheattestedisTPMS_CERTIFY_INFO(§10.5) —name+qualifiedNamebothTPM2B_NAME. - ADDED:
src/attest/attest.types.ts—TpmsAttestdiscriminator ontype;TpmsCertifyInfo,TpmsQuoteInfo,TpmsSessionAuditInfo,TpmsCommandAuditInfo,TpmsTimeAttestInfo,TpmsCreationInfo,TpmsNvCertifyInfo,TpmsNvDigestCertifyInfo,ClockInfo. - ADDED:
src/attest/serialize.ts—serializeTpmsAttest(att): Uint8Arrayproduces byte-identical re-serialisation of anyparseTpmsAttestoutput. - ADDED:
src/name/name.ts—computeTpmtPublicName(pub, hash): Uint8Arrayper TCG Part 1 §16. Output =nameAlg (2 BE bytes) || H_nameAlg(serializeTpmtPublic(pub)). Hash backend injected viaHashLike(caller supplies a SHA-1/256/384/512 implementation matchingnameAlg; the package itself imports no hash code).nameAlg === TPM_ALG_NULLis rejected withTPM_STRUCT.NAME_ALG_NULL. - ADDED:
src/name/name.types.ts—HashLikeinterface (digest(bytes: Uint8Array): Uint8Array). - ADDED:
src/extra-data/extra-data.ts—extraDataMatches(actual: Uint8Array, expected: Uint8Array, equal: ConstantTimeEqual): boolean; the comparison function is injected to keep the package zero-runtime-dep on crypto-utils. - ADDED:
src/extra-data/extra-data.types.ts—ConstantTimeEqualfunction signature. - ADDED:
src/schemas/schemas.ts— Zod boundary schemas:TpmsAttestSchema,TpmtPublicSchema,TpmsRsaParmsSchema,TpmsEccParmsSchema,TpmsKeyedHashParmsSchema,TpmsSymCipherParmsSchema,TpmsCertifyInfoSchema,ClockInfoSchema,Tpm2bSchema,TpmAlgIdSchema,TpmEccCurveSchema,ObjectAttributesSchema. Every public parser validates output through the matching schema; failure raisesTPM_STRUCT.MALFORMEDwith Zod issues attached ascause.issues. - ADDED:
src/errors/error-codes.ts—defineCodes("TPM_STRUCT", [...])registry:MAGIC_INVALID,TRUNCATED,ALG_UNSUPPORTED,CURVE_UNSUPPORTED,TYPE_MISMATCH,MALFORMED,NAME_ALG_NULL,ATTEST_TYPE_UNSUPPORTED,PARAMETERS_INVALID,OBJECT_ATTRIBUTES_RESERVED_NONZERO.TpmStructError extends SuaveplanError<TpmStructErrorCode>. - ADDED:
src/observability/metrics.ts— histogramstpm_struct.parse_tpms_attest_duration_ms{outcome},tpm_struct.parse_tpmt_public_duration_ms{type,outcome},tpm_struct.compute_name_duration_ms{name_alg}; counterstpm_struct.parse_total{kind,outcome},tpm_struct.parse_errors_total{code},tpm_struct.alg_total{alg}. All attributes low-cardinality enums. - ADDED:
src/diagnostic/diagnostic.ts—toDiagnostic(value: TpmsAttest | TpmtPublic): stringhuman-readable dump withTPM_ALGsymbolic names inlined; debug-only; never invoked from production hot paths. - ADDED:
src/__fixtures__/webauthn-tpm-vector.ts— known-good real-world WebAuthntpm-format attestation byte strings (attestationData+pubArea) plus expected parsed structures and expected name digest. Used by the WebAuthn integration scenario. - ADDED:
src/__benchmarks__/parse-tpms-attest.bench.ts,parse-tpmt-public-rsa2048.bench.ts,parse-tpmt-public-ecc-p256.bench.ts,compute-name.bench.ts— mitata suites. - ADDED:
src/index.ts— explicit named re-exports; noexport *(peropenspec/AGENTS.md§6a).
Subpath exports
.— root barrel:parseTpmsAttest,parseTpmtPublic,computeTpmtPublicName,extraDataMatches,TPM_ALG,TPM_ECC_CURVE, error codes../buffer—TpmBufferReader,TpmBufferWriter, types../alg—TPM_ALG,TPM_ECC_CURVE,algIdToName,eccCurveToName, literal-union types../public—parseTpmtPublic,serializeTpmtPublic,TpmtPublic, parameter / point / attribute types../attest—parseTpmsAttest,serializeTpmsAttest,TpmsAttest, everyTPMU_ATTESTmember type,ClockInfo../name—computeTpmtPublicName,HashLike../extra-data—extraDataMatches,ConstantTimeEqual../schemas— Zod boundary schemas../errors—TPM_STRUCT_CODES,TpmStructError,TpmStructErrorCode../diagnostic—toDiagnostic.
Dependencies
@suaveplan/error workspace:^—defineCodes,SuaveplanErrorbase.@suaveplan/types workspace:^— shared interface contracts.@opentelemetry/api catalog:observability— histograms + counters.tslib catalog:build.zod catalog:forms— option + result boundary validation.- No
node:*imports, notpm-js/node-tss/tss.js/@noble/*/@stablelib/*. Pure TypeScript overUint8Array+DataView.
Out of scope
- TPM I/O / TPM commands — no
TPM2_*command marshalling, no TSS device driver, no/dev/tpmrm0or WindowsTbsi_*integration, no TPM2_TOOLS shell wrapping. Justification: this package is buffer-only; TPM device interaction is the job of a TSS-shaped package that is not currently planned. Folding it in would require a Node-only target and pull in driver-level concerns that no current consumer needs. - TPM key creation / sealing / unsealing — no
TPM2_Create, noTPM2_Load, no auth-session orchestration. Justification: this is a parser/serialiser, not a key manager. - TPM session management / HMAC / parameter encryption — no session contexts, no encryption sessions, no parameter encryption per TCG Part 1 §19. Justification: same — buffer parsing only.
- AIK / EK credential validation chain — TCG Credential Profile EK 2.0 procedures (EK certificate verification, manufacturer-CA pin) are the WebAuthn verifier's job; this package returns the parsed structures it needs to do that work.
- TPM 1.2 structures — TPM 1.2 (
TPM_PUBKEY,TPM_QUOTE_INFO) is out of scope; the WebAuthntpmformat mandates TPM 2.0. - COSE / CBOR / WebAuthn-format-specific concerns —
@suaveplan/cborhandles CBOR,@suaveplan/webauthnhandles theattStmtmap structure. This package never sees CBOR bytes.
Source mapping
This package is genesis-original; no ../suaveplan/enterprise/tpm-struct.md predecessor exists. The source-of-record is the TCG TPM 2.0 specification (Library Part 2 — Structures, revision 1.59, plus Library Part 1 — Architecture for the name digest) and W3C WebAuthn Level 3 for the consumer contract.
| Requirement | Source | Notes |
|---|---|---|
parseTpmsAttest magic check (0xff544347 / "TCG\xff") |
TCG Part 2 §10.12 TPMS_ATTEST.magic + §6.2 TPM_GENERATED_VALUE |
Big-endian uint32; mismatch ⇒ MAGIC_INVALID |
parseTpmsAttest type field |
TCG Part 2 §10.12 type: TPMI_ST_ATTEST + §10.5 TPM_ST constants |
Eight-member subset of TPM_ST_ATTEST_* |
parseTpmsAttest qualifiedSigner / extraData / clockInfo / firmwareVersion / attested |
TCG Part 2 §10.12 field layout | qualifiedSigner = TPM2B_NAME; extraData = TPM2B_DATA; clockInfo per §10.10 (17 bytes); firmwareVersion = uint64 BE |
attested union dispatch |
TCG Part 2 §10.11 TPMU_ATTEST + §10.5 TPMS_CERTIFY_INFO |
TPM_ST_ATTEST_CERTIFY ⇒ TPMS_CERTIFY_INFO (name + qualifiedName) |
parseTpmtPublic type |
TCG Part 2 §12.2 TPMT_PUBLIC.type: TPMI_ALG_PUBLIC |
RSA / ECC / KEYEDHASH / SYMCIPHER |
parseTpmtPublic nameAlg |
TCG Part 2 §12.2 nameAlg: TPMI_ALG_HASH + §11.1 |
SHA-1/256/384/512; NULL rejected for name-digest call site |
parseTpmtPublic objectAttributes |
TCG Part 2 §8.3 TPMA_OBJECT 32-bit bitfield |
Named-flag accessor + raw uint32; reserved bits non-zero ⇒ OBJECT_ATTRIBUTES_RESERVED_NONZERO |
parseTpmtPublic authPolicy |
TCG Part 2 §12.2 authPolicy: TPM2B_DIGEST |
Length-prefixed |
| RSA parameters | TCG Part 2 §12.2.3.5 TPMS_RSA_PARMS |
symmetric / scheme / keyBits / exponent (uint32, 0 ⇒ default 65537 per spec) |
| ECC parameters | TCG Part 2 §12.2.3.6 TPMS_ECC_PARMS |
symmetric / scheme / curveID (TPMI_ECC_CURVE) / kdf; curve lookup via §11.2.5 |
| RSA unique field | TCG Part 2 §12.2 unique: TPM2B_PUBLIC_KEY_RSA |
Modulus bytes |
| ECC unique field | TCG Part 2 §12.2 unique: TPMS_ECC_POINT |
x + y each TPM2B_ECC_PARAMETER |
computeTpmtPublicName |
TCG Part 1 §16 Names | `nameAlg (2 BE) |
TPM2B reader |
TCG Part 2 §10.1 TPM2B_* size-prefixed buffer |
UINT16 BE length + exact bytes |
TPM_ALG_ID registry |
TCG Part 2 §11.1 ALG_TABLE | 31-entry registry; unknown ⇒ ALG_UNSUPPORTED |
WebAuthn consumer contract (extraData, pubArea match) |
W3C WebAuthn L3 §8.3 steps 4–10 | extraData === attToBeSigned; pubArea name === credentialPublicKey name |
| EK credential context | TCG Credential Profile EK 2.0 v2.3 §3 | Informational — drives the attested union shape decisions |
Requirements with no source line
| Requirement | Justification |
|---|---|
defineCodes("TPM_STRUCT", [...]) 10-code registry |
Genesis convention (CLAUDE.md Quality + @suaveplan/error); each code maps to a distinct §10 / §11 / §12 / §16 failure class so callers can branch on root cause. |
| Zod boundary schemas at every public type | Genesis convention (CLAUDE.md §11). A buggy reader cannot leak a partially-populated object past the boundary. |
| OTel surface (parse + name histograms; parse-total + parse-errors + alg counters) | Genesis convention — every hot-path package emits histograms + counters. alg attribute bounded by §11.1 registry; code bounded by the 10-entry code registry. |
300-line file budget + 100/100/100/100 coverage + sibling .md per impl file |
CLAUDE.md Quality + Documentation sections. |
| Property tests (round-trip + single-bit-flip) | CLAUDE.md "no mocks" rule; round-trip is the only test that proves parser + serialiser agree under arbitrary valid inputs. |
| Mitata benchmarks | CLAUDE.md global benchmark requirement; baseline numbers feed README perf table. |
Hash-injection via HashLike |
Cross-cutting design — keeps the package universal-tier and free of crypto-engine selection; consumer wires @suaveplan/crypto-hash at the call site. |
Verification approach
Every requirement carries a **Verified by:** line in specs/tpm-struct/spec.md naming concrete it(...) titles inside the test files below.
packages/security/tpm-struct/src/buffer/buffer.test.ts— reader cursor advancement;readTpm2blength/payload round-trip;assertEndrejects trailing bytes; out-of-bounds reads raiseTRUNCATED.packages/security/tpm-struct/src/alg/alg.test.ts— every §11.1 entry round-trips throughalgIdToName; unknown ID raisesALG_UNSUPPORTED; every §11.2.5 curve round-trips througheccCurveToName.packages/security/tpm-struct/src/public/public.test.ts—parseTpmtPublicfor RSA-2048, ECC P-256, ECC P-384, KEYEDHASH, SYMCIPHER;objectAttributesflag round-trip; reserved bits non-zero raisesOBJECT_ATTRIBUTES_RESERVED_NONZERO.packages/security/tpm-struct/src/public/property.test.ts— fast-check round-trip: arbitrary validTpmtPublic→serializeTpmtPublic→parseTpmtPublicbyte-identical.packages/security/tpm-struct/src/public/serialize.test.ts—serializeTpmtPublicproduces byte-identical output for parsed real-world fixtures.packages/security/tpm-struct/src/attest/attest.test.ts— magic mismatch raisesMAGIC_INVALID; everyTPM_ST_ATTEST_*variant decodes the matchingattestedunion member; unknowntyperaisesATTEST_TYPE_UNSUPPORTED.packages/security/tpm-struct/src/attest/property.test.ts— fast-check round-trip: arbitrary validTpmsAttest→serializeTpmsAttest→parseTpmsAttestbyte-identical; single-bit-flip on any field either decodes to a different value or raises a typedTpmStructError.packages/security/tpm-struct/src/attest/serialize.test.ts—serializeTpmsAttestbyte-identical for parsed real-world fixtures.packages/security/tpm-struct/src/name/name.test.ts—computeTpmtPublicNamematches the TCG Part 1 §16 expected output for SHA-1/256/384/512 nameAlg variants;nameAlg === TPM_ALG_NULLraisesNAME_ALG_NULL.packages/security/tpm-struct/src/extra-data/extra-data.test.ts—extraDataMatchestrue on equal buffers, false on mismatched bytes, false on length mismatch; delegates to the injectedConstantTimeEqual.packages/security/tpm-struct/src/schemas/schemas.test.ts— every Zod schema accepts a known-good fixture and rejects a malformed fixture withMALFORMED+ Zod issues oncause.packages/security/tpm-struct/src/errors/error-codes.test.ts— every code in theTPM_STRUCT_CODESregistry is thrown by ≥1 production path;expectTypeOf<TpmStructErrorCode>().toEqualTypeOf<…>()pins the literal union.packages/security/tpm-struct/src/observability/metrics.test.ts— histograms + counters emit under a real in-memoryMeterProvider;algandcodeattributes match the bounded enums.packages/security/tpm-struct/src/diagnostic/diagnostic.test.ts—toDiagnosticemits symbolic algorithm names; never invokes parser hot paths (no metric emission during diagnostic).packages/security/tpm-struct/src/__integration__/webauthn-vector.test.ts— known-good WebAuthntpmattestation fixture:parseTpmsAttest(attestationData)succeeds;parseTpmtPublic(pubArea)succeeds;computeTpmtPublicName(parsedPub, sha256)equals the expected fixture digest byte-for-byte.packages/security/tpm-struct/src/index.test.ts— every documented subpath export resolves.
Migration impact
None — additive only; this is the first delta of a new package. No existing genesis code imports from @suaveplan/tpm-struct. @suaveplan/webauthn is the gating consumer and lands in a follow-up change once this package archives; that change SHALL add the workspace:^ dep edge and wire the tpm-attestation verifier.
Impact
Affected dependents
bunx turbo ls --filter=...@suaveplan/tpm-struct returns no current dependents (the package does not yet exist on genesis). The pending consumer is:
@suaveplan/webauthn— [ ] tests run / [ ] docs reviewed / [x] no action needed in THIS change (reason: webauthn is itself pending; thetpm-attestation verifier wiring lands in the webauthn migration change once this package archives, perSTATUS.mdandopenspec/AGENTS.md§0b).
No foundation-package fan-out applies (this is a new universal-tier leaf package; foundation deps @suaveplan/error and @suaveplan/types are unchanged).