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

365 lines
25 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# 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"`
<!-- markdownlint-enable MD024 -->