first commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# `signature`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`signature.ts` is the Ed25519 signature-verification gate run by the
|
||||
loader before any packed bytes are handed to `Unpacker.unpack`. It
|
||||
exists because the upstream audit (`enterprise/packer-browser.md` §3.5)
|
||||
explicitly required archive-signature verification — without it, an
|
||||
attacker who can substitute the bytes at the packed URL can ship
|
||||
arbitrary content to the page. By verifying the manifest signature
|
||||
under a caller-supplied public key first, the loader gives consumers a
|
||||
hard cryptographic anchor before any decompression / decryption runs.
|
||||
|
||||
## Features
|
||||
|
||||
- Thin wrapper over `@suaveplan/crypto`'s `ed25519.verify` — single
|
||||
authoritative implementation, no parallel curve code.
|
||||
- Boundary defence: a signature whose length is not 64 bytes throws
|
||||
`PackerBrowserError(ARCHIVE_SIGNATURE_INVALID)` rather than silently
|
||||
returning `false`. This catches wire corruption / header mis-parse
|
||||
before it pollutes downstream logs.
|
||||
- Returns a boolean for cryptographic verify-or-not — never throws on
|
||||
a non-verifying-but-well-formed signature (that's a domain decision
|
||||
the caller makes; the loader throws on the bool side).
|
||||
- Optional `messageResolver` for i18n on the boundary throw.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { verifyArchiveSignature } from "@suaveplan/packer-browser";
|
||||
|
||||
const ok = verifyArchiveSignature(manifestBytes, signature, publicKey);
|
||||
if (!ok) throw new Error("signature did not verify");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto";
|
||||
import { verifyArchiveSignature } from "@suaveplan/packer-browser";
|
||||
|
||||
const { publicKey, secretKey } = ed25519.generateKeypair();
|
||||
const message = new TextEncoder().encode("manifest:v1");
|
||||
const sig = ed25519.sign(secretKey, message);
|
||||
|
||||
const valid = verifyArchiveSignature(message, sig, publicKey);
|
||||
// valid === true
|
||||
|
||||
const tampered = new Uint8Array(sig);
|
||||
tampered[0] ^= 1;
|
||||
const invalid = verifyArchiveSignature(message, tampered, publicKey);
|
||||
// invalid === false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyArchiveSignature(manifestBytes, signature, publicKey, opts?)`
|
||||
— returns `boolean`.
|
||||
- Throws `PackerBrowserError(ARCHIVE_SIGNATURE_INVALID)` when
|
||||
`signature.length !== 64`.
|
||||
- Forwards `publicKey` to `ed25519.verify`, which throws if the key
|
||||
length is not 32 bytes.
|
||||
- `VerifyArchiveSignatureOptions`:
|
||||
- `messageResolver?: MessageResolver` — resolver for the boundary
|
||||
error message.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Signature material is conventionally hex-encoded in the
|
||||
`X-Archive-Signature` response header; the loader decodes it before
|
||||
calling this function. The public key is **not** sourced from the wire
|
||||
— the loader requires the caller to pass `verifyKey` on
|
||||
`PackedResourceLoadOptions`, ensuring an out-of-band trust anchor.
|
||||
Cluster B's encrypted-archive keyring adds the per-key public-key
|
||||
selection step on top of this primitive; Cluster A simply takes one
|
||||
public key per `load(...)` call.
|
||||
Reference in New Issue
Block a user