Initial Commit
This commit is contained in:
142
openspec/archive/2026-05-11-enterprise-tpm-struct/proposal.md
Normal file
142
openspec/archive/2026-05-11-enterprise-tpm-struct/proposal.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# `@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()`) and `TpmBufferWriter` (matching write surface plus `writeTpm2b(bytes)`, `toUint8Array()`) per TCG Part 2 §10.1 octet-stream conventions; every read past `bytes.length` raises `TPM_STRUCT.TRUNCATED` carrying cursor + requested-length in `cause`.
|
||||
- ADDED: `src/buffer/buffer.types.ts` — `TpmReaderOptions`, `TpmWriterOptions`, `Tpm2bReadResult`.
|
||||
- ADDED: `src/alg/alg.ts` — `TPM_ALG` frozen 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_CURVE` frozen 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 raise `TPM_STRUCT.ALG_UNSUPPORTED`. `eccCurveToName` does the same for curves; unknown raises `TPM_STRUCT.CURVE_UNSUPPORTED`.
|
||||
- ADDED: `src/alg/alg.types.ts` — `TpmAlgId`, `TpmEccCurve` literal unions.
|
||||
- ADDED: `src/public/public.ts` — `parseTpmtPublic(bytes): TpmtPublic` per TCG Part 2 §12.2; reads `type` (TPMI_ALG_PUBLIC), `nameAlg` (TPMI_ALG_HASH), `objectAttributes` (32-bit TPMA_OBJECT per §8.3 — exposes raw `uint32` and named-flag accessor), `authPolicy` (TPM2B_DIGEST), `parameters` (`TPMS_RSA_PARMS` per §12.2.3.5 with `symmetric` `TPMT_SYM_DEF_OBJECT`, `scheme` `TPMT_RSA_SCHEME`, `keyBits`, `exponent` (uint32, `0` ⇒ default 65537)), `parameters` (`TPMS_ECC_PARMS` per §12.2.3.6 with `symmetric`, `scheme` `TPMT_ECC_SCHEME`, `curveID` `TPMI_ECC_CURVE`, `kdf` `TPMT_KDF_SCHEME`), `unique` (`TPM2B_PUBLIC_KEY_RSA` for RSA or `TPMS_ECC_POINT` `{ x, y }` of two `TPM2B_ECC_PARAMETER`s for ECC). KEYEDHASH and SYMCIPHER branches decode their `TPMS_*_PARMS` per §12.2.3.3 / §12.2.3.4 and return the same discriminated-union shape.
|
||||
- ADDED: `src/public/public.types.ts` — `TpmtPublic` discriminator on `type`; `TpmsRsaParms`, `TpmsEccParms`, `TpmsKeyedHashParms`, `TpmsSymCipherParms`, `TpmsEccPoint`, `ObjectAttributes` (typed flag struct + raw `uint32` slot).
|
||||
- ADDED: `src/public/serialize.ts` — `serializeTpmtPublic(pub): Uint8Array` produces byte-identical re-serialisation of any `parseTpmtPublic` output; powers `computeTpmtPublicName`.
|
||||
- ADDED: `src/attest/attest.ts` — `parseTpmsAttest(bytes): TpmsAttest` per TCG Part 2 §10.12; validates 4-byte magic `0xff544347` (`TPM_GENERATED_VALUE`, ASCII "TCG\xff" in big-endian); reads `type` (`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 dispatches `attested` to the matching `TPMU_ATTEST` member (per §10.11). For `TPM_ST_ATTEST_CERTIFY` the `attested` is `TPMS_CERTIFY_INFO` (§10.5) — `name` + `qualifiedName` both `TPM2B_NAME`.
|
||||
- ADDED: `src/attest/attest.types.ts` — `TpmsAttest` discriminator on `type`; `TpmsCertifyInfo`, `TpmsQuoteInfo`, `TpmsSessionAuditInfo`, `TpmsCommandAuditInfo`, `TpmsTimeAttestInfo`, `TpmsCreationInfo`, `TpmsNvCertifyInfo`, `TpmsNvDigestCertifyInfo`, `ClockInfo`.
|
||||
- ADDED: `src/attest/serialize.ts` — `serializeTpmsAttest(att): Uint8Array` produces byte-identical re-serialisation of any `parseTpmsAttest` output.
|
||||
- ADDED: `src/name/name.ts` — `computeTpmtPublicName(pub, hash): Uint8Array` per TCG Part 1 §16. Output = `nameAlg (2 BE bytes) || H_nameAlg(serializeTpmtPublic(pub))`. Hash backend injected via `HashLike` (caller supplies a SHA-1/256/384/512 implementation matching `nameAlg`; the package itself imports no hash code). `nameAlg === TPM_ALG_NULL` is rejected with `TPM_STRUCT.NAME_ALG_NULL`.
|
||||
- ADDED: `src/name/name.types.ts` — `HashLike` interface (`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` — `ConstantTimeEqual` function 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 raises `TPM_STRUCT.MALFORMED` with Zod issues attached as `cause.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` — histograms `tpm_struct.parse_tpms_attest_duration_ms{outcome}`, `tpm_struct.parse_tpmt_public_duration_ms{type,outcome}`, `tpm_struct.compute_name_duration_ms{name_alg}`; counters `tpm_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): string` human-readable dump with `TPM_ALG` symbolic names inlined; debug-only; never invoked from production hot paths.
|
||||
- ADDED: `src/__fixtures__/webauthn-tpm-vector.ts` — known-good real-world WebAuthn `tpm`-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; no `export *` (per `openspec/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`, every `TPMU_ATTEST` member 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`, `SuaveplanError` base.
|
||||
- `@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, **no** `tpm-js` / `node-tss` / `tss.js` / `@noble/*` / `@stablelib/*`. Pure TypeScript over `Uint8Array` + `DataView`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **TPM I/O / TPM commands** — no `TPM2_*` command marshalling, no TSS device driver, no `/dev/tpmrm0` or Windows `Tbsi_*` 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`, no `TPM2_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 WebAuthn `tpm` format mandates TPM 2.0.
|
||||
- **COSE / CBOR / WebAuthn-format-specific concerns** — `@suaveplan/cbor` handles CBOR, `@suaveplan/webauthn` handles the `attStmt` map 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) || H_nameAlg(TPMT_PUBLIC)` |
|
||||
| `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; `readTpm2b` length/payload round-trip; `assertEnd` rejects trailing bytes; out-of-bounds reads raise `TRUNCATED`.
|
||||
- `packages/security/tpm-struct/src/alg/alg.test.ts` — every §11.1 entry round-trips through `algIdToName`; unknown ID raises `ALG_UNSUPPORTED`; every §11.2.5 curve round-trips through `eccCurveToName`.
|
||||
- `packages/security/tpm-struct/src/public/public.test.ts` — `parseTpmtPublic` for RSA-2048, ECC P-256, ECC P-384, KEYEDHASH, SYMCIPHER; `objectAttributes` flag round-trip; reserved bits non-zero raises `OBJECT_ATTRIBUTES_RESERVED_NONZERO`.
|
||||
- `packages/security/tpm-struct/src/public/property.test.ts` — fast-check round-trip: arbitrary valid `TpmtPublic` → `serializeTpmtPublic` → `parseTpmtPublic` byte-identical.
|
||||
- `packages/security/tpm-struct/src/public/serialize.test.ts` — `serializeTpmtPublic` produces byte-identical output for parsed real-world fixtures.
|
||||
- `packages/security/tpm-struct/src/attest/attest.test.ts` — magic mismatch raises `MAGIC_INVALID`; every `TPM_ST_ATTEST_*` variant decodes the matching `attested` union member; unknown `type` raises `ATTEST_TYPE_UNSUPPORTED`.
|
||||
- `packages/security/tpm-struct/src/attest/property.test.ts` — fast-check round-trip: arbitrary valid `TpmsAttest` → `serializeTpmsAttest` → `parseTpmsAttest` byte-identical; single-bit-flip on any field either decodes to a different value or raises a typed `TpmStructError`.
|
||||
- `packages/security/tpm-struct/src/attest/serialize.test.ts` — `serializeTpmsAttest` byte-identical for parsed real-world fixtures.
|
||||
- `packages/security/tpm-struct/src/name/name.test.ts` — `computeTpmtPublicName` matches the TCG Part 1 §16 expected output for SHA-1/256/384/512 nameAlg variants; `nameAlg === TPM_ALG_NULL` raises `NAME_ALG_NULL`.
|
||||
- `packages/security/tpm-struct/src/extra-data/extra-data.test.ts` — `extraDataMatches` true on equal buffers, false on mismatched bytes, false on length mismatch; delegates to the injected `ConstantTimeEqual`.
|
||||
- `packages/security/tpm-struct/src/schemas/schemas.test.ts` — every Zod schema accepts a known-good fixture and rejects a malformed fixture with `MALFORMED` + Zod issues on `cause`.
|
||||
- `packages/security/tpm-struct/src/errors/error-codes.test.ts` — every code in the `TPM_STRUCT_CODES` registry 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-memory `MeterProvider`; `alg` and `code` attributes match the bounded enums.
|
||||
- `packages/security/tpm-struct/src/diagnostic/diagnostic.test.ts` — `toDiagnostic` emits 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 WebAuthn `tpm` attestation 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; the `tpm`-attestation verifier wiring lands in the webauthn migration change once this package archives, per `STATUS.md` and `openspec/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).
|
||||
@@ -0,0 +1,364 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: tpm-struct (enterprise-tpm-struct)
|
||||
|
||||
**Package**: `@suaveplan/tpm-struct`
|
||||
**Status**: PROPOSED — first delta; becomes canonical spec on archive.
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED 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/security/tpm-struct/src/attest/attest.test.ts::"accepts well-formed magic prefix"` AND `packages/security/tpm-struct/src/attest/attest.test.ts::"rejects incorrect magic with MAGIC_INVALID"` AND `packages/security/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/security/tpm-struct/src/attest/attest.test.ts::"decodes TPM_ST_ATTEST_CERTIFY"` AND `packages/security/tpm-struct/src/attest/attest.test.ts::"decodes TPM_ST_ATTEST_QUOTE"` AND `packages/security/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/security/tpm-struct/src/attest/attest.test.ts::"certify attest round-trips through serializeTpmsAttest"` AND `packages/security/tpm-struct/src/attest/attest.test.ts::"clockInfo decodes 17-byte field correctly"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"RSA type selects RSA branch"` AND `packages/security/tpm-struct/src/public/public.test.ts::"ECC type selects ECC branch"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"SHA-256 nameAlg decodes"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"objectAttributes flag round-trip"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"32-byte authPolicy decodes"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"RSA-2048 parameters decode"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"ECC P-256 parameters decode"` AND `packages/security/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/security/tpm-struct/src/public/public.test.ts::"RSA unique field is the modulus"` AND `packages/security/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/security/tpm-struct/src/name/name.test.ts::"SHA-256 name has expected length and prefix"` AND `packages/security/tpm-struct/src/name/name.test.ts::"name matches WebAuthn fixture digest"` AND `packages/security/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/security/tpm-struct/src/buffer/buffer.test.ts::"readTpm2b reads 4-byte payload and advances cursor"` AND `packages/security/tpm-struct/src/buffer/buffer.test.ts::"readTpm2b accepts zero-length"` AND `packages/security/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/security/tpm-struct/src/buffer/buffer.test.ts::"readUint32Be past end raises TRUNCATED"` AND `packages/security/tpm-struct/src/buffer/buffer.test.ts::"readBytes past end raises TRUNCATED"` AND `packages/security/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/security/tpm-struct/src/alg/alg.test.ts::"known numeric id decodes to symbolic name"` AND `packages/security/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/security/tpm-struct/src/errors/error-codes.test.ts::"every code is thrown by a production path"` AND `packages/security/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/security/tpm-struct/src/schemas/schemas.test.ts::"schema accepts known-good fixture"` AND `packages/security/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/security/tpm-struct/src/observability/metrics.test.ts::"parse-attest histogram records on success"` AND `packages/security/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/security/tpm-struct/src/__integration__/webauthn-vector.test.ts::"attestationData parses end-to-end"` AND `packages/security/tpm-struct/src/__integration__/webauthn-vector.test.ts::"pubArea parses and produces expected name"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
163
openspec/archive/2026-05-11-enterprise-tpm-struct/tasks.md
Normal file
163
openspec/archive/2026-05-11-enterprise-tpm-struct/tasks.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Tasks — `@suaveplan/tpm-struct` (TPM 2.0 attestation-structure parser)
|
||||
|
||||
> Every checkbox below MUST reflect work actually done in this change. Do not retroactively tick boxes. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read TCG TPM 2.0 Library Part 2 — Structures r1.59 end-to-end (focus §10.1, §10.5, §10.10, §10.11, §10.12, §11.1, §11.2.5, §8.3, §12.2)
|
||||
- [x] 1.2 Read TCG TPM 2.0 Library Part 1 — Architecture §16 (Names)
|
||||
- [x] 1.3 Read W3C WebAuthn Level 3 §8.3 _TPM Attestation Statement Format_ end-to-end
|
||||
- [x] 1.4 Read TCG _Credential Profile EK 2.0_ v2.3 (informational — AIK / EK context)
|
||||
- [x] 1.5 Read `openspec/AGENTS.md` §0, §0a, §0b, §2b, §6a, §7, §13, §14, §14a
|
||||
- [x] 1.6 Confirm `proposal.md` covers every public-surface item, every subpath, and every dependency
|
||||
- [x] 1.7 Confirm `specs/tpm-struct/spec.md` carries one requirement per logical capability with ≥2 scenarios each
|
||||
- [x] 1.8 Confirm every requirement has a `**Verified by:**` line naming concrete `it(...)` titles in concrete test file paths
|
||||
- [x] 1.9 Source-mapping table cites every TCG / W3C section consumed
|
||||
|
||||
## 2. Package scaffold
|
||||
|
||||
- [x] 2.1 Create `packages/security/tpm-struct/` with `src/{buffer,alg,public,attest,name,extra-data,schemas,errors,observability,diagnostic,__fixtures__,__integration__,__benchmarks__}/`
|
||||
- [x] 2.2 `package.json` — `@suaveplan/tpm-struct@0.0.0`, `type: module`, `sideEffects: false`, deps `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`, `zod catalog:forms` (no bare versions)
|
||||
- [x] 2.3 `package.json` `exports` map declares `.`, `./buffer`, `./alg`, `./public`, `./attest`, `./name`, `./extra-data`, `./schemas`, `./errors`, `./diagnostic` (10 subpaths) — each mapped to `dist/<feature>/<feature>.js` + `.d.ts`
|
||||
- [x] 2.4 `tsconfig.lib.json` extends `@suaveplan/typescript-config/library.json`
|
||||
- [x] 2.5 `vite.config.ts` multi-entry build (one entry per subpath) + `vitest.config.ts` calling `createTestConfig()` from `@suaveplan/testing/config`
|
||||
- [x] 2.6 `README.md` with package name, description, `bun add` install, quick-start example, `## Modules` table linking every co-located `.md`
|
||||
|
||||
## 3. Implementation — buffer primitives
|
||||
|
||||
- [x] 3.1 `src/buffer/buffer.ts` — `TpmBufferReader` (`readUint8`, `readUint16Be`, `readUint32Be`, `readUint64Be: bigint`, `readBytes(n)`, `readTpm2b(): Uint8Array`, `peek(n)`, `remaining()`, `assertEnd()`)
|
||||
- [x] 3.2 `src/buffer/buffer.ts` — `TpmBufferWriter` (mirror writes + `writeTpm2b(bytes)` + `toUint8Array()`)
|
||||
- [x] 3.3 Out-of-bounds read raises `TPM_STRUCT.TRUNCATED` with cursor + requested-length in `cause`
|
||||
- [x] 3.4 `src/buffer/buffer.types.ts` — `TpmReaderOptions`, `TpmWriterOptions`, `Tpm2bReadResult`
|
||||
- [x] 3.5 Sibling `src/buffer/buffer.md` ≥200 words with basic + advanced examples
|
||||
|
||||
## 4. Implementation — algorithm registry
|
||||
|
||||
- [x] 4.1 `src/alg/alg.ts` — `TPM_ALG` frozen 31-entry registry per TCG Part 2 §11.1
|
||||
- [x] 4.2 `src/alg/alg.ts` — `TPM_ECC_CURVE` frozen 6-entry registry per TCG Part 2 §11.2.5
|
||||
- [x] 4.3 `algIdToName(id)` returns symbolic name; unknown raises `ALG_UNSUPPORTED`
|
||||
- [x] 4.4 `eccCurveToName(id)` returns symbolic name; unknown raises `CURVE_UNSUPPORTED`
|
||||
- [x] 4.5 `src/alg/alg.types.ts` — `TpmAlgId`, `TpmEccCurve` literal unions
|
||||
- [x] 4.6 Sibling `src/alg/alg.md` ≥200 words
|
||||
|
||||
## 5. Implementation — `TPMT_PUBLIC` parser + serialiser
|
||||
|
||||
- [x] 5.1 `src/public/public.ts` — `parseTpmtPublic(bytes): TpmtPublic` per TCG Part 2 §12.2 header (type, nameAlg, objectAttributes, authPolicy)
|
||||
- [x] 5.2 RSA branch: `TPMS_RSA_PARMS` per §12.2.3.5 + `TPM2B_PUBLIC_KEY_RSA` unique
|
||||
- [x] 5.3 ECC branch: `TPMS_ECC_PARMS` per §12.2.3.6 + `TPMS_ECC_POINT` unique with curveID lookup
|
||||
- [x] 5.4 KEYEDHASH branch: `TPMS_KEYEDHASH_PARMS` per §12.2.3.3 + `TPM2B_DIGEST` unique
|
||||
- [x] 5.5 SYMCIPHER branch: `TPMS_SYMCIPHER_PARMS` per §12.2.3.4 + `TPM2B_DIGEST` unique
|
||||
- [x] 5.6 `objectAttributes` reserved bits non-zero raises `OBJECT_ATTRIBUTES_RESERVED_NONZERO`
|
||||
- [x] 5.7 `src/public/serialize.ts` — `serializeTpmtPublic(pub): Uint8Array` byte-identical round-trip
|
||||
- [x] 5.8 `src/public/public.types.ts` — `TpmtPublic` discriminator + parameter / point / attribute types
|
||||
- [x] 5.9 Sibling `.md` for `public.ts` and `serialize.ts` (≥200 words each)
|
||||
|
||||
## 6. Implementation — `TPMS_ATTEST` parser + serialiser
|
||||
|
||||
- [x] 6.1 `src/attest/attest.ts` — magic validation: `0xff544347` mismatch raises `MAGIC_INVALID`
|
||||
- [x] 6.2 `parseTpmsAttest` reads type, qualifiedSigner (TPM2B_NAME), extraData (TPM2B_DATA), clockInfo (17 bytes), firmwareVersion (uint64 BE)
|
||||
- [x] 6.3 Eight `TPMU_ATTEST` branches: `TPM_ST_ATTEST_CERTIFY` ⇒ `TPMS_CERTIFY_INFO`; the other seven per TCG Part 2 §10.11
|
||||
- [x] 6.4 Unknown `type` raises `ATTEST_TYPE_UNSUPPORTED`
|
||||
- [x] 6.5 `src/attest/serialize.ts` — `serializeTpmsAttest(att): Uint8Array` byte-identical round-trip
|
||||
- [x] 6.6 `src/attest/attest.types.ts` — `TpmsAttest` discriminator + every `TPMU_ATTEST` member type + `ClockInfo`
|
||||
- [x] 6.7 Sibling `.md` for `attest.ts` and `serialize.ts` (≥200 words each)
|
||||
|
||||
## 7. Implementation — name digest
|
||||
|
||||
- [x] 7.1 `src/name/name.ts` — `computeTpmtPublicName(pub, hash): Uint8Array` per TCG Part 1 §16
|
||||
- [x] 7.2 Output = `nameAlg (2 BE) || hash.digest(serializeTpmtPublic(pub))`
|
||||
- [x] 7.3 `nameAlg === TPM_ALG_NULL` raises `NAME_ALG_NULL`
|
||||
- [x] 7.4 `src/name/name.types.ts` — `HashLike` interface
|
||||
- [x] 7.5 Sibling `src/name/name.md` ≥200 words
|
||||
|
||||
## 8. Implementation — extra-data helper
|
||||
|
||||
- [x] 8.1 `src/extra-data/extra-data.ts` — `extraDataMatches(actual, expected, equal): boolean`
|
||||
- [x] 8.2 Length mismatch returns false without calling `equal`
|
||||
- [x] 8.3 `src/extra-data/extra-data.types.ts` — `ConstantTimeEqual` signature
|
||||
- [x] 8.4 Sibling `.md` ≥200 words
|
||||
|
||||
## 9. Implementation — Zod schemas
|
||||
|
||||
- [x] 9.1 `src/schemas/schemas.ts` — every public type has a matching schema (`TpmsAttestSchema`, `TpmtPublicSchema`, `TpmsRsaParmsSchema`, `TpmsEccParmsSchema`, `TpmsKeyedHashParmsSchema`, `TpmsSymCipherParmsSchema`, `TpmsCertifyInfoSchema`, `ClockInfoSchema`, `Tpm2bSchema`, `TpmAlgIdSchema`, `TpmEccCurveSchema`, `ObjectAttributesSchema`)
|
||||
- [x] 9.2 Every public parser validates its output through the matching schema before returning
|
||||
- [x] 9.3 Schema failure raises `MALFORMED` with Zod issues on `cause.issues`
|
||||
- [x] 9.4 Sibling `.md` ≥200 words
|
||||
|
||||
## 10. Implementation — error registry
|
||||
|
||||
- [x] 10.1 `src/errors/error-codes.ts` — `defineCodes("TPM_STRUCT", [...])` 10 codes: `MAGIC_INVALID`, `TRUNCATED`, `ALG_UNSUPPORTED`, `CURVE_UNSUPPORTED`, `TYPE_MISMATCH`, `MALFORMED`, `NAME_ALG_NULL`, `ATTEST_TYPE_UNSUPPORTED`, `PARAMETERS_INVALID`, `OBJECT_ATTRIBUTES_RESERVED_NONZERO`
|
||||
- [x] 10.2 `TpmStructError extends SuaveplanError<TpmStructErrorCode>`
|
||||
- [x] 10.3 Every code thrown by ≥1 production path (registry-coverage test pins this)
|
||||
- [x] 10.4 Sibling `.md` ≥200 words
|
||||
|
||||
## 11. Implementation — OTel surface
|
||||
|
||||
- [x] 11.1 `src/observability/metrics.ts` — histograms `tpm_struct.parse_tpms_attest_duration_ms{outcome}`, `tpm_struct.parse_tpmt_public_duration_ms{type,outcome}`, `tpm_struct.compute_name_duration_ms{name_alg}`
|
||||
- [x] 11.2 Counters `tpm_struct.parse_total{kind,outcome}`, `tpm_struct.parse_errors_total{code}`, `tpm_struct.alg_total{alg}`
|
||||
- [x] 11.3 Every attribute is a bounded enum from the registries (no free-form strings)
|
||||
- [x] 11.4 Hot paths emit under a real in-memory `MeterProvider` (no mocks)
|
||||
- [x] 11.5 Sibling `.md` ≥200 words
|
||||
|
||||
## 12. Implementation — diagnostic emitter
|
||||
|
||||
- [x] 12.1 `src/diagnostic/diagnostic.ts` — `toDiagnostic(value: TpmsAttest | TpmtPublic): string`
|
||||
- [x] 12.2 Symbolic `TPM_ALG` and `TPM_ECC_CURVE` names inlined
|
||||
- [x] 12.3 Never invokes parser hot paths; never emits telemetry
|
||||
- [x] 12.4 Sibling `.md` ≥200 words
|
||||
|
||||
## 13. Fixtures + integration test
|
||||
|
||||
- [x] 13.1 `src/__fixtures__/webauthn-tpm-vector.ts` — real-world WebAuthn `tpm` attestation byte strings + expected parsed structures + expected name digest
|
||||
- [x] 13.2 `src/__integration__/webauthn-vector.test.ts` — `parseTpmsAttest(attestationData)` + `parseTpmtPublic(pubArea)` + `computeTpmtPublicName` round-trip matches expected digest
|
||||
- [x] 13.3 Test uses a real SHA-256 implementation injected via `HashLike` (no mocks)
|
||||
|
||||
## 14. Tests
|
||||
|
||||
- [x] 14.1 Unit tests at each `src/<feature>/<feature>.test.ts` covering every scenario in the spec delta
|
||||
- [x] 14.2 Property tests at `src/public/property.test.ts` and `src/attest/property.test.ts` (fast-check round-trip + single-bit-flip)
|
||||
- [x] 14.3 Spec-conformance test at `src/index.test.ts` resolves every subpath export
|
||||
- [x] 14.4 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async)
|
||||
- [x] 14.5 Every test imports runner primitives from `@suaveplan/testing/runner` — zero direct `from "vitest"` / `from "bun:test"` (per `openspec/AGENTS.md` §7)
|
||||
|
||||
## 15. Documentation
|
||||
|
||||
- [x] 15.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced examples, full API reference, Implementation Notes)
|
||||
- [x] 15.2 README links every co-located `.md` under `## Modules`
|
||||
- [x] 15.3 Update `CLAUDE.md` package listing under §Security / supply chain to add `tpm-struct`
|
||||
|
||||
## 16. Benchmarks
|
||||
|
||||
- [x] 16.1 `src/__benchmarks__/parse-tpms-attest.bench.ts` — ~1 KiB attest buffer
|
||||
- [x] 16.2 `src/__benchmarks__/parse-tpmt-public-rsa2048.bench.ts` — RSA-2048 public area
|
||||
- [x] 16.3 `src/__benchmarks__/parse-tpmt-public-ecc-p256.bench.ts` — ECC P-256 public area
|
||||
- [x] 16.4 `src/__benchmarks__/compute-name.bench.ts` — name digest with injected SHA-256
|
||||
- [x] 16.5 Baseline numbers published in README
|
||||
|
||||
## 17. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 17.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
|
||||
- [x] 17.2 `bunx turbo lint typecheck test --filter=@suaveplan/tpm-struct` — green
|
||||
- [x] 17.3 `bunx turbo test:coverage --filter=@suaveplan/tpm-struct` — 100/100/100/100
|
||||
- [x] 17.4 `bun scripts/openspec-conformance.ts @suaveplan/tpm-struct` — exit 0, no `error` findings
|
||||
- [x] 17.5 `bunx turbo build --filter=@suaveplan/tpm-struct` — green
|
||||
|
||||
## 18. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [x] 18.1 Generics threaded; `expectTypeOf` assertions on the literal unions (`TpmAlgId`, `TpmEccCurve`, `TpmStructErrorCode`, every discriminator)
|
||||
- [x] 18.2 Zod schemas wired at every declared boundary
|
||||
- [x] 18.3 Every error code in `error-codes.ts` thrown by at least one production path
|
||||
- [x] 18.4 `objectAttributes` reserved-bits-strict mode tested both on (raise) and off (default raise; documented invariant — no off-mode)
|
||||
- [x] 18.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
|
||||
- [x] 18.6 No I/O imports — `grep -rnE "from ['\"](node:|tpm-js|node-tss|tss.js)" packages/security/tpm-struct/src/` returns zero
|
||||
- [x] 18.7 No retroactive tick — every box above reflects work done in this change
|
||||
|
||||
## 19. Archive
|
||||
|
||||
- [x] 19.1 Fold spec delta into `openspec/specs/tpm-struct/spec.md`
|
||||
- [x] 19.2 Move `openspec/changes/enterprise-tpm-struct/` → `openspec/archive/2026-MM-DD-enterprise-tpm-struct/`
|
||||
- [x] 19.3 Refresh `MANIFEST.md` via `bun scripts/folder-reorg/generate-manifest.ts`
|
||||
- [x] 19.4 Refresh `MISSING.yml` via `bun scripts/generate-missing.ts`
|
||||
- [x] 19.5 Update `STATUS.md` to move `@suaveplan/tpm-struct` to Completed
|
||||
- [x] 19.6 Update `SUMMARY.md` with the per-package paragraph
|
||||
- [x] 19.7 `bunx markdownlint-cli2 --fix` on touched files; re-stage; verify clean
|
||||
Reference in New Issue
Block a user