Files
2026-05-27 23:29:53 +01:00

25 KiB

Capability Spec: tpm-struct

Package: @suaveplan/tpm-struct Status: CANONICAL — folded from enterprise-tpm-struct change on 2026-05-11.

Every requirement below carries a **Verified by:** line. (See openspec/AGENTS.md Section 13.)

Requirements

Requirement: parseTpmsAttest validates TPM_GENERATED_VALUE magic prefix

The package SHALL export parseTpmsAttest(bytes: Uint8Array): TpmsAttest and SHALL reject any input whose first four bytes do not equal 0xff 0x54 0x43 0x47 (the big-endian encoding of TPM_GENERATED_VALUE = 0xff544347, ASCII "\xffTCG") per TCG TPM 2.0 Library Part 2 §10.12 + §6.2. Magic-prefix mismatch SHALL raise TpmStructError with code TPM_STRUCT.MAGIC_INVALID before any further bytes are consumed.

Scenario: well-formed magic prefix is accepted

  • WHEN parseTpmsAttest(bytes) is called with a buffer whose first four bytes equal 0xff 0x54 0x43 0x47 and whose remainder is a valid TPMS_ATTEST
  • THEN the call returns a TpmsAttest value without throwing

Scenario: incorrect magic byte raises MAGIC_INVALID

  • WHEN parseTpmsAttest(bytes) is called with a buffer whose first four bytes equal 0xff 0x54 0x43 0x46 (final byte off-by-one)
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.MAGIC_INVALID"

Scenario: shorter-than-magic input raises TRUNCATED

  • WHEN parseTpmsAttest(bytes) is called with a 3-byte buffer
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.TRUNCATED"

Verified by: packages/crypto/tpm-struct/src/attest/attest.test.ts::"accepts well-formed magic prefix" AND packages/crypto/tpm-struct/src/attest/attest.test.ts::"rejects incorrect magic with MAGIC_INVALID" AND packages/crypto/tpm-struct/src/attest/attest.test.ts::"rejects 3-byte buffer with TRUNCATED"


Requirement: parseTpmsAttest decodes TPMI_ST_ATTEST type

The package SHALL decode the type field of TPMS_ATTEST as a UINT16 BE drawn from the TPM_ST_ATTEST_* subset of TCG Part 2 §10.5 (TPM_ST_ATTEST_NV = 0x8014, TPM_ST_ATTEST_COMMAND_AUDIT = 0x8015, TPM_ST_ATTEST_SESSION_AUDIT = 0x8016, TPM_ST_ATTEST_CERTIFY = 0x8017, TPM_ST_ATTEST_QUOTE = 0x8018, TPM_ST_ATTEST_TIME = 0x8019, TPM_ST_ATTEST_CREATION = 0x801a, TPM_ST_ATTEST_NV_DIGEST = 0x801c). The decoded value SHALL be exposed as the discriminator on the returned TpmsAttest object. Unknown type values SHALL raise TPM_STRUCT.ATTEST_TYPE_UNSUPPORTED.

Scenario: TPM_ST_ATTEST_CERTIFY discriminates to certify branch

  • WHEN parseTpmsAttest(bytes) is called with a buffer carrying type === 0x8017
  • THEN the returned value has type === "TPM_ST_ATTEST_CERTIFY" and an attested: TpmsCertifyInfo

Scenario: TPM_ST_ATTEST_QUOTE discriminates to quote branch

  • WHEN parseTpmsAttest(bytes) is called with a buffer carrying type === 0x8018
  • THEN the returned value has type === "TPM_ST_ATTEST_QUOTE" and an attested: TpmsQuoteInfo

Scenario: unknown type raises ATTEST_TYPE_UNSUPPORTED

  • WHEN parseTpmsAttest(bytes) is called with a buffer carrying type === 0x8099
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.ATTEST_TYPE_UNSUPPORTED"

