first commit
This commit is contained in:
133
docs/security-stack/security/tpm-struct/README.md
Normal file
133
docs/security-stack/security/tpm-struct/README.md
Normal 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.
|
||||
|
||||
93
docs/security-stack/security/tpm-struct/src/alg/alg.md
Normal file
93
docs/security-stack/security/tpm-struct/src/alg/alg.md
Normal 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.
|
||||
112
docs/security-stack/security/tpm-struct/src/attest/attest.md
Normal file
112
docs/security-stack/security/tpm-struct/src/attest/attest.md
Normal 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.
|
||||
@@ -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.
|
||||
86
docs/security-stack/security/tpm-struct/src/buffer/buffer.md
Normal file
86
docs/security-stack/security/tpm-struct/src/buffer/buffer.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
86
docs/security-stack/security/tpm-struct/src/name/name.md
Normal file
86
docs/security-stack/security/tpm-struct/src/name/name.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
102
docs/security-stack/security/tpm-struct/src/public/public.md
Normal file
102
docs/security-stack/security/tpm-struct/src/public/public.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user