first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
# @suaveplan/tpm-struct - Zero-dep, universal TPM 2.0 attestation-structure parser/serialiser. Decodes TPMS_ATTEST (TCG Part 2 §10.12) and TPMT_PUBLIC (§12.2) byte strings, computes canonical TPM names (Part 1 §16), and underpins WebAuthn 'tpm'-format attestation verification.
## Summary
Zero-runtime-dep TPM 2.0 attestation-structure parser
and serialiser. Decodes `TPMS_ATTEST` (TCG TPM 2.0 Library Part 2
§10.12), decodes `TPMT_PUBLIC` (§12.2), and computes canonical TPM 2.0
name digests per TCG Part 1 §16. Underpins WebAuthn `tpm`-format
attestation verification (W3C WebAuthn L3 §8.3) and every future TPM
2.0 attestation context (AIK enrollment, remote attestation, vTPM
provisioning) without dragging in a TPM driver, Node-only runtime, or
CommonJS legacy bridge.
The package is buffer-only: it neither imports a hash library nor
talks to a TPM device. Callers inject `HashLike` for `computeTpmtPublicName`
and `ConstantTimeEqual` for `extraDataMatches`, which keeps the
package free of crypto-engine selection while making the right shape
available at the WebAuthn step-5/step-8 call site.
<!-- begin:auto-docs -->
## Documentation tree
- [alg](./src/alg/alg.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [alg](./src/alg/alg.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - serialize — `serializeTpmsAttest` re-emits a parsed…
- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./src/attest/serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - serialize — `serializeTpmtPublic` re-emits a parsed…
- [object-attributes](./src/public/object-attributes.md) — `decodeObjectAttributes` (and its inverse `encodeObjectAttributes`) walk the 32-bit `TPMA_OBJECT` bitfield documented i…
- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./src/public/serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [alg](./src/alg/alg.md) — <!-- begin:auto-docs --> _(no submodules)_- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmsAttest` re-emits a parsed…
- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./src/attest/serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmtPublic` re-emits a parsed…
- [object-attributes](./src/public/object-attributes.md) — `decodeObjectAttributes` (and its inverse `encodeObjectAttributes`) walk the 32-bit `TPMA_OBJECT` bitfield documented i…
- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./src/public/serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg](./src/alg/alg.md) — <!-- begin:auto-docs --> _(no submodules)_- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmsAttest` re-emits a parsed…
- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./src/attest/serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmtPublic` re-emits a parsed…
- [object-attributes](./src/public/object-attributes.md) — `decodeObjectAttributes` (and its inverse `encodeObjectAttributes`) walk the 32-bit `TPMA_OBJECT` bitfield documented i…
- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./src/public/serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg](./src/alg/alg.md) — <!-- begin:auto-docs --> _(no submodules)_- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmsAttest` re-emits a parsed…
- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./src/attest/serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmtPublic` re-emits a parsed…
- [object-attributes](./src/public/object-attributes.md) — `decodeObjectAttributes` (and its inverse `encodeObjectAttributes`) walk the 32-bit `TPMA_OBJECT` bitfield documented i…
- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_- serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./src/public/serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg](./src/alg/alg.md) — <!-- begin:auto-docs --> _(no submodules)_- [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_ - [attest](./src/attest/attest.md) — <!-- begin:auto-docs --> _(no submodules)_ - [serialize](./src/attest/serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [buffer](./src/buffer/buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_ - [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_ - [extra-data](./src/extra-data/extra-data.md) — <!-- begin:auto-docs --> _(no submodules)_- [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_ - [name](./src/name/name.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_ - [object-attributes](./src/public/object-attributes.md) — `decodeObjectAttributes` (and its inverse `encodeObjectAttributes`) walk the 32-bit `TPMA_OBJECT` bitfield documented i…
- [public](./src/public/public.md) — <!-- begin:auto-docs --> _(no submodules)_ - [serialize](./src/public/serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/tpm-struct
```
## Quick start
```ts
import { createHash } from "node:crypto";
import {
parseTpmsAttest,
parseTpmtPublic,
computeTpmtPublicName,
extraDataMatches,
} from "@suaveplan/tpm-struct";
const att = parseTpmsAttest(attestationData); // TPMS_ATTEST
const pub = parseTpmtPublic(pubArea); // TPMT_PUBLIC
const name = computeTpmtPublicName(pub, { // 0x000b || sha256(...)
digest: (b) => new Uint8Array(createHash("sha256").update(b).digest()),
});
if (
att.type === "TPM_ST_ATTEST_CERTIFY" &&
extraDataMatches(att.attested.name, name, /* injected constantTimeEqual */ null!)
) {
// pubArea is the public area the TPM certified.
}
```
## Modules
| Module | Purpose |
|---|---|
| [`buffer`](./src/buffer/buffer.md) | `TpmBufferReader` / `TpmBufferWriter` over §10.1 octet streams. |
| [`alg`](./src/alg/alg.md) | `TPM_ALG` / `TPM_ECC_CURVE` frozen registries + symbolic lookup. |
| [`public/public`](./src/public/public.md) | `parseTpmtPublic` per §12.2. |
| [`public/serialize`](./src/public/serialize.md) | `serializeTpmtPublic` canonical encoder. |
| [`public/object-attributes`](./src/public/object-attributes.md) | `TPMA_OBJECT` bitfield decoder (§8.3). |
| [`attest/attest`](./src/attest/attest.md) | `parseTpmsAttest` per §10.12. |
| [`attest/serialize`](./src/attest/serialize.md) | `serializeTpmsAttest` canonical encoder. |
| [`name/name`](./src/name/name.md) | `computeTpmtPublicName` per Part 1 §16. |
| [`extra-data/extra-data`](./src/extra-data/extra-data.md) | WebAuthn step-8 `extraData` match. |
| [`schemas/schemas`](./src/schemas/schemas.md) | Zod boundary schemas. |
| [`errors/error-codes`](./src/errors/error-codes.md) | `TPM_STRUCT_CODES` + `TpmStructError`. |
| [`observability/metrics`](./src/observability/metrics.md) | OTel histograms + counters. |
| [`diagnostic/diagnostic`](./src/diagnostic/diagnostic.md) | Debug-only human-readable dumper. |
## Subpath exports
`.`, `./buffer`, `./alg`, `./public`, `./attest`, `./name`,
`./extra-data`, `./schemas`, `./errors`, `./diagnostic`.
## Dependencies
`@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`,
`zod`. No `node:*` imports; no external TPM tooling.
## License
MIT.

View File

@@ -0,0 +1,93 @@
# `alg` — TPM 2.0 algorithm and curve registries
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `alg` module exposes the two frozen registries every other module in
this package depends on: `TPM_ALG` (TCG TPM 2.0 Library Part 2 §11.1
ALG_TABLE) and `TPM_ECC_CURVE` (§11.2.5). The registries map between
on-the-wire UINT16 BE numeric identifiers and symbolic `TPM_ALG_<NAME>` /
`TPM_ECC_<NAME>` strings used throughout the discriminated unions on
`TpmtPublic.type`, `TpmtPublic.nameAlg`, and `TpmsEccParms.curveID`.
Symbolic names are also what flows into OTel attributes so dashboards
never see opaque hex IDs and so the attribute cardinality is bounded by
the registry size.
## Features
- Full §11.1 ALG_TABLE coverage (RSA, the SHA-1/2/3 family, AES,
KEYEDHASH, RSA/ECC schemes, KDF schemes, symmetric modes).
- Full §11.2.5 ECC curve table (NIST P-192/224/256/384/521, BN_P256/638,
SM2_P256).
- O(1) bidirectional lookup: `algIdToName` / `nameToAlgId` and
`eccCurveToName` / `nameToEccCurve`.
- Literal-typed unions `TpmAlgId` and `TpmEccCurve` so consumers can
switch over symbolic names with exhaustive checking.
- Unknown identifiers fail with the registered error codes
`TPM_STRUCT.ALG_UNSUPPORTED` and `TPM_STRUCT.CURVE_UNSUPPORTED` rather
than returning a fallback — the parser refuses to silently accept
values outside the registry because the TCG ID space includes
implementation-specific entries whose interpretation cannot be assumed.
## Usage: Basic Example
```ts
import { algIdToName } from "@suaveplan/tpm-struct/alg";
const name = algIdToName(0x000b); // "TPM_ALG_SHA256"
```
## Usage: Advanced Example
```ts
import {
algIdToName,
eccCurveToName,
nameToAlgId,
nameToEccCurve,
TPM_ALG,
TPM_ECC_CURVE,
} from "@suaveplan/tpm-struct/alg";
// Convert every entry in the registry to a (id, name) tuple for a
// diagnostic dump.
const all = Object.entries(TPM_ALG).map(([key, id]) => ({
key,
id,
name: algIdToName(id),
}));
// Round-trip a symbolic curve back to its numeric on-the-wire value.
const id = nameToEccCurve("TPM_ECC_NIST_P256"); // 0x0003
// And the inverse.
const sym = eccCurveToName(id); // "TPM_ECC_NIST_P256"
void nameToAlgId; // for completeness
void TPM_ECC_CURVE; // for completeness
```
## API Reference
- **`TPM_ALG`** — frozen `Record<AlgKey, number>` mapping symbolic
algorithm key to UINT16 wire id (e.g. `RSA = 0x0001`, `SHA256 = 0x000b`).
- **`TPM_ECC_CURVE`** — frozen `Record<CurveKey, number>` for §11.2.5.
- **`algIdToName(id: number): TpmAlgId`** — reverse lookup; raises
`TPM_STRUCT.ALG_UNSUPPORTED` on unknown ids.
- **`nameToAlgId(name: TpmAlgId): number`** — forward lookup; raises on
unknown names (e.g. mistyped string literal).
- **`eccCurveToName(id) / nameToEccCurve(name)`** — same shape, ECC table.
- **`TpmAlgId`** — literal union `\`TPM_ALG_${AlgKey}\``.
- **`TpmEccCurve`** — literal union `\`TPM_ECC_${CurveKey}\``.
## Implementation Notes
The registries are frozen at module load and the reverse-lookup maps are
built once in an immediately-invoked function so subsequent
`algIdToName` / `eccCurveToName` calls hit a `Map.get` rather than
re-scan the object. The registries intentionally over-cover the
`TPMI_ALG_PUBLIC` set used by `parseTpmtPublic` — the parser then
narrows back down to RSA/ECC/KEYEDHASH/SYMCIPHER and raises
`ALG_UNSUPPORTED` if a different (but otherwise registered) algorithm
appears in the `type` slot.

View File

@@ -0,0 +1,112 @@
# `attest/attest` — `TPMS_ATTEST` parser
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- serialize — `serializeTpmsAttest` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
- [serialize](./serialize.md) — `serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the TCG-canonical byte string per Library Part 2 §10.12.…
## Purpose
`parseTpmsAttest` decodes the TCG TPM 2.0 `TPMS_ATTEST` byte string per
Library Part 2 §10.12 into a discriminated-union value keyed on the
`TPMI_ST_ATTEST` `type` field. The eight supported variants
(`CERTIFY`, `QUOTE`, `SESSION_AUDIT`, `COMMAND_AUDIT`, `TIME`,
`CREATION`, `NV`, `NV_DIGEST`) each select the matching `TPMU_ATTEST`
member per §10.11. This is the primary entry point for any TPM 2.0
attestation-statement verifier: WebAuthn `tpm`-format relying parties,
remote-attestation servers, and AIK-credential issuers all start by
calling this function on the signed TPM-emitted blob.
## Features
- Magic-prefix validation (`TPM_GENERATED_VALUE = 0xff544347`) — fails
fast before any subsequent bytes are consumed.
- Full `TPMI_ST_ATTEST` coverage (eight variants).
- `qualifiedSigner: TPM2B_NAME` + `extraData: TPM2B_DATA` + 17-byte
`clockInfo` + `firmwareVersion: UINT64` decoded in spec order.
- Per-variant `TPMU_ATTEST` decoder: `CERTIFY → TPMS_CERTIFY_INFO`,
`QUOTE → TPMS_QUOTE_INFO` (with `TPML_PCR_SELECTION` capped at 32
banks), `COMMAND_AUDIT → TPMS_COMMAND_AUDIT_INFO`, etc.
- Boundary `TpmsAttestSchema` validates the constructed result; failures
raise `MALFORMED` carrying the Zod issues on `cause.detail`.
- OTel: `tpm_struct.parse_tpms_attest_duration_ms{outcome}` histogram +
`tpm_struct.parse_total{kind="attest",outcome}` counter.
## Usage: Basic Example
```ts
import { parseTpmsAttest } from "@suaveplan/tpm-struct/attest";
const att = parseTpmsAttest(certInfoBytes);
if (att.type === "TPM_ST_ATTEST_CERTIFY") {
// att.attested.name is the canonical name of the certified key
console.log(att.attested.name.byteLength);
}
```
## Usage: Advanced Example
```ts
import {
parseTpmsAttest,
parseTpmtPublic,
computeTpmtPublicName,
extraDataMatches,
} from "@suaveplan/tpm-struct";
// WebAuthn `tpm`-format step 4 (name match) + step 8 (extraData match).
function verifyWebAuthnTpm(
attestationData: Uint8Array,
pubArea: Uint8Array,
expectedToBeSigned: Uint8Array,
sha256: (b: Uint8Array) => Uint8Array,
constantTimeEqual: (a: Uint8Array, b: Uint8Array) => boolean
) {
const att = parseTpmsAttest(attestationData);
if (att.type !== "TPM_ST_ATTEST_CERTIFY") {
throw new Error("tpm: attest must be CERTIFY");
}
const pub = parseTpmtPublic(pubArea);
const computedName = computeTpmtPublicName(pub, { digest: sha256 });
if (!extraDataMatches(att.attested.name, computedName, constantTimeEqual)) {
throw new Error("tpm: pubArea name != attested name");
}
if (
!extraDataMatches(att.extraData, expectedToBeSigned, constantTimeEqual)
) {
throw new Error("tpm: extraData != attToBeSigned");
}
}
```
## API Reference
- **`parseTpmsAttest(bytes: Uint8Array): TpmsAttest`** — returns a
discriminated-union value. Throws `TpmStructError` with one of
`MAGIC_INVALID`, `ATTEST_TYPE_UNSUPPORTED`, `TRUNCATED`, `MALFORMED`,
or `ALG_UNSUPPORTED` (for the inner `digestAlg`/`hash` fields).
- **`TpmsAttest`** — discriminated union over `type`. Every variant
carries `magic`, `qualifiedSigner`, `extraData`, `clockInfo`,
`firmwareVersion`, `attested`.
## Implementation Notes
The 32-bank cap on `TPML_PCR_SELECTION` is a defensive bound the TPM
2.0 spec does not enforce strictly but every shipped TPM observes (one
selection per hash bank, with five hash banks the maximum in any shipped
firmware). The cap prevents an adversarial attest blob from causing the
parser to allocate an unbounded selection array. The cap raises
`MALFORMED` rather than `TRUNCATED` because the count is structurally
invalid even on an arbitrarily large buffer.

View File

@@ -0,0 +1,72 @@
# `attest/serialize` — `TPMS_ATTEST` canonical serialiser
## Purpose
`serializeTpmsAttest` re-emits a parsed `TpmsAttest` value as the
TCG-canonical byte string per Library Part 2 §10.12. The serialiser is
the inverse of `parseTpmsAttest`: for any input the parser accepts,
`serialize(parse(bytes))` returns the same bytes. This invariant is
load-bearing for two downstream uses: signature verification (the TPM
endorsement key signed these exact bytes — a re-serialisation that
differed by a single bit would silently fail every signature check) and
proxy / replay use cases that want to forward a verified attest blob
without trusting the originating wire to preserve the formatting.
## Features
- Byte-identical round-trip across all eight `TPMU_ATTEST` variants.
- Re-emits the 32-bit `magic` prefix regardless of source — the
serialiser is the authoritative encoder, not a passthrough.
- Re-emits `TPM2B_*` size prefixes from the decoded payload length so a
mutated stored size is silently ignored.
- Renders the 17-byte `clockInfo` substructure exactly per §10.10.
- Per-variant attested encoder for `CERTIFY`, `QUOTE`, `SESSION_AUDIT`,
`COMMAND_AUDIT`, `TIME`, `CREATION`, `NV`, `NV_DIGEST`.
## Usage: Basic Example
```ts
import {
parseTpmsAttest,
serializeTpmsAttest,
} from "@suaveplan/tpm-struct/attest";
const att = parseTpmsAttest(bytes);
const re = serializeTpmsAttest(att);
// `re` equals `bytes` byte-for-byte.
```
## Usage: Advanced Example
```ts
import {
parseTpmsAttest,
serializeTpmsAttest,
} from "@suaveplan/tpm-struct/attest";
// Proxy / verifier-of-verifiers pattern: validate, normalise, forward.
function relayAttestation(raw: Uint8Array, verifySig: (b: Uint8Array) => boolean) {
const att = parseTpmsAttest(raw);
const canonical = serializeTpmsAttest(att);
if (!verifySig(canonical)) {
throw new Error("attestation signature did not verify");
}
return { att, canonical };
}
```
## API Reference
- **`serializeTpmsAttest(att: TpmsAttest): Uint8Array`** — returns a
fresh byte string. Does not throw on any well-typed input — the
caller must construct `att` through `parseTpmsAttest` (or a Zod-
validated synthetic constructor) so the input is structurally valid.
## Implementation Notes
The serialiser dispatches on `att.type` via a `switch` that the
TypeScript compiler verifies as exhaustive. Each per-variant encoder
walks the §10.11 / §10.12 field order using `TpmBufferWriter` so size
prefixes and big-endian conventions stay locked to the reader semantics.
The serialiser writes into a single growing buffer to avoid the cost of
multiple `Uint8Array` allocations on the encoder hot path.

View File

@@ -0,0 +1,86 @@
# `buffer` — TPM 2.0 octet-stream reader and writer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `buffer` module provides the lowest layer of `@suaveplan/tpm-struct`: a
strictly-bounded, big-endian byte reader (`TpmBufferReader`) and a matching
growing-array writer (`TpmBufferWriter`) shaped to the TCG TPM 2.0 Library
Part 2 §10.1 octet-stream conventions. Every higher-level parser in the
package (the `TPMS_ATTEST` decoder, the `TPMT_PUBLIC` decoder, the name
serialiser) builds on these primitives, which is why they receive their
own subpath export. Keeping bounds enforcement, big-endian semantics, and
the `TPM2B_*` size-prefix protocol in one place means the rest of the
package can be written as straight-line code without per-field bounds
checks or accidental little-endian reads.
## Features
- Big-endian readers for UINT8/16/32/64 (the 64-bit form returns a
`bigint` so it never silently truncates).
- `TPM2B_*` size-prefix reader and writer (UINT16 length + payload).
- Cursor-stable error path: out-of-bounds reads raise
`TpmStructError(TPM_STRUCT.TRUNCATED)` carrying `cursor` and `requested`
on the cause chain so log adapters can format the failure.
- `assertEnd()` rejects trailing bytes — guards against silent length
drift between the wire format and consumers.
- Constant-time-safe `peek(n)` (no advancing, never throws on clip).
- Growing writer with geometric reallocation hint via `initialCapacity`.
## Usage: Basic Example
```ts
import { TpmBufferReader } from "@suaveplan/tpm-struct/buffer";
const reader = new TpmBufferReader(new Uint8Array([0x00, 0x04, 0xde, 0xad, 0xbe, 0xef]));
const payload = reader.readTpm2b(); // Uint8Array[0xde, 0xad, 0xbe, 0xef]
reader.assertEnd();
```
## Usage: Advanced Example
```ts
import { TpmBufferReader, TpmBufferWriter } from "@suaveplan/tpm-struct/buffer";
// Encode then decode an arbitrary TPMS_CLOCK_INFO substructure.
const w = new TpmBufferWriter({ initialCapacity: 17 });
w.writeUint64Be(100n); // clock
w.writeUint32Be(5); // resetCount
w.writeUint32Be(3); // restartCount
w.writeUint8(1); // safe = true
const bytes = w.toUint8Array();
const r = new TpmBufferReader(bytes);
const clock = r.readUint64Be();
const reset = r.readUint32Be();
const restart = r.readUint32Be();
const safe = r.readUint8() !== 0;
r.assertEnd();
```
## API Reference
- **`new TpmBufferReader(bytes, { cursor? })`** — construct over a
`Uint8Array`. Out-of-range `cursor` raises `TPM_STRUCT.TRUNCATED`.
- **`readUint8 / readUint16Be / readUint32Be`** — return `number`.
- **`readUint64Be()`** — returns `bigint`.
- **`readBytes(n)`** — returns a fresh `Uint8Array` copy (never aliases).
- **`readTpm2b()`** / **`readTpm2bWithSize()`** — TCG `TPM2B_*` size-prefix.
- **`peek(n)` / `remaining()` / `cursor()` / `assertEnd()`** — read-only
introspection.
- **`new TpmBufferWriter({ initialCapacity? })`** — growing writer.
- **`writeUint8 / 16Be / 32Be / 64Be / Bytes / Tpm2b`** — mirror writes.
- **`toUint8Array()`** — finalise into a fresh `Uint8Array`.
## Implementation Notes
The reader holds a single `DataView` over the supplied `Uint8Array`'s
backing buffer and advances a private cursor by the exact bytes consumed.
`readTpm2b` rewinds the cursor to the start of the size prefix on a
short-read failure so callers can produce diagnostic dumps from the
failing position. The writer grows its backing array by powers of two
starting at `initialCapacity` (default 256) and reuses the same buffer
across writes. Numeric range checks reject out-of-range writes at the
boundary — this is cheaper than letting `DataView` truncate silently.

View File

@@ -0,0 +1,99 @@
# `diagnostic` — human-readable dump of `TpmsAttest` / `TpmtPublic`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`toDiagnostic` renders a parsed `TpmsAttest` or `TpmtPublic` value as a
human-readable, grep-friendly multi-line string. The dumper exists for
debugging and for failed-attestation post-mortems — a TPM attestation
that fails verification produces a structured `TpmStructError`, but
the byte-level interpretation that led to the failure is what an
operator needs to root-cause the issue (was the wrong algorithm
negotiated? was the wire format corrupted in transit? did a particular
flag bit signal a different operating mode?). The diagnostic dumper
never emits telemetry — it is debug-only and SHOULD NOT be invoked on
production hot paths.
## Features
- Symbolic `TPM_ALG_*` and `TPM_ECC_*` names inlined throughout the
output (no opaque hex algorithm IDs).
- Type-aware rendering for every TpmsAttest variant (`CERTIFY`, `QUOTE`,
`SESSION_AUDIT`, `COMMAND_AUDIT`, `TIME`, `CREATION`, `NV`,
`NV_DIGEST`) and every TpmtPublic branch (RSA, ECC, KEYEDHASH,
SYMCIPHER).
- Compact rendering for `objectAttributes` — only the enabled flag
names appear; `{}` for none enabled.
- Hex rendering for every byte field (with `0x` prefix even on empty
payloads so the absence of bytes is visually distinct from a missing
field).
## Usage: Basic Example
```ts
import { parseTpmtPublic, toDiagnostic } from "@suaveplan/tpm-struct";
const pub = parseTpmtPublic(pubAreaBytes);
console.log(toDiagnostic(pub));
// TPMT_PUBLIC {
// type: TPM_ALG_RSA
// nameAlg: TPM_ALG_SHA256
// objectAttributes: { raw: 0x00040472, flags: {fixedTPM, fixedParent, sensitiveDataOrigin, userWithAuth, sign} }
// ...
// }
```
## Usage: Advanced Example
```ts
import {
parseTpmsAttest,
parseTpmtPublic,
toDiagnostic,
TpmStructError,
} from "@suaveplan/tpm-struct";
// Attach a diagnostic dump to a downstream verifier's structured log
// when verification fails.
function verifyAndLog(
attestBytes: Uint8Array,
pubAreaBytes: Uint8Array,
runVerify: (att: ReturnType<typeof parseTpmsAttest>, pub: ReturnType<typeof parseTpmtPublic>) => void
) {
try {
const att = parseTpmsAttest(attestBytes);
const pub = parseTpmtPublic(pubAreaBytes);
runVerify(att, pub);
} catch (e) {
if (e instanceof TpmStructError) {
try {
console.error("attest:\n" + toDiagnostic(parseTpmsAttest(attestBytes)));
} catch {
// attest itself was malformed; nothing to dump.
}
try {
console.error("pubArea:\n" + toDiagnostic(parseTpmtPublic(pubAreaBytes)));
} catch {
// pubArea itself was malformed; nothing to dump.
}
}
throw e;
}
}
```
## API Reference
- **`toDiagnostic(value: TpmsAttest | TpmtPublic): string`** — return a
multi-line dump. Discriminates on whether the value's `type` starts
with `TPM_ST_ATTEST_` (attest) or `TPM_ALG_` (public).
## Implementation Notes
The dumper is intentionally separate from any per-parser code path so
no production parser pays the rendering cost. Symbolic names appear
inline rather than as hex IDs so the output is grep-friendly — an
operator can search transcripts for `TPM_ALG_RSA` or `TPM_ECC_NIST_P256`
without first cross-referencing the registry.

View File

@@ -0,0 +1,104 @@
# `errors/error-codes` — `TPM_STRUCT` error registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `errors` module ships the canonical `defineCodes("TPM_STRUCT", [...])`
registry (10 codes) and the `TpmStructError extends SuaveplanError<TpmStructErrorCode>`
base class every other module throws from. Stable codes are
non-negotiable: consumers (WebAuthn verifiers, attestation gateways,
log scrapers) branch on `err.code` to distinguish "buffer was short"
from "magic prefix wrong" from "the registered code I care about for
my dashboard." A registry with a fixed alphabet is also what makes
the `tpm_struct.parse_errors_total{code}` OTel counter bounded.
## Features
- Ten registered codes covering every failure class from §8.3 (object
attributes), §10.x (attest), §11.x (algorithm and curve), §12.x
(public area), and §16 (name).
- `TpmStructError` extends `SuaveplanError<TpmStructErrorCode>` so
`instanceof` narrows the `.code` union.
- `tpmStructError(code, message, detail?)` helper attaches a structured
`detail` payload (cursor, requested length, Zod issues) to the cause
chain so log adapters can format failures without downcasting.
## Usage: Basic Example
```ts
import {
TPM_STRUCT_CODES,
TpmStructError,
tpmStructError,
} from "@suaveplan/tpm-struct/errors";
try {
// ... parser code
} catch (e) {
if (e instanceof TpmStructError) {
console.error(`tpm-struct failure: ${e.code}`);
if (e.code === TPM_STRUCT_CODES.TRUNCATED) {
// recoverable: ask the upstream for a longer buffer
}
}
}
void tpmStructError;
```
## Usage: Advanced Example
```ts
import {
TPM_STRUCT_CODES,
TpmStructError,
} from "@suaveplan/tpm-struct/errors";
type ParseFailureMetric = "magic" | "truncated" | "alg" | "curve" | "other";
function classify(err: TpmStructError): ParseFailureMetric {
switch (err.code) {
case TPM_STRUCT_CODES.MAGIC_INVALID:
return "magic";
case TPM_STRUCT_CODES.TRUNCATED:
return "truncated";
case TPM_STRUCT_CODES.ALG_UNSUPPORTED:
return "alg";
case TPM_STRUCT_CODES.CURVE_UNSUPPORTED:
return "curve";
default:
return "other";
}
}
// `cause.detail` carries the structured failure context.
function reportTruncated(err: TpmStructError) {
const detail =
((err as unknown as { cause?: { detail?: Record<string, unknown> } })
.cause?.detail) ?? {};
return { cursor: detail.cursor, requested: detail.requested };
}
```
## API Reference
- **`TPM_STRUCT_CODES`** — frozen registry: `MAGIC_INVALID`, `TRUNCATED`,
`ALG_UNSUPPORTED`, `CURVE_UNSUPPORTED`, `TYPE_MISMATCH`, `MALFORMED`,
`NAME_ALG_NULL`, `ATTEST_TYPE_UNSUPPORTED`, `PARAMETERS_INVALID`,
`OBJECT_ATTRIBUTES_RESERVED_NONZERO`. Each value is the literal
`TPM_STRUCT.<NAME>`.
- **`TpmStructError`** — base class, message is always prefixed with the
code literal.
- **`TpmStructErrorCode`** — literal union of the ten code strings.
- **`tpmStructError(code, message, detail?)`** — convenience constructor;
`detail` (when provided) is wrapped in an Error so it survives through
the `cause` chain.
## Implementation Notes
The error message is always prefixed with the code literal so log
scrapers find a stable substring without inspecting `.code`. Non-Error
detail payloads are wrapped in a synthetic `Error` carrying a `detail`
slot — this preserves the standard ECMAScript Error.cause shape while
still surfacing structured fields downstream code can read.

View File

@@ -0,0 +1,81 @@
# `extra-data` — `TPMS_ATTEST.extraData` constant-time match
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`extraDataMatches` compares a TPM-emitted `extraData` blob (the value the
TPM signed alongside the attest body) against the relying-party-recomputed
`attToBeSigned` digest per W3C WebAuthn Level 3 §8.3 step 8. The function
is intentionally tiny: it short-circuits on length mismatch (length is
public, not a secret) and then delegates the content comparison to a
caller-injected constant-time equality function. Keeping the constant-
time primitive injectable lets `@suaveplan/tpm-struct` stay universal-
tier (no `@suaveplan/crypto-utils` runtime dep) while letting consumers
wire in whichever hardened equality their crypto stack ships.
## Features
- Length short-circuit (length is not a secret).
- Pluggable equality function via the `ConstantTimeEqual` type.
- Zero-allocation: works directly over the supplied `Uint8Array`s.
## Usage: Basic Example
```ts
import { extraDataMatches } from "@suaveplan/tpm-struct/extra-data";
declare const constantTimeEqual: (a: Uint8Array, b: Uint8Array) => boolean;
if (extraDataMatches(att.extraData, recomputedDigest, constantTimeEqual)) {
// attToBeSigned matched.
}
```
## Usage: Advanced Example
```ts
import { constantTimeEqual } from "@suaveplan/crypto-utils";
import { extraDataMatches } from "@suaveplan/tpm-struct/extra-data";
// Compose with the rest of the WebAuthn `tpm`-attestation chain.
function verifyExtraDataAndName(
att: { extraData: Uint8Array; attested: { name: Uint8Array } },
recomputedAttToBeSigned: Uint8Array,
recomputedName: Uint8Array
): { extraDataOk: boolean; nameOk: boolean } {
return {
extraDataOk: extraDataMatches(
att.extraData,
recomputedAttToBeSigned,
constantTimeEqual
),
nameOk: extraDataMatches(
att.attested.name,
recomputedName,
constantTimeEqual
),
};
}
```
## API Reference
- **`extraDataMatches(actual: Uint8Array, expected: Uint8Array, equal: ConstantTimeEqual): boolean`**
— `true` if and only if `actual.length === expected.length` and
`equal(actual, expected)` returns true. Returns `false` on length
mismatch without invoking `equal`.
- **`ConstantTimeEqual`** — `(a: Uint8Array, b: Uint8Array) => boolean`.
Implementations MUST NOT short-circuit on first byte mismatch — the
wall-time MUST depend only on the longer input's length.
## Implementation Notes
The function deliberately does not own a constant-time-equality fallback.
A naive `for` loop short-circuiting on mismatch would leak content
equality through timing; a sample implementation that XOR-accumulates
every byte is correct but lives in `@suaveplan/crypto-utils`. By keeping
the primitive injectable, this module avoids importing a crypto package
from a universal parser while still surfacing the right API shape
for the WebAuthn step 8 check.

View File

@@ -0,0 +1,86 @@
# `name/name` — TPM 2.0 canonical name digest
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`computeTpmtPublicName` produces the canonical TPM 2.0 name for a
`TpmtPublic` value per TCG TPM 2.0 Library Part 1 — Architecture §16
_Names_. The canonical name is the two-byte big-endian `nameAlg`
identifier followed by the digest of the canonical `TPMT_PUBLIC` byte
serialisation under that algorithm. The W3C WebAuthn Level 3 `tpm`-
attestation verifier compares this exact value against the certified
name carried in the `TPMS_CERTIFY_INFO.name` slot to prove that the
public area the relying party recovered is the one the TPM signed
(WebAuthn L3 §8.3 step 5).
## Features
- Hash backend injected via a `HashLike` interface so this package
stays universal; consumers wire `@suaveplan/crypto-hash` (or any
compatible primitive) at the call site.
- Caller-controlled algorithm selection — the injected hash MUST match
`pub.nameAlg`.
- Rejects `pub.nameAlg === TPM_ALG_NULL` with `TPM_STRUCT.NAME_ALG_NULL`
because a canonical name cannot be computed without a digest.
- OTel: `tpm_struct.compute_name_duration_ms{name_alg}` histogram +
`tpm_struct.parse_total{kind="name",outcome}` counter.
## Usage: Basic Example
```ts
import { createHash } from "node:crypto";
import {
computeTpmtPublicName,
parseTpmtPublic,
} from "@suaveplan/tpm-struct";
const pub = parseTpmtPublic(pubAreaBytes);
const name = computeTpmtPublicName(pub, {
digest: (b) => new Uint8Array(createHash("sha256").update(b).digest()),
});
// name === 0x000b || sha256(canonical TPMT_PUBLIC bytes)
```
## Usage: Advanced Example
```ts
import { sha256, sha384, sha512 } from "@noble/hashes/sha2";
import { sha1 } from "@noble/hashes/legacy";
import {
computeTpmtPublicName,
parseTpmtPublic,
} from "@suaveplan/tpm-struct";
// Route to the correct backend at call time based on the public area's
// nameAlg. The package itself imports no hash code; the caller picks
// the engine.
function hashForNameAlg(nameAlg: string) {
if (nameAlg === "TPM_ALG_SHA1") return { digest: sha1 };
if (nameAlg === "TPM_ALG_SHA256") return { digest: sha256 };
if (nameAlg === "TPM_ALG_SHA384") return { digest: sha384 };
if (nameAlg === "TPM_ALG_SHA512") return { digest: sha512 };
throw new Error(`unsupported nameAlg ${nameAlg}`);
}
const pub = parseTpmtPublic(pubAreaBytes);
const name = computeTpmtPublicName(pub, hashForNameAlg(pub.nameAlg));
```
## API Reference
- **`computeTpmtPublicName(pub: TpmtPublic, hash: HashLike): Uint8Array`**
— return a fresh byte string `nameAlg (2 BE) || hash.digest(serializeTpmtPublic(pub))`.
Throws `TpmStructError(NAME_ALG_NULL)` on `pub.nameAlg ===
"TPM_ALG_NULL"`. Re-throws any error raised by `hash.digest`.
- **`HashLike`** — `{ digest(bytes: Uint8Array): Uint8Array }`. The
digest size MUST match the algorithm (SHA-1 → 20, SHA-256 → 32, etc.).
## Implementation Notes
The function delegates to `serializeTpmtPublic` for the canonical byte
encoding so the parser and the name digest cannot drift independently.
The 2-byte `nameAlg` prefix is the big-endian wire id (looked up via
`nameToAlgId`) rather than the symbolic name — this matches the binary
form the TPM emits and that downstream verifiers reproduce.

View File

@@ -0,0 +1,115 @@
# `observability/metrics` — OTel histograms and counters
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `observability/metrics` module owns the OpenTelemetry surface of
`@suaveplan/tpm-struct`. Every public parser invocation records into
one of three histograms and one of three counters; the histograms
bucket wall-time by `outcome` (and by `type` / `name_alg` where it adds
value), and the counters expose hit / error rates with low-cardinality
attributes drawn from the bounded `TPM_ALG`, `TPM_STRUCT_CODES`, and
`PublicBranch` enums. The full surface is what makes a running TPM-
attestation gateway observable: a sudden spike in
`tpm_struct.parse_errors_total{code="TPM_STRUCT.MAGIC_INVALID"}` is
the canonical signal that an upstream is sending corrupted or
non-TPM blobs.
## Features
- Three histograms: `parse_tpms_attest_duration_ms{outcome}`,
`parse_tpmt_public_duration_ms{type,outcome}`,
`compute_name_duration_ms{name_alg}`.
- Three counters: `parse_total{kind,outcome}`, `parse_errors_total{code}`,
`alg_total{alg}`.
- Every attribute value is a bounded enum (no free-form strings).
- Lazy instrument resolution — instruments are created on first call so
consumers can install a custom `MeterProvider` before any module-load
side effects.
- `resetTpmStructMetricsForTesting()` clears the instrument cache for
tests that swap the global provider between cases.
## Usage: Basic Example
```ts
import { metrics } from "@opentelemetry/api";
import { parseTpmsAttest } from "@suaveplan/tpm-struct";
// Once at app boot, install your MeterProvider.
metrics.setGlobalMeterProvider(myProvider);
// Subsequent calls emit metrics automatically.
parseTpmsAttest(attestBytes);
```
## Usage: Advanced Example
```ts
import {
metrics,
type Counter,
type Histogram,
type Meter,
type MeterProvider,
} from "@opentelemetry/api";
import {
parseTpmsAttest,
resetTpmStructMetricsForTesting,
} from "@suaveplan/tpm-struct";
// In-memory provider for assertions in tests.
function recordingProvider() {
const counters: { name: string; value: number; attrs?: Record<string, string> }[] = [];
const histograms: typeof counters = [];
const meter: Partial<Meter> = {
createCounter: (name: string) =>
({
add: (v: number, a?: Record<string, string>) =>
counters.push({ name, value: v, attrs: a }),
}) as unknown as Counter,
createHistogram: (name: string) =>
({
record: (v: number, a?: Record<string, string>) =>
histograms.push({ name, value: v, attrs: a }),
}) as unknown as Histogram,
};
const provider: Partial<MeterProvider> = {
getMeter: () => meter as Meter,
};
return { provider: provider as MeterProvider, counters, histograms };
}
const { provider, counters, histograms } = recordingProvider();
metrics.setGlobalMeterProvider(provider);
resetTpmStructMetricsForTesting();
parseTpmsAttest(certifyBytes);
console.log(counters, histograms);
```
## API Reference
- **`recordAttestDuration(outcome, durationMs)`** — record a
`parse_tpms_attest_duration_ms` observation.
- **`recordPublicDuration(type, outcome, durationMs)`** — record a
`parse_tpmt_public_duration_ms` observation.
- **`recordNameDuration(nameAlg, durationMs)`** — record a
`compute_name_duration_ms` observation.
- **`recordParseDuration(kind, branchOrAlg, outcome, durationMs)`** —
dispatch helper used by every parser.
- **`recordParseTotal(kind, outcome)`** — increment `parse_total`.
- **`recordParseError(err)`** — increment `parse_errors_total{code}`
using the registered code when `err` is a `TpmStructError`, or
`"TPM_STRUCT.OTHER"` otherwise (sentinel label, not a registry
member).
- **`recordAlgObserved(alg)`** — increment `alg_total{alg}`.
- **`resetTpmStructMetricsForTesting()`** — clear the cached instruments.
## Implementation Notes
Instruments are cached at module scope so the per-call cost stays a
single `Map.get`. The `OTHER` sentinel on `parse_errors_total` is a
defensive label — the parsers only ever throw `TpmStructError`, but
the helper accepts `unknown` because library consumers may forward
arbitrary failures through a wrapping function.

View File

@@ -0,0 +1,84 @@
# `public/object-attributes` — `TPMA_OBJECT` bitfield decoder
## Purpose
`decodeObjectAttributes` (and its inverse `encodeObjectAttributes`) walk
the 32-bit `TPMA_OBJECT` bitfield documented in TCG TPM 2.0 Library
Part 2 §8.3. The bitfield is transmitted big-endian on the wire and
controls every meaningful property of a TPM-managed object — whether it
can sign or decrypt, whether it is `fixedTPM`/`fixedParent`, whether the
TPM imposes a dictionary-attack lockout (`noDA`), and so on. The
decoder enforces the §8.3 normative requirement that reserved bits MUST
be transmitted as 0; a non-zero reserved bit raises
`TPM_STRUCT.OBJECT_ATTRIBUTES_RESERVED_NONZERO` rather than silently
ignoring the bit, because the most likely root cause is a wire-format
drift or an attempted attack on an attribute downstream consumers will
not inspect.
## Features
- Decodes the 11 named flags at the §8.3 normative positions:
`fixedTPM` (1), `stClear` (2), `fixedParent` (4),
`sensitiveDataOrigin` (5), `userWithAuth` (6), `adminWithPolicy` (7),
`noDA` (10), `encryptedDuplication` (11), `restricted` (16),
`decrypt` (17), `sign` (18).
- Exposes the raw uint32 alongside the named flags so the canonical
serialiser can re-emit the exact bit pattern even if a future revision
of §8.3 adds new named flags.
- Constant-time mask check for reserved bits; any reserved bit set raises
with the exact mask of offending bits in the error message.
## Usage: Basic Example
```ts
import { decodeObjectAttributes } from "@suaveplan/tpm-struct/public";
const attrs = decodeObjectAttributes(0x00040472);
console.log(attrs.flags.fixedTPM, attrs.flags.sign); // true true
```
## Usage: Advanced Example
```ts
import {
decodeObjectAttributes,
encodeObjectAttributes,
} from "@suaveplan/tpm-struct/public";
const attrs = decodeObjectAttributes(0x00040472);
// Use the typed flag struct to gate a downstream operation.
function requireSigningKey(attrs: ReturnType<typeof decodeObjectAttributes>) {
if (!attrs.flags.sign) {
throw new Error("key is not a signing key");
}
if (attrs.flags.restricted) {
// Restricted signing keys can only sign TPM-internal structures;
// reject for raw signing requests.
throw new Error("restricted signing key");
}
}
// Round-trip preservation: encode → raw → decode again.
const raw = encodeObjectAttributes(attrs);
console.log(decodeObjectAttributes(raw).raw === raw); // true
```
## API Reference
- **`decodeObjectAttributes(raw: number): ObjectAttributes`** — return
`{raw, flags}`. Throws `TpmStructError(OBJECT_ATTRIBUTES_RESERVED_NONZERO)`
on any reserved bit non-zero.
- **`encodeObjectAttributes(attrs: ObjectAttributes): number`** — return
the raw uint32 bit pattern.
- **`FLAG_BITS`** — map of flag name → bit position; useful for tests.
- **`NAMED_BIT_MASK`** — the union of all named bit positions.
## Implementation Notes
The decoder uses unsigned right-shift (`>>>`) throughout so the high bit
of the 32-bit value is treated as unsigned. The reserved-bit mask is
computed at module load by OR-ing every `FLAG_BITS` entry; everything
outside that mask is considered reserved. Adding a new named flag is a
two-line change: extend `FLAG_BITS` and the corresponding interface in
`public.types.ts`.

View File

@@ -0,0 +1,102 @@
# `public/public` — `TPMT_PUBLIC` parser
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- serialize — `serializeTpmtPublic` re-emits a parsed…
- [serialize](./serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
- [serialize](./serialize.md) — `serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the TCG-canonical byte string per Library Part 2 §12.2. I…
## Purpose
`parseTpmtPublic` decodes a TCG TPM 2.0 `TPMT_PUBLIC` byte string per
Library Part 2 §12.2 into a discriminated-union value keyed on the
`TPMI_ALG_PUBLIC` `type` field. The four supported branches —
`TPM_ALG_RSA`, `TPM_ALG_ECC`, `TPM_ALG_KEYEDHASH`, `TPM_ALG_SYMCIPHER` —
each decode their own `TPMS_*_PARMS` parameter substructure and matching
`TPMU_PUBLIC_ID` unique slot. The parser is the central seam between the
on-the-wire byte format and every downstream consumer of TPM public
areas: `computeTpmtPublicName` re-emits the bytes through
`serializeTpmtPublic` and hashes them for the canonical Part 1 §16 name
digest, while the WebAuthn `tpm`-format verifier validates the `pubArea`
against the credential public key in `authData.attestedCredentialData`.
## Features
- Full §12.2 field order enforcement (`type` → `nameAlg` →
`objectAttributes` → `authPolicy` → `parameters` → `unique`).
- RSA-2048/3072/4096 with default-exponent (`0` → `65537`) normalisation
per §12.2.3.5 plus a `rawExponent` slot preserved for round-trip
fidelity.
- ECC P-256/P-384/P-521/BN-256/BN-638/SM2-256 with `curveID` lookup via
the §11.2.5 registry; unknown curves raise `CURVE_UNSUPPORTED`.
- KEYEDHASH NULL / HMAC / XOR schemes per §12.2.3.3.
- SYMCIPHER with full `TPMT_SYM_DEF_OBJECT` decoding per §12.2.3.4.
- `objectAttributes` decoded into raw `uint32` + typed flag struct;
reserved bits non-zero raise `OBJECT_ATTRIBUTES_RESERVED_NONZERO`.
- Boundary Zod schema (`TpmtPublicSchema`) validates the constructed
result before returning — malformed parser output raises `MALFORMED`.
- OTel histograms (`tpm_struct.parse_tpmt_public_duration_ms{type,outcome}`)
- counters (`tpm_struct.parse_total{kind="public",outcome}` and
`tpm_struct.parse_errors_total{code}`).
## Usage: Basic Example
```ts
import { parseTpmtPublic } from "@suaveplan/tpm-struct/public";
const pub = parseTpmtPublic(pubAreaBytes);
if (pub.type === "TPM_ALG_RSA") {
console.log(pub.parameters.keyBits, pub.parameters.exponent);
console.log(pub.unique.modulus.byteLength);
}
```
## Usage: Advanced Example
```ts
import { createHash } from "node:crypto";
import {
computeTpmtPublicName,
parseTpmtPublic,
} from "@suaveplan/tpm-struct";
const pub = parseTpmtPublic(pubAreaBytes);
if (pub.nameAlg === "TPM_ALG_SHA256" && pub.type === "TPM_ALG_ECC") {
const name = computeTpmtPublicName(pub, {
digest: (b) => new Uint8Array(createHash("sha256").update(b).digest()),
});
// `name` is `0x000b || sha256(serializeTpmtPublic(pub))` — feed this
// into the WebAuthn `tpm`-attestation name-match step (W3C WebAuthn
// L3 §8.3 step 5).
}
```
## API Reference
- **`parseTpmtPublic(bytes: Uint8Array): TpmtPublic`** — returns a
discriminated-union value. Throws `TpmStructError` with one of
`ALG_UNSUPPORTED`, `CURVE_UNSUPPORTED`, `OBJECT_ATTRIBUTES_RESERVED_NONZERO`,
`PARAMETERS_INVALID`, `TRUNCATED`, or `MALFORMED`.
- **`TpmtPublic`** — discriminator union over `type`. Each branch carries
`nameAlg`, `objectAttributes`, `authPolicy`, `parameters`, `unique`.
## Implementation Notes
The parser is deliberately straight-line: every field decode advances the
underlying `TpmBufferReader`'s cursor by exactly the bytes named in the
spec. `assertEnd()` is called at the end of every successful parse so a
trailing byte is reported as `TRUNCATED` rather than silently dropped.
The exponent-zero normalisation rule is preserved alongside `rawExponent`
because re-serialisation must emit the original bytes so the §16 name
digest stays stable.

View File

@@ -0,0 +1,86 @@
# `public/serialize` — `TPMT_PUBLIC` canonical serialiser
## Purpose
`serializeTpmtPublic` re-emits a parsed `TpmtPublic` value as the
TCG-canonical byte string per Library Part 2 §12.2. It is the
round-trip partner of `parseTpmtPublic`: for any input the parser
accepts, `serialize(parse(bytes))` returns the same bytes. This
property is load-bearing — `computeTpmtPublicName` per Part 1 §16
hashes the canonical bytes to produce the TPM name digest, and any
mismatch between the parser and the serialiser would silently produce a
wrong name, which in turn would silently fail every downstream
attestation check.
## Features
- Byte-identical round-trip across RSA, ECC, KEYEDHASH, and SYMCIPHER
branches.
- Preserves `rawExponent` (default-exponent normalisation is a parse-time
convenience; the wire bytes are emitted unchanged).
- Re-writes every `TPM2B_*` size prefix from the decoded payload length —
the serialiser never trusts a stored size value.
- Schema-validated input via the public API: only outputs of
`parseTpmtPublic` (which are validated) can reach the serialiser.
## Usage: Basic Example
```ts
import {
parseTpmtPublic,
serializeTpmtPublic,
} from "@suaveplan/tpm-struct/public";
const pub = parseTpmtPublic(bytes);
const reSerialised = serializeTpmtPublic(pub);
// reSerialised equals bytes byte-for-byte.
```
## Usage: Advanced Example
```ts
import {
parseTpmtPublic,
serializeTpmtPublic,
} from "@suaveplan/tpm-struct/public";
// Re-emit pub-area bytes through the canonical serialiser before signing
// or hashing. This is the canonical witness for TCG TPM 2.0 Library
// Part 1 §16 name computation.
function canonicalPubArea(rawPubArea: Uint8Array): Uint8Array {
return serializeTpmtPublic(parseTpmtPublic(rawPubArea));
}
// Property test pattern used by `property.test.ts`:
import fc from "fast-check";
fc.assert(
fc.property(fc.uint8Array({ maxLength: 600 }), (bytes) => {
// Parsing might throw; that is expected for invalid inputs.
// The serialiser is exercised only on parse-success paths.
let parsed;
try {
parsed = parseTpmtPublic(bytes);
} catch {
return true;
}
return serializeTpmtPublic(parsed).join() === bytes.join();
}),
{ numRuns: 100 }
);
```
## API Reference
- **`serializeTpmtPublic(pub: TpmtPublic): Uint8Array`** — returns a
fresh byte string. Throws `TpmStructError(PARAMETERS_INVALID)` if a
parameter substructure is internally inconsistent (e.g. non-NULL RSA
scheme without `hashAlg`).
## Implementation Notes
The serialiser dispatches on `pub.type` and walks the §12.2 field order.
The symmetric / scheme / KDF sub-writers each consult their algorithm
discriminator to decide whether trailing fields are emitted (e.g. an RSA
scheme of `TPM_ALG_NULL` emits only the 2-byte scheme id and no hashAlg).
This mirrors how the parser reads those substructures and is what makes
the round-trip property hold across the full input space.

View File

@@ -0,0 +1,84 @@
# `schemas` — Zod boundary schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `schemas` module exports a Zod schema for every public return type
of `@suaveplan/tpm-struct`. Every public parser validates its
constructed result through the matching schema BEFORE returning. A
schema-validation failure raises `TpmStructError(TPM_STRUCT.MALFORMED)`
with the Zod issue array attached to `cause.detail.issues`. The schemas
are the last line of defence against parser bugs: even if the byte
decoder produces a misshaped object (e.g. due to a future refactor),
the boundary catches the mismatch before the value escapes into a
consumer.
## Features
- Schema coverage for: `TpmsAttest`, `TpmtPublic`, the four parameter
substructures (`TpmsRsaParms`, `TpmsEccParms`, `TpmsKeyedHashParms`,
`TpmsSymCipherParms`), `TpmsCertifyInfo`, `ClockInfo`, `Tpm2b`,
`TpmAlgId`, `TpmEccCurve`, and `ObjectAttributes`.
- `TpmtPublicSchema` and `TpmsAttestSchema` are discriminated unions on
the public `type` field so consumers can narrow the parsed result
exhaustively without manual `if/else` chains.
- `validateTpmtPublic` / `validateTpmsAttest` wrap `safeParse` and
rethrow Zod failures as typed `TpmStructError` instances.
## Usage: Basic Example
```ts
import { TpmtPublicSchema } from "@suaveplan/tpm-struct/schemas";
const parsed = TpmtPublicSchema.parse(value); // throws ZodError on failure
```
## Usage: Advanced Example
```ts
import {
parseTpmtPublic,
TpmtPublicSchema,
} from "@suaveplan/tpm-struct";
// Re-validate a deserialised JSON payload at the consumer boundary
// (e.g. an admin tool that snapshots parsed TpmtPublic to JSON for
// inspection). Note that JSON cannot round-trip `Uint8Array` directly —
// the consumer must rehydrate `Uint8Array` slots before parsing.
function rehydrateAndValidate(jsonShape: unknown): ReturnType<typeof parseTpmtPublic> {
const candidate = rehydrateBytes(jsonShape);
// Validate with the schema; throws ZodError on failure (the
// package's own error wrapping is reserved for parser-emitted
// failures, not admin-tool ingestion).
return TpmtPublicSchema.parse(candidate);
}
declare function rehydrateBytes(v: unknown): unknown;
```
## API Reference
- **`TpmtPublicSchema`** — discriminated union on `type` with four
branches.
- **`TpmsAttestSchema`** — discriminated union on `type` with eight
branches.
- **`TpmsRsaParmsSchema` / `TpmsEccParmsSchema` /
`TpmsKeyedHashParmsSchema` / `TpmsSymCipherParmsSchema`** — parameter
substructures.
- **`TpmsCertifyInfoSchema` / `ClockInfoSchema`** — common substructures.
- **`Tpm2bSchema`** — runtime check that a value is a `Uint8Array`.
- **`TpmAlgIdSchema` / `TpmEccCurveSchema`** — string regex for symbolic
names.
- **`ObjectAttributesSchema`** — `{raw, flags}` shape.
- **`validateTpmtPublic(value)` / `validateTpmsAttest(value)`** —
throwing wrappers raising `TPM_STRUCT.MALFORMED`.
## Implementation Notes
The discriminated-union schemas use `z.discriminatedUnion("type", [...])`
so Zod evaluates only the matching branch. This keeps validation
proportional to the size of the matching branch rather than the union;
on the hot path that matters because every successful parse runs the
matching schema once before returning.