Verified by: packages/crypto/tpm-struct/src/attest/attest.test.ts::"decodes TPM_ST_ATTEST_CERTIFY" AND packages/crypto/tpm-struct/src/attest/attest.test.ts::"decodes TPM_ST_ATTEST_QUOTE" AND packages/crypto/tpm-struct/src/attest/attest.test.ts::"rejects unknown attest type with ATTEST_TYPE_UNSUPPORTED"


Requirement: parseTpmsAttest decodes qualifiedSigner, extraData, clockInfo, firmwareVersion, attested

The package SHALL decode the trailing fields of TPMS_ATTEST per TCG Part 2 §10.12 in field order: qualifiedSigner: TPM2B_NAME (UINT16 BE length + name bytes), extraData: TPM2B_DATA (UINT16 BE length + data bytes), clockInfo: TPMS_CLOCK_INFO (uint64 BE clock, uint32 BE resetCount, uint32 BE restartCount, uint8 safe — 17 bytes total per §10.10), firmwareVersion: UINT64 BE, and the attested: TPMU_ATTEST union member selected by type per §10.11. For TPM_ST_ATTEST_CERTIFY the attested SHALL be TPMS_CERTIFY_INFO containing name: TPM2B_NAME followed by qualifiedName: TPM2B_NAME per §10.5.

Scenario: certify attest round-trips through serialize

  • WHEN parseTpmsAttest(bytes) is called with a valid TPM_ST_ATTEST_CERTIFY buffer, then serializeTpmsAttest is invoked on the result
  • THEN the re-serialised bytes equal the original input byte-for-byte

Scenario: clockInfo decodes 17 bytes in correct field order

  • WHEN parseTpmsAttest(bytes) is called with clockInfo segment 0000000000000064 00000005 00000003 01 (clock=100, reset=5, restart=3, safe=true)
  • THEN the returned clockInfo equals { clock: 100n, resetCount: 5, restartCount: 3, safe: true }

Verified by: packages/crypto/tpm-struct/src/attest/attest.test.ts::"certify attest round-trips through serializeTpmsAttest" AND packages/crypto/tpm-struct/src/attest/attest.test.ts::"clockInfo decodes 17-byte field correctly" AND packages/crypto/tpm-struct/src/attest/serialize.test.ts::"byte-identical re-serialisation for real-world fixture"


Requirement: parseTpmtPublic decodes type (TPMI_ALG_PUBLIC)

The package SHALL export parseTpmtPublic(bytes: Uint8Array): TpmtPublic and SHALL decode the first UINT16 BE field as type: TPMI_ALG_PUBLIC per TCG Part 2 §12.2, recognising TPM_ALG_RSA = 0x0001, TPM_ALG_ECC = 0x0023, TPM_ALG_KEYEDHASH = 0x0008, and TPM_ALG_SYMCIPHER = 0x0025. The decoded value SHALL drive the parameter / unique union dispatch. Any other type SHALL raise TPM_STRUCT.ALG_UNSUPPORTED.

Scenario: TPM_ALG_RSA selects RSA branch

  • WHEN parseTpmtPublic(bytes) is called with type === 0x0001
  • THEN the returned value has type === "TPM_ALG_RSA" and parameters: TpmsRsaParms

Scenario: TPM_ALG_ECC selects ECC branch

  • WHEN parseTpmtPublic(bytes) is called with type === 0x0023
  • THEN the returned value has type === "TPM_ALG_ECC" and parameters: TpmsEccParms

Scenario: unsupported algorithm raises ALG_UNSUPPORTED

  • WHEN parseTpmtPublic(bytes) is called with type === 0x0099
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.ALG_UNSUPPORTED"

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"RSA type selects RSA branch" AND packages/crypto/tpm-struct/src/public/public.test.ts::"ECC type selects ECC branch" AND packages/crypto/tpm-struct/src/public/public.test.ts::"unknown type raises ALG_UNSUPPORTED"


Requirement: parseTpmtPublic decodes nameAlg (TPMI_ALG_HASH)

