Initial Commit
This commit is contained in:
108
packages/foundation/data/packer/src/integrity/integrity.md
Normal file
108
packages/foundation/data/packer/src/integrity/integrity.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `integrity/integrity` — BLAKE3 per-entry digest and bundle-level Merkle root
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/packer/integrity` provides the content-addressable integrity layer
|
||||
the rest of the package uses to detect any tampering or accidental corruption
|
||||
of packed bundles. It computes a per-entry BLAKE3-256 digest and a
|
||||
bundle-level Merkle root over the sorted entry digests; both are recorded in
|
||||
the produced envelope (whole-file `PackedDataMetadata.integrity`, chunked
|
||||
`Manifest.merkleRoot` plus per-entry `ManifestChunkEntry.entryDigest`) and
|
||||
verified on read. Verification coexists with the existing ed25519 signing
|
||||
pipeline — neither replaces the other — because BLAKE3 is fast enough to run
|
||||
on every chunk without measurable overhead while ed25519 still anchors the
|
||||
publisher's identity.
|
||||
|
||||
## Features
|
||||
|
||||
- BLAKE3-256 one-shot digest of arbitrary bytes via `@noble/hashes/blake3`.
|
||||
- `merkleRootBlake3(digests)` over already-sorted entry digests.
|
||||
- `computeWholeFileIntegrity(payload)` convenience producing the `{ entryDigest, merkleRoot }`
|
||||
pair stored on `PackedDataMetadata.integrity`.
|
||||
- Constant-time `bytesEqual` for use in verify paths.
|
||||
- Strict input validation: every digest must be exactly 32 bytes; mismatches
|
||||
raise `TypeError` (programmer error — not a packer error code).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
entryDigestBlake3,
|
||||
merkleRootBlake3,
|
||||
computeWholeFileIntegrity,
|
||||
} from "@suaveplan/packer/integrity";
|
||||
|
||||
const payload = new TextEncoder().encode("hello world");
|
||||
const digest = entryDigestBlake3(payload);
|
||||
console.log(digest.byteLength); // 32
|
||||
|
||||
const integrity = computeWholeFileIntegrity(payload);
|
||||
console.log(integrity.entryDigest.byteLength); // 32
|
||||
console.log(integrity.merkleRoot.byteLength); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Build a Merkle root over a chunked bundle's entries, sorted by `chunkId`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
entryDigestBlake3,
|
||||
merkleRootBlake3,
|
||||
bytesEqual,
|
||||
} from "@suaveplan/packer/integrity";
|
||||
|
||||
const chunks = [/* wire bytes from packChunks(...) */];
|
||||
const digests = chunks
|
||||
.map((c) => ({ id: c.chunkId, d: entryDigestBlake3(c.bytes) }))
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((x) => x.d);
|
||||
const root = merkleRootBlake3(digests);
|
||||
|
||||
// Later, on read:
|
||||
const recomputed = merkleRootBlake3(digests);
|
||||
if (!bytesEqual(root, recomputed)) {
|
||||
throw new Error("Merkle root drifted between pack and verify");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `entryDigestBlake3(data: Uint8Array): Uint8Array` — BLAKE3-256 of `data`.
|
||||
Returns a fresh 32-byte array.
|
||||
- `merkleRootBlake3(digests: readonly Uint8Array[]): Uint8Array` — BLAKE3-256
|
||||
of the byte concatenation of `digests` (caller supplies sort order).
|
||||
Empty input maps to `blake3("")`. Each digest must be 32 bytes;
|
||||
mismatches throw `TypeError`.
|
||||
- `computeWholeFileIntegrity(payload): PackedDataIntegrity` — single-entry
|
||||
convenience returning `{ entryDigest: blake3(payload), merkleRoot: blake3(blake3(payload)) }`.
|
||||
- `bytesEqual(a, b): boolean` — constant-time byte-equality test on two
|
||||
`Uint8Array`s.
|
||||
- `BLAKE3_DIGEST_LEN: 32` — exported constant for callers that need the
|
||||
digest length without recomputing.
|
||||
- `PackedDataIntegrity` — `{ entryDigest, merkleRoot }` shape stored on
|
||||
`PackedDataMetadata.integrity`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- BLAKE3 is sourced from `@noble/hashes/blake3`. The package adds
|
||||
`@noble/hashes` as a direct dependency (catalog `crypto`) rather than going
|
||||
through `@suaveplan/crypto-hash`, which does not yet surface BLAKE3 —
|
||||
exposing it there is tracked separately and is out of scope for this change.
|
||||
- The Merkle root scheme is intentionally flat (single-level hash of sorted
|
||||
concatenated leaves) rather than a binary tree. The bundle-level root's job
|
||||
is to detect any leaf substitution, which a flat root accomplishes; binary
|
||||
Merkle proofs are not in the public surface and adding them later does not
|
||||
invalidate the flat root.
|
||||
- Domain separation: the whole-file Merkle root wraps the lone entry digest
|
||||
in a second BLAKE3 call so that a verifier never confuses a leaf digest
|
||||
with a root. The same applies to chunked manifests — `merkleRoot` is
|
||||
always `blake3(concat(...))`, never the bare concatenation.
|
||||
- `bytesEqual` is intentionally constant-time. Integrity verification is not
|
||||
a cryptographic primitive (the signature does that), but the verify path
|
||||
sits on the request hot path and constant-time comparisons keep the
|
||||
semantics consistent with the rest of the package.
|
||||
Reference in New Issue
Block a user