The package SHALL decode the second UINT16 BE field of TPMT_PUBLIC as nameAlg: TPMI_ALG_HASH per TCG Part 2 §12.2, recognising at minimum TPM_ALG_SHA1 = 0x0004, TPM_ALG_SHA256 = 0x000b, TPM_ALG_SHA384 = 0x000c, TPM_ALG_SHA512 = 0x000d, and the sentinel TPM_ALG_NULL = 0x0010. The decoded value SHALL be exposed on the returned TpmtPublic so computeTpmtPublicName can route to the correct hash.

Scenario: SHA-256 nameAlg decodes

  • WHEN parseTpmtPublic(bytes) is called with nameAlg === 0x000b
  • THEN the returned value has nameAlg === "TPM_ALG_SHA256"

Scenario: TPM_ALG_NULL nameAlg is accepted by the parser

  • WHEN parseTpmtPublic(bytes) is called with nameAlg === 0x0010
  • THEN the returned value has nameAlg === "TPM_ALG_NULL" (the name-digest call site rejects it; the parser does not)

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"SHA-256 nameAlg decodes" AND packages/crypto/tpm-struct/src/public/public.test.ts::"TPM_ALG_NULL nameAlg accepted by parser"


Requirement: parseTpmtPublic decodes objectAttributes (TPMA_OBJECT bitfield)

The package SHALL decode objectAttributes as a UINT32 BE bitfield per TCG Part 2 §8.3 and SHALL expose it both as the raw uint32 and as a typed ObjectAttributes record with named flags (fixedTPM, stClear, fixedParent, sensitiveDataOrigin, userWithAuth, adminWithPolicy, noDA, encryptedDuplication, restricted, decrypt, sign). Any reserved bit set non-zero SHALL raise TPM_STRUCT.OBJECT_ATTRIBUTES_RESERVED_NONZERO.

Scenario: well-formed attributes round-trip

  • WHEN parseTpmtPublic(bytes) is called with objectAttributes === 0x00040472 (fixedTPM + fixedParent + sensitiveDataOrigin + userWithAuth + sign)
  • THEN the returned objectAttributes.flags equals { fixedTPM: true, fixedParent: true, sensitiveDataOrigin: true, userWithAuth: true, sign: true, ... } with all other flags false

Scenario: reserved bit raises OBJECT_ATTRIBUTES_RESERVED_NONZERO

  • WHEN parseTpmtPublic(bytes) is called with objectAttributes === 0x00000001 (reserved bit 0)
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.OBJECT_ATTRIBUTES_RESERVED_NONZERO"

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"objectAttributes flag round-trip" AND packages/crypto/tpm-struct/src/public/public.test.ts::"reserved bit raises OBJECT_ATTRIBUTES_RESERVED_NONZERO"


Requirement: parseTpmtPublic decodes authPolicy (TPM2B_DIGEST)

The package SHALL decode authPolicy as a TPM2B_DIGEST per TCG Part 2 §12.2 — UINT16 BE length followed by exactly that many bytes of policy digest. Length-zero authPolicy SHALL be accepted and SHALL produce an empty Uint8Array.

Scenario: 32-byte authPolicy decodes

  • WHEN parseTpmtPublic(bytes) is called with authPolicy segment 0020 followed by 32 bytes
  • THEN the returned authPolicy is a 32-byte Uint8Array equal to those bytes

Scenario: empty authPolicy decodes

  • WHEN parseTpmtPublic(bytes) is called with authPolicy segment 0000
  • THEN the returned authPolicy is a zero-length Uint8Array

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"32-byte authPolicy decodes" AND packages/crypto/tpm-struct/src/public/public.test.ts::"empty authPolicy decodes"


Requirement: RSA parameter decoding (TPMS_RSA_PARMS)

When parseTpmtPublic encounters type === TPM_ALG_RSA, the package SHALL decode parameters as TPMS_RSA_PARMS per TCG Part 2 §12.2.3.5: symmetric: TPMT_SYM_DEF_OBJECT, scheme: TPMT_RSA_SCHEME, keyBits: UINT16 BE (TPMI_RSA_KEY_BITS), exponent: UINT32 BE. Per §12.2.3.5, an exponent value of 0 SHALL be normalised to 65537 (Fermat F4) on output while preserving the raw field on a rawExponent slot for round-trip fidelity. The unique field SHALL be decoded as TPM2B_PUBLIC_KEY_RSA (UINT16 BE length + modulus bytes).

Scenario: RSA-2048 parameters decode

  • WHEN parseTpmtPublic(bytes) is called with a real-world RSA-2048 TPMT_PUBLIC fixture
  • THEN the returned parameters.keyBits === 2048, parameters.exponent === 65537, and unique.modulus.length === 256

Scenario: exponent zero normalises to 65537

  • WHEN parseTpmtPublic(bytes) is called with an exponent field of 0x00000000
  • THEN the returned parameters.exponent === 65537 and parameters.rawExponent === 0

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"RSA-2048 parameters decode" AND packages/crypto/tpm-struct/src/public/public.test.ts::"exponent zero normalises to 65537"


Requirement: ECC parameter decoding (TPMS_ECC_PARMS) with curveID lookup

When parseTpmtPublic encounters type === TPM_ALG_ECC, the package SHALL decode parameters as TPMS_ECC_PARMS per TCG Part 2 §12.2.3.6: symmetric: TPMT_SYM_DEF_OBJECT, scheme: TPMT_ECC_SCHEME, curveID: TPMI_ECC_CURVE, kdf: TPMT_KDF_SCHEME. The curveID SHALL be looked up in the TPM_ECC_CURVE registry (§11.2.5) — NIST_P256 = 0x0003, NIST_P384 = 0x0004, NIST_P521 = 0x0005, BN_P256 = 0x0010, BN_P638 = 0x0011, SM2_P256 = 0x0020. Unknown curve IDs SHALL raise TPM_STRUCT.CURVE_UNSUPPORTED.

Scenario: ECC P-256 parameters decode

  • WHEN parseTpmtPublic(bytes) is called with a real-world ECC P-256 TPMT_PUBLIC fixture
  • THEN the returned parameters.curveID === "TPM_ECC_NIST_P256" and unique.x.length === 32 and unique.y.length === 32

Scenario: unknown ECC curve raises CURVE_UNSUPPORTED

  • WHEN parseTpmtPublic(bytes) is called with curveID === 0x0099
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.CURVE_UNSUPPORTED"

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"ECC P-256 parameters decode" AND packages/crypto/tpm-struct/src/public/public.test.ts::"unknown ECC curve raises CURVE_UNSUPPORTED"


Requirement: unique field decoding (TPM2B_PUBLIC_KEY_RSA or TPMS_ECC_POINT)

The package SHALL decode the unique field of TPMT_PUBLIC per TCG Part 2 §12.2 according to the type discriminator: for TPM_ALG_RSA the field is TPM2B_PUBLIC_KEY_RSA (UINT16 BE length + modulus bytes); for TPM_ALG_ECC the field is TPMS_ECC_POINT carrying x: TPM2B_ECC_PARAMETER followed by y: TPM2B_ECC_PARAMETER (each a UINT16 BE length + parameter bytes).

Scenario: RSA unique field is the modulus

  • WHEN parseTpmtPublic(bytes) is called with an RSA-2048 fixture
  • THEN the returned unique.modulus equals the 256 modulus bytes from the fixture

Scenario: ECC unique field is the point

  • WHEN parseTpmtPublic(bytes) is called with an ECC P-256 fixture
  • THEN the returned unique equals { x: <32 bytes>, y: <32 bytes> } matching the fixture's affine coordinates

Verified by: packages/crypto/tpm-struct/src/public/public.test.ts::"RSA unique field is the modulus" AND packages/crypto/tpm-struct/src/public/public.test.ts::"ECC unique field is the point"


Requirement: computeTpmtPublicName produces nameAlg || H_nameAlg(TPMT_PUBLIC)

The package SHALL export computeTpmtPublicName(pub: TpmtPublic, hash: HashLike): Uint8Array and SHALL produce the canonical name per TCG TPM 2.0 Library Part 1 §16 — the two-byte big-endian nameAlg identifier followed by the digest of the canonical TPMT_PUBLIC byte serialisation computed via the injected hash.digest function. The injected hash MUST match pub.nameAlg; the package itself imports no hashing code. pub.nameAlg === TPM_ALG_NULL SHALL raise TPM_STRUCT.NAME_ALG_NULL — names cannot be computed without a digest.

Scenario: SHA-256 name has expected length and prefix

  • WHEN computeTpmtPublicName(pub, sha256) is called for a TpmtPublic with nameAlg === "TPM_ALG_SHA256"
  • THEN the returned Uint8Array has length 34 and starts with the bytes 0x00 0x0b

Scenario: name matches WebAuthn fixture digest

  • WHEN computeTpmtPublicName(parsedPubArea, sha256) is called against the WebAuthn tpm-attestation fixture
  • THEN the returned bytes equal the fixture's expectedName byte-for-byte

Scenario: TPM_ALG_NULL nameAlg raises NAME_ALG_NULL

  • WHEN computeTpmtPublicName(pub, sha256) is called with pub.nameAlg === "TPM_ALG_NULL"
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.NAME_ALG_NULL"

Verified by: packages/crypto/tpm-struct/src/name/name.test.ts::"SHA-256 name has expected length and prefix" AND packages/crypto/tpm-struct/src/name/name.test.ts::"name matches WebAuthn fixture digest" AND packages/crypto/tpm-struct/src/name/name.test.ts::"TPM_ALG_NULL nameAlg raises NAME_ALG_NULL"


Requirement: TPM2B length-prefix reader

The package SHALL provide TpmBufferReader.readTpm2b() returning a Uint8Array of exactly the bytes following the UINT16 BE size prefix per TCG Part 2 §10.1 TPM2B_* convention. The reader SHALL advance the cursor by 2 + size bytes. The reader SHALL accept zero-length TPM2B values, returning an empty Uint8Array. If the declared size exceeds the remaining buffer length, the reader SHALL raise TPM_STRUCT.TRUNCATED.

Scenario: TPM2B with 4-byte payload

  • WHEN readTpm2b() is called over the bytes 0004 deadbeef
  • THEN the call returns a 4-byte Uint8Array equal to [0xde, 0xad, 0xbe, 0xef] and the reader cursor advances by 6

Scenario: zero-length TPM2B returns empty buffer

  • WHEN readTpm2b() is called over the bytes 0000
  • THEN the call returns a zero-length Uint8Array and the reader cursor advances by 2

Scenario: declared size exceeds remaining buffer raises TRUNCATED

  • WHEN readTpm2b() is called over the bytes 0008 deadbeef (declared 8, available 4)
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.TRUNCATED"

Verified by: packages/crypto/tpm-struct/src/buffer/buffer.test.ts::"readTpm2b reads 4-byte payload and advances cursor" AND packages/crypto/tpm-struct/src/buffer/buffer.test.ts::"readTpm2b accepts zero-length" AND packages/crypto/tpm-struct/src/buffer/buffer.test.ts::"readTpm2b raises TRUNCATED on short buffer"


Requirement: bounds enforcement on every reader operation

Every TpmBufferReader method SHALL raise TpmStructError with code TPM_STRUCT.TRUNCATED when a read would consume bytes past the end of the underlying buffer. The error SHALL carry cause.cursor (the cursor at the time of the failed read) and cause.requested (the number of bytes the read attempted to consume). The reader SHALL NOT silently return short reads or pad with zero bytes.

Scenario: readUint32Be past end raises TRUNCATED

  • WHEN readUint32Be() is called with only 3 bytes remaining
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.TRUNCATED" and cause.requested === 4

Scenario: readBytes past end raises TRUNCATED

  • WHEN readBytes(100) is called with 50 bytes remaining
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.TRUNCATED" and cause.requested === 100

Scenario: assertEnd raises when trailing bytes remain

  • WHEN assertEnd() is called with 1 byte remaining in the buffer
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.TRUNCATED"

Verified by: packages/crypto/tpm-struct/src/buffer/buffer.test.ts::"readUint32Be past end raises TRUNCATED" AND packages/crypto/tpm-struct/src/buffer/buffer.test.ts::"readBytes past end raises TRUNCATED" AND packages/crypto/tpm-struct/src/buffer/buffer.test.ts::"assertEnd raises with trailing bytes"


Requirement: TPM2_ALG_ID registry and symbolic lookup

The package SHALL expose a frozen TPM_ALG const-object whose properties are the symbolic names of every TPM 2.0 algorithm identifier defined in TCG Part 2 §11.1 ALG_TABLE (at minimum: NULL, RSA, SHA1, HMAC, AES, KEYEDHASH, SHA256, SHA384, SHA512, XOR, RSASSA, RSAES, RSAPSS, OAEP, ECDSA, ECDH, ECDAA, SM2, ECSCHNORR, ECMQV, KDF1_SP800_56A, KDF2, KDF1_SP800_108, ECC, SYMCIPHER, CTR, OFB, CBC, CFB, ECB). The package SHALL export algIdToName(id: number): TpmAlgId returning the symbolic name. Unknown numeric IDs SHALL raise TPM_STRUCT.ALG_UNSUPPORTED.

Scenario: known numeric id decodes to symbolic name

  • WHEN algIdToName(0x000b) is called
  • THEN the call returns "TPM_ALG_SHA256"

Scenario: unknown numeric id raises ALG_UNSUPPORTED

  • WHEN algIdToName(0x0099) is called
  • THEN the call throws TpmStructError with code === "TPM_STRUCT.ALG_UNSUPPORTED"

Verified by: packages/crypto/tpm-struct/src/alg/alg.test.ts::"known numeric id decodes to symbolic name" AND packages/crypto/tpm-struct/src/alg/alg.test.ts::"unknown numeric id raises ALG_UNSUPPORTED"


Requirement: TPM_STRUCT error registry coverage

The package SHALL ship a defineCodes("TPM_STRUCT", [...]) registry containing exactly the following 10 codes: MAGIC_INVALID, TRUNCATED, ALG_UNSUPPORTED, CURVE_UNSUPPORTED, TYPE_MISMATCH, MALFORMED, NAME_ALG_NULL, ATTEST_TYPE_UNSUPPORTED, PARAMETERS_INVALID, OBJECT_ATTRIBUTES_RESERVED_NONZERO. Every code SHALL be thrown by at least one production code path. The package SHALL export TpmStructError extends SuaveplanError<TpmStructErrorCode> and the TpmStructErrorCode literal union SHALL exactly equal the registry keys (pinned with expectTypeOf).

Scenario: every code is thrown by a production path

  • WHEN the registry-coverage test iterates every TpmStructErrorCode and runs the matching trigger fixture
  • THEN each trigger raises a TpmStructError whose code matches the iterated code

Scenario: literal union pins to registry keys

  • WHEN expectTypeOf<TpmStructErrorCode>().toEqualTypeOf<...>() runs at type-check time
  • THEN the type-check passes for the canonical 10-member union and fails for any other shape

Verified by: packages/crypto/tpm-struct/src/errors/error-codes.test.ts::"every code is thrown by a production path" AND packages/crypto/tpm-struct/src/errors/error-codes.test.ts::"literal union pins to registry keys"


Requirement: Zod boundary schemas validate every public return type

The package SHALL ship Zod schemas for every public return type (TpmsAttestSchema, TpmtPublicSchema, TpmsRsaParmsSchema, TpmsEccParmsSchema, TpmsKeyedHashParmsSchema, TpmsSymCipherParmsSchema, TpmsCertifyInfoSchema, ClockInfoSchema, Tpm2bSchema, TpmAlgIdSchema, TpmEccCurveSchema, ObjectAttributesSchema). Every public parser SHALL validate its constructed result through the matching schema before returning. Schema-validation failure SHALL raise TPM_STRUCT.MALFORMED carrying the Zod issue list on cause.issues.

Scenario: schema accepts known-good fixture

  • WHEN TpmtPublicSchema.parse(parsed) is called with the output of parseTpmtPublic over a real-world fixture
  • THEN the call returns without throwing

Scenario: schema rejects malformed object with MALFORMED

  • WHEN a hand-constructed TpmtPublic-shaped value is missing the nameAlg field and TpmtPublicSchema.parse(value) is invoked behind the parser boundary
  • THEN the surfaced error is a TpmStructError with code === "TPM_STRUCT.MALFORMED" and cause.issues is the Zod issue array

Verified by: packages/crypto/tpm-struct/src/schemas/schemas.test.ts::"schema accepts known-good fixture" AND packages/crypto/tpm-struct/src/schemas/schemas.test.ts::"schema rejects malformed object with MALFORMED"


Requirement: OpenTelemetry histograms and counters

The package SHALL emit on every public parser invocation: tpm_struct.parse_tpms_attest_duration_ms histogram with outcome attribute (ok | error); tpm_struct.parse_tpmt_public_duration_ms histogram with type (rsa | ecc | keyedhash | symcipher) and outcome attributes; tpm_struct.compute_name_duration_ms histogram with name_alg attribute. Counters: tpm_struct.parse_total{kind,outcome} (kind ∈ attest | public | name, outcome ∈ ok | error); tpm_struct.parse_errors_total{code} (code drawn from the TPM_STRUCT registry); tpm_struct.alg_total{alg} (alg drawn from TPM_ALG). Every attribute value SHALL be a bounded enum drawn from a frozen registry; no free-form strings SHALL flow into telemetry attributes.

Scenario: parse-attest histogram records on success

  • WHEN parseTpmsAttest(bytes) succeeds under an in-memory MeterProvider
  • THEN tpm_struct.parse_tpms_attest_duration_ms{outcome="ok"} records exactly one observation and tpm_struct.parse_total{kind="attest",outcome="ok"} increments by 1

Scenario: parse-public error counter increments on failure

  • WHEN parseTpmtPublic(bytes) raises ALG_UNSUPPORTED under an in-memory MeterProvider
  • THEN tpm_struct.parse_errors_total{code="TPM_STRUCT.ALG_UNSUPPORTED"} increments by 1 and tpm_struct.parse_total{kind="public",outcome="error"} increments by 1

Verified by: packages/crypto/tpm-struct/src/observability/metrics.test.ts::"parse-attest histogram records on success" AND packages/crypto/tpm-struct/src/observability/metrics.test.ts::"parse-public error counter increments on failure"


Requirement: WebAuthn tpm-attestation integration vector

The package SHALL ship a real-world WebAuthn tpm-format attestation fixture (attestationData byte string + pubArea byte string + expected parsed structures + expected name digest under SHA-256). The integration test SHALL exercise parseTpmsAttest(attestationData), parseTpmtPublic(pubArea), and computeTpmtPublicName(parsedPub, sha256) end-to-end and SHALL assert that the resulting name equals the fixture's expectedName byte-for-byte. The injected HashLike SHALL be a real SHA-256 implementation (no mocks).

Scenario: attestationData parses end-to-end

  • WHEN parseTpmsAttest(fixture.attestationData) is called
  • THEN the returned value has type === "TPM_ST_ATTEST_CERTIFY" and attested matches fixture.expectedAttested

Scenario: pubArea parses and produces expected name

  • WHEN parseTpmtPublic(fixture.pubArea) is called and computeTpmtPublicName(parsedPub, sha256) is invoked on the result
  • THEN the returned name Uint8Array equals fixture.expectedName byte-for-byte

Verified by: packages/crypto/tpm-struct/src/__integration__/webauthn-vector.test.ts::"attestationData parses end-to-end" AND packages/crypto/tpm-struct/src/__integration__/webauthn-vector.test.ts::"pubArea parses and produces expected name"