@aleutian/verification 0.0.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +597 -0
- package/LICENSE +201 -0
- package/README.md +485 -0
- package/dist/browser/index.d.ts +3093 -0
- package/dist/browser/index.global.js +9747 -0
- package/dist/browser/index.global.js.map +1 -0
- package/dist/browser/index.js +5358 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/cli/aleutian-verify.js +3841 -0
- package/dist/cli/aleutian-verify.js.map +1 -0
- package/dist/index.cjs +5758 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3505 -0
- package/dist/index.d.ts +3505 -0
- package/dist/index.js +5609 -0
- package/dist/index.js.map +1 -0
- package/package.json +96 -5
- package/testdata/v3_golden_capture_request.json +85 -0
- package/testdata/v3_golden_capture_signed.json +27 -0
- package/testdata/v3_golden_sar_acknowledged.json +21 -0
- package/testdata/v3_golden_sar_extended.json +40 -0
- package/testdata/v3_golden_sar_fulfilled.json +46 -0
- package/testdata/v3_golden_sar_intent.json +60 -0
- package/testdata/v3_golden_sar_rejected.json +40 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@aleutian/verification` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to a SemVer track per the
|
|
7
|
+
[SDK Release Charter](../../docs/sdk/release_charter_v1.md). While on the
|
|
8
|
+
0.x track, minor bumps may introduce breaking changes; patch bumps remain
|
|
9
|
+
backwards-compatible. Cross-language version alignment activates only after
|
|
10
|
+
a 1.0 cut (see charter §5).
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.7.0] — 2026-10-20
|
|
15
|
+
|
|
16
|
+
### Added — X-Wing specification vectors
|
|
17
|
+
|
|
18
|
+
- `test/xwingSpecVectors.test.ts` holds the Tier-2 X-Wing decapsulation to the
|
|
19
|
+
specification's official vectors (`sdk/testdata/xwing_spec_vectors.json`, now
|
|
20
|
+
part of proof's vector set; `MANIFEST.json` SHA-256 `304583d41eca4e74e2fb19cd799f499e663d60d0366bb70f4f712db903264d3c`). It re-checks the
|
|
21
|
+
fixture against the SHAKE128 checksum the specification publishes, derives each
|
|
22
|
+
`pk` from its `sk`, and decapsulates each `ct` to `ss`. Until now this code was
|
|
23
|
+
checked only against vectors this project generated. The only source change is
|
|
24
|
+
an internal export of the two functions for the test, outside the package's
|
|
25
|
+
exports.
|
|
26
|
+
|
|
27
|
+
### Added — proof v0.5.0 formats
|
|
28
|
+
|
|
29
|
+
Implemented from proof's format-spec (§2.0, §2.2, §2.5, §5.0, §5.5, §9),
|
|
30
|
+
bundle-format §16 and sink-format §9.1 alone, and held to all five new shared
|
|
31
|
+
vector files of the v0.5.0 set (`MANIFEST.json` SHA-256
|
|
32
|
+
`2eecb9f606ca3937849bea3d891eeed2fc4306d77ccf344b01ddf33ff4e0f514`). The
|
|
33
|
+
v2/v3/v6/bundle-v1 paths are unchanged and still pass their frozen vectors.
|
|
34
|
+
Dispatch is on declared versions only (`format_version`, anchor `version`,
|
|
35
|
+
bundle `format`).
|
|
36
|
+
|
|
37
|
+
- **Chain format v4**: `computeLeafHashV4`, `computeChainHashV4` (ids and
|
|
38
|
+
types hashed as stored, never normalised; length-prefixed). `verifyChainOffline`
|
|
39
|
+
verifies v4 entries, erased ones from their stored leaf hash (no chain hash
|
|
40
|
+
is taken on trust), and v2/v3 chains continued in v4 (§2.5). The walk applies
|
|
41
|
+
§5.5's break order: `format_field_misuse` for a v2/v3 entry after v4, a v4
|
|
42
|
+
member on a v2/v3 entry, or a v4 entry with run fields or a tombstone shape;
|
|
43
|
+
the v4 first-entry rule and the two entry shapes as `invalid_field`; leaf
|
|
44
|
+
hash before chain hash. New result field `erasedFound`. `parseEntriesText`
|
|
45
|
+
reads v4 entries (an erased one has no id, type or content hash) and records
|
|
46
|
+
which members were present (`presentMembers`). Held to every case of
|
|
47
|
+
`chain_v4_vectors.json` (vectors, `v3_prefix`, rejects) and the 39
|
|
48
|
+
`chain_cases` of `verify_verdict_v4_vectors.json`.
|
|
49
|
+
- **Anchors v7**: strict reading (`entry_count`, `start_seq`, `end_seq` are
|
|
50
|
+
JSON strings, returned as `bigint`, so values above 2^53 stay exact; every
|
|
51
|
+
member required), §9.3 invariants, `canonicalizeAnchorV7`,
|
|
52
|
+
`computeAnchorChainHashV7` (length-prefixed subject), and
|
|
53
|
+
`verifyAnchorLocally` with the ML-DSA context `aleutian.proof.anchor.v7`
|
|
54
|
+
(chosen from the declared version; v1–v6 keep the empty context).
|
|
55
|
+
`bindAnchorV7ToEntries(anchor, entries, { subject, previousAnchorHash })`
|
|
56
|
+
binds by sequence in §9.7's order and reports `subjectChecked`; new outcome
|
|
57
|
+
`tenant_mismatch`. `bindAnchorToEntries` and `verifyChainOffline` route a v7
|
|
58
|
+
anchor there. Held to `anchor_v7_canonical_vectors.json` (canonical bytes,
|
|
59
|
+
chain hash, signatures, rejects) and the 23 `bind_cases` of
|
|
60
|
+
`verify_verdict_v4_vectors.json`.
|
|
61
|
+
- **Record envelope `aleutian.proof.record.v2`**: `buildRecordEnvelopeV2`,
|
|
62
|
+
`verifyRecordSignatureV2` (field 0x09 leaf hash, context
|
|
63
|
+
`aleutian.proof.record.v2`), and `verifyMLDSA65PureWithContext`. Held to
|
|
64
|
+
`record_v2_vectors.json`.
|
|
65
|
+
- **Bundles `aleutian.proof.bundle.v2`** in `verifyProofBundle` and the
|
|
66
|
+
`verify-proof-bundle` CLI: entries with `salt` and `leaf_hash` (both
|
|
67
|
+
required; a v1-shaped or erased-shaped entry is `structure`), v7 checkpoints
|
|
68
|
+
(step e over entry `end_seq` by index, counts compared as integers), record.v2
|
|
69
|
+
signatures, and the v2 value limit of 17,000,000 (`PROOF_BUNDLE_LIMITS.valuesV2`).
|
|
70
|
+
Same result format and codes. Held to all 192 cases of `bundle_v2_vectors.json`,
|
|
71
|
+
and to each signature a case lists.
|
|
72
|
+
|
|
73
|
+
### Changed — proof v0.5.0 vectors, revised
|
|
74
|
+
|
|
75
|
+
Held to the revised v0.5.0 vector set (`MANIFEST.json` SHA-256
|
|
76
|
+
`62b96a47373c1cf41b21af29ac23b9760eee205b00933d26769efcee09b5c662`):
|
|
77
|
+
52 `chain_cases`, 20 `entries_text_cases` and 23 `bind_cases` in
|
|
78
|
+
`verify_verdict_v4_vectors.json`, 30 anchor v7 rejects, 22 chain v4 vectors
|
|
79
|
+
and 193 bundle v2 cases.
|
|
80
|
+
|
|
81
|
+
- The verdict vectors now pin the number of breaks a walk reports with every
|
|
82
|
+
break collected (§5.0, §5.5: at most one per entry from steps 2 to 8, after
|
|
83
|
+
a `sequence_gap`); the walk already agreed on every case.
|
|
84
|
+
- `parseEntriesText` refuses a member name that case-folds to a format member
|
|
85
|
+
without being it (`ſalt` with U+017F, `eraſed`), as it already refused
|
|
86
|
+
`CHAIN_HASH`: a case-folding reader takes either for the real member.
|
|
87
|
+
- A v7 anchor's `anchor_id` and `previous_anchor_id` must be non-empty, and
|
|
88
|
+
its `subject`, `anchor_id`, `previous_anchor_id` and `signing_key_id` valid
|
|
89
|
+
UTF-8 (no lone surrogate), in `checkAnchorWellFormed` (§9.3, §9.8). The
|
|
90
|
+
strict text reader already refused null members, invalid UTF-8 and lone
|
|
91
|
+
surrogate escapes.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- `canonicalizeAnchorForVersion` refuses a version with no canonical form
|
|
96
|
+
(5, or anything but 1–4, 6, 7) instead of canonicalizing it as v1.
|
|
97
|
+
- A v1–v6 anchor carrying `previous_anchor_hash` or a sequence range is
|
|
98
|
+
refused (format-spec §9.8), and a v2 or v3 entry carrying `salt`,
|
|
99
|
+
`leaf_hash` or `erased` is the break `format_field_misuse` (§2.2); both
|
|
100
|
+
members were ignored before.
|
|
101
|
+
|
|
102
|
+
Held to proof's SHARED vectors for chain hash v3, anchor v6 and key ids
|
|
103
|
+
(computed in Python from the format spec), instead of constants copied from Go.
|
|
104
|
+
They found two real disagreements with the format, both fixed here.
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- **Anchor canonical bytes now escape exactly as Go does** (format-spec §9.2):
|
|
109
|
+
`<` `>` `&` U+2028 U+2029 as `\u` escapes, in every anchor version. Before,
|
|
110
|
+
an anchor whose id or subject held one of them canonicalized differently from
|
|
111
|
+
the bytes that were signed, and failed verification.
|
|
112
|
+
- **Chain hash v3 refuses malformed inputs** (format-spec §2.1, §3), as proof
|
|
113
|
+
does: a `previous_hash` that is not empty or 128 lowercase hex, a
|
|
114
|
+
`content_hash` that is neither 128 lowercase hex nor a tombstone value, a
|
|
115
|
+
`global_seq` above 2^63 − 1, or a timestamp not in the `YYYY-MM-DDTHH:MM:SS.ffffffZ` form. The preimage is `|`-delimited and not
|
|
116
|
+
self-delimiting, so a malformed field could make two different entries hash
|
|
117
|
+
alike. *(Breaking only for callers that passed malformed values.)*
|
|
118
|
+
- **Proof bundle verifier, after review (`_72e`), to bundle-format revision 3.2:**
|
|
119
|
+
- **Bounded memory.** The bundle is read in one streaming pass that counts
|
|
120
|
+
JSON values (§5's new 15,000,000 limit, `too-large` at §9.1 position 3;
|
|
121
|
+
`ProofBundleLimits` gains `values`), knows each value's §4 position, stops
|
|
122
|
+
building at the first structure violation, duplicate name, wrong `format`
|
|
123
|
+
or count overrun, and decodes escaped strings without one allocation per
|
|
124
|
+
escape. 16 MiB of `{}` chains went from about 1.3 GB of heap to under
|
|
125
|
+
1 MB; 64 MiB no longer crashes Node.
|
|
126
|
+
- **Piped CLI output is complete.** The bin sets `process.exitCode` instead
|
|
127
|
+
of calling `process.exit()`, which cut `--json | jq` at 64 KiB on macOS. An
|
|
128
|
+
error escaping the command prints a fixed message and exits 5, never a
|
|
129
|
+
stack.
|
|
130
|
+
- Checkpoint step e is linear (the first malformed entry is found once).
|
|
131
|
+
- Hashing in the bundle verifier is synchronous (`@noble/hashes`).
|
|
132
|
+
- Key files must be regular files of at most 16 KiB (a directory, FIFO or
|
|
133
|
+
larger file is a usage error); the bundle file is opened once and read up
|
|
134
|
+
to one byte past the 256 MiB limit.
|
|
135
|
+
- Unreadable human output states both trust dimensions (§10).
|
|
136
|
+
- A malformed `trust` argument throws `ProofBundleVerifierError`.
|
|
137
|
+
- The bundle verifier no longer imports `keys.ts`, so browser bundles of it
|
|
138
|
+
carry no embedded key fixtures: the v6 anchor canonicalization moved to
|
|
139
|
+
`anchorCanonical.ts` (`canonicalizeAnchorV6` is still exported unchanged).
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
|
|
143
|
+
- Key ids (format-spec §9.6): `keyIdMLDSA65`, `keyIdXWing`, `mldsa65Spki`. A verifier uses them to check that the
|
|
144
|
+
key it verifies a signature with really has the key id the signature names.
|
|
145
|
+
- The commitment and `record.v1` vectors are vendored too, ready for the sink
|
|
146
|
+
bundle verifier (`_72`).
|
|
147
|
+
- **Proof sink export bundles** (`aleutian.proof.bundle.v1`, proof's
|
|
148
|
+
`bundle-format.md` revision 3.2): `verifyProofBundle(bytes, { checkpointKeys,
|
|
149
|
+
recordKeys })` returns the `aleutian.proof.bundle-result.v1` result, and the
|
|
150
|
+
CLI gains `aleutian-verify verify-proof-bundle <bundle.json>
|
|
151
|
+
[--checkpoint-key F]... [--record-key F]... [--json]` (exit 0 ok, 1 failed,
|
|
152
|
+
2 usage, 3 unreadable, 4 unauthenticated, 5 internal error). Written from the
|
|
153
|
+
spec alone, independently of proof's code, and held to all 114 cases of the
|
|
154
|
+
shared `bundle_v1_vectors.json`. A strict JSON parser enforces the bundle
|
|
155
|
+
grammar (valid UTF-8, no BOM, no lone surrogates, duplicate names refused
|
|
156
|
+
after unescaping, depth 8, numbers never rounded); `global_seq` is a BigInt.
|
|
157
|
+
Signatures are pure ML-DSA-65 with an empty context: context and HashML-DSA
|
|
158
|
+
signatures fail. Output never prints a bundle value other than a valid chain
|
|
159
|
+
id. Browser-safe.
|
|
160
|
+
- `parseProofPublicKeyFile`: trust key files (one `PUBLIC KEY` PEM block, the
|
|
161
|
+
RFC 9881 SPKI of an ML-DSA-65 key); anything else is refused.
|
|
162
|
+
- Salted commitments (format-spec §8): `computeCommitment`, `openCommitment`.
|
|
163
|
+
- The record-signature envelope (sink-format §9.1): `buildRecordEnvelopeV1`,
|
|
164
|
+
`verifyRecordSignatureV1`, `verifyMLDSA65Pure`. Held to the commitment and
|
|
165
|
+
`record.v1` vectors, including the pinned regression signature.
|
|
166
|
+
|
|
167
|
+
## [0.6.0] — 2026-09-26
|
|
168
|
+
|
|
169
|
+
_Prepared, never published._ This version was built and gated for chain format
|
|
170
|
+
v3 and anchor v6, but never uploaded. Its contents shipped first in 0.7.0, with
|
|
171
|
+
everything in that entry. This number is skipped.
|
|
172
|
+
|
|
173
|
+
First release to implement the CURRENT AleutianChain formats, and the first
|
|
174
|
+
held to shared cross-language conformance vectors.
|
|
175
|
+
|
|
176
|
+
### Added
|
|
177
|
+
|
|
178
|
+
- **Chain hash v3** (`computeChainHashV3`, `CHAIN_HASH_PREFIX_V3`), which binds
|
|
179
|
+
the chain-wide `globalSeq` rather than v2's batch-local `sequenceNum`, making
|
|
180
|
+
a chain independent of how it was grouped into append calls.
|
|
181
|
+
- **`globalSeq` accepts `bigint`.** The format's sequence is a 64-bit integer
|
|
182
|
+
and JavaScript's `number` holds 53 bits exactly: the reference
|
|
183
|
+
implementation's own test vector `9007199254740993` becomes `…992` as a
|
|
184
|
+
literal AND through `JSON.parse`. Passing a `number` above
|
|
185
|
+
`Number.MAX_SAFE_INTEGER` now THROWS rather than hashing a value that is not
|
|
186
|
+
the one on the wire — a wrong digest surfaces downstream as a chain break on
|
|
187
|
+
intact data.
|
|
188
|
+
- **Format dispatch** in `verifyChainOffline`; `formatVersion` absent or 0 means
|
|
189
|
+
v2, which keeps every pre-v3 chain verifiable.
|
|
190
|
+
- **Anchor v6** (`canonicalizeAnchorV6`), where `subject` replaces `company_id`
|
|
191
|
+
inside the signed bytes and moves from 3rd alphabetically to 8th. v5 remains
|
|
192
|
+
refused.
|
|
193
|
+
- Break types `unknown_format` and `format_field_misuse`, neither of which is
|
|
194
|
+
tamper evidence.
|
|
195
|
+
- **Conformance vector enforcement** (`test/conformanceVectors.test.ts`).
|
|
196
|
+
|
|
197
|
+
### Changed
|
|
198
|
+
|
|
199
|
+
- **BREAKING: `Anchor.companyId` is now `Anchor.subject`.** The wire key still
|
|
200
|
+
follows the anchor's version, and `parseAnchor` accepts either spelling —
|
|
201
|
+
refusing two different values under them.
|
|
202
|
+
|
|
203
|
+
### Fixed
|
|
204
|
+
|
|
205
|
+
- The Security section claimed **"Ed25519 signatures via tweetnacl"**. Anchors
|
|
206
|
+
are ML-DSA-65; tweetnacl is used for X25519 key exchange in the tier-2 unseal
|
|
207
|
+
path, not for signatures. It also said HMAC-SHA256 while the feature list nine
|
|
208
|
+
lines above said HMAC-SHA-512 — the latter is correct.
|
|
209
|
+
|
|
210
|
+
## [0.5.0] — 2026-08-20
|
|
211
|
+
|
|
212
|
+
Closes `consent_rendered_hash_06`. `hashRendered` computed unkeyed SHA-512,
|
|
213
|
+
which has never matched production server output — the server has
|
|
214
|
+
HMAC-keyed this hash since 2026-04-16 (`18_20_review_01`), closing a
|
|
215
|
+
rainbow-table oracle on PII-adjacent rendered consent bytes. This release
|
|
216
|
+
makes the SDK match.
|
|
217
|
+
|
|
218
|
+
### Changed
|
|
219
|
+
|
|
220
|
+
- **BREAKING: `hashRendered(b: Uint8Array): Promise<string>` is now
|
|
221
|
+
`hashRendered(b: Uint8Array, tenantPepper: Uint8Array): string`** —
|
|
222
|
+
synchronous, and moved to a new module (`consentRenderedHash.ts`). There
|
|
223
|
+
is no unkeyed variant to fall back to. Callers obtain `tenantPepper` via
|
|
224
|
+
the new consent-pepper reveal endpoint
|
|
225
|
+
(`POST /v1/internal/consent/rendered-pepper/reveal`,
|
|
226
|
+
`consent_rendered_hash_04`) before calling `hashRendered`.
|
|
227
|
+
- **BREAKING: `hashRendered` is no longer exported from the browser bundle**
|
|
228
|
+
(`@aleutian/verification/browser`, `dist/browser/index.js`). Its input now
|
|
229
|
+
includes a SECRET tenant pepper, which must never ship into
|
|
230
|
+
browser-executable code — same reasoning that already excludes the
|
|
231
|
+
`webhook` module from the browser build. Node-only; import from the
|
|
232
|
+
default (Node) entry point.
|
|
233
|
+
|
|
234
|
+
## [0.4.0] — 2026-06-14
|
|
235
|
+
|
|
236
|
+
Closes `sdk_anchor_binding_gap`. Previous SDK releases could not validate
|
|
237
|
+
real Aleutian-produced anchors because the anchor's `chain_hash` lives in
|
|
238
|
+
a different SHA-512 domain (`"aleutian.anchor.v2:"`) than the per-entry
|
|
239
|
+
`chain_hash` (`"aleutian.chain.v2:"`). `verifyChainOffline` was doing raw
|
|
240
|
+
byte-equality between the walked tip and `anchor.chainHash` — a comparison
|
|
241
|
+
that can never succeed for real producer output. This release ships the
|
|
242
|
+
missing binding primitive and rewrites the verification step to use it.
|
|
243
|
+
|
|
244
|
+
### Added
|
|
245
|
+
|
|
246
|
+
- **`computeAnchorChainHash(prevAnchorHash, companyId, startEntryId, endEntryId, tipChainHash)`** — async; byte-identical to the Go SDK `ComputeAnchorChainHash` and the producer at `internal/chainlinker/anchor_generator.go`. Recomputes the SHA-512 over the `"aleutian.anchor.v2:"` domain that the producer stores in `anchors.chain_hash`.
|
|
247
|
+
- **`SEED_ANCHOR_HASH`** — the genesis `previousAnchorHash` (`SHA-512("aleutian.anchor.seed.v2")`), used as the prior-anchor input when verifying the first anchor in a company's chain.
|
|
248
|
+
- **`SEED_ANCHOR_ID`** — sentinel `previousAnchorId` for genesis anchors.
|
|
249
|
+
- **`ANCHOR_CHAIN_DOMAIN_V2`** — exported domain-separator constant, for callers reimplementing the hash in non-SDK code.
|
|
250
|
+
- **`ChainVerifyOptions.anchor`** + **`ChainVerifyOptions.previousAnchorHash`** — the new supported anchor-binding API. Pass the full anchor and the prior anchor's `chainHash`; the SDK recomputes the expected anchor digest and constant-time compares against `anchor.chainHash`. Empty `previousAnchorHash` + `anchor.previousAnchorId === SEED_ANCHOR_ID` auto-fills `SEED_ANCHOR_HASH` for convenience.
|
|
251
|
+
- New CLI flag on `aleutian-verify`: `--previous-anchor-hash <hex>`. Genesis anchors can omit it; mid-chain anchors require it.
|
|
252
|
+
|
|
253
|
+
### Changed
|
|
254
|
+
|
|
255
|
+
- `Anchor.chainHash` doc-comment corrected — clarifies that this field is the anchor-of-anchors digest (output of `computeAnchorChainHash`), NOT the tip entry's `chain_hash`. Verifiers must recompute, not byte-compare.
|
|
256
|
+
- README + module doc-comments updated to the new pattern; the prior examples referenced `anchor.chainState.lastChainHash`, a field that never existed on the public `Anchor` type.
|
|
257
|
+
|
|
258
|
+
### Deprecated
|
|
259
|
+
|
|
260
|
+
- **`ChainVerifyOptions.expectedLastHash`** — the raw-equality path that could never bind real producer anchors. Preserved this release for any caller still passing it; will be removed in the next minor.
|
|
261
|
+
|
|
262
|
+
### Migration
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// Before (0.3.x — could not bind real anchors)
|
|
266
|
+
const result = await verifyChainOffline(entries, {
|
|
267
|
+
expectedLastHash: anchor.chainHash,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// After (0.4.0)
|
|
271
|
+
import { SEED_ANCHOR_HASH } from "@aleutian/verification";
|
|
272
|
+
const result = await verifyChainOffline(entries, {
|
|
273
|
+
anchor,
|
|
274
|
+
// For genesis anchors pass SEED_ANCHOR_HASH (or omit; SDK auto-fills).
|
|
275
|
+
// For mid-chain anchors pass the prior anchor's chainHash here.
|
|
276
|
+
previousAnchorHash: SEED_ANCHOR_HASH,
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Verification
|
|
281
|
+
|
|
282
|
+
Pinned by cross-language golden vector (byte-identical Go + Python + JS),
|
|
283
|
+
SDK↔producer byte-equality regression test, and end-to-end live-BQ smoke
|
|
284
|
+
against real `duncan` production data (`scripts/smoke-sdk-live.sh`).
|
|
285
|
+
|
|
286
|
+
## [0.3.1] — 2026-05-29
|
|
287
|
+
|
|
288
|
+
Patch release shipping two deferred items from
|
|
289
|
+
`dsar_chain_f_post_review_followups`: an additive options-object API
|
|
290
|
+
shape for `verifyChainEntryV3` (M1) and elimination of the redundant
|
|
291
|
+
per-field NFC re-normalization on the canonical-bytes hot path (M6).
|
|
292
|
+
No canonical-bytes change for NFC inputs (the only contract that
|
|
293
|
+
matters cross-language).
|
|
294
|
+
|
|
295
|
+
### Added
|
|
296
|
+
|
|
297
|
+
- **`verifyChainEntryV3` options-object form (M1).** The function now
|
|
298
|
+
accepts a second-shape call:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
verifyChainEntryV3(entry, { signature, contentHash, trustManifest })
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
in addition to the existing positional form. The new form is
|
|
305
|
+
preferred going forward; it removes positional-argument-order
|
|
306
|
+
bugs and is the only form supported by the 0.4.0 cut. A new
|
|
307
|
+
`VerifyChainEntryV3Options` interface is exported. The new shape
|
|
308
|
+
emits no deprecation warning.
|
|
309
|
+
|
|
310
|
+
### Deprecated
|
|
311
|
+
|
|
312
|
+
- **Positional form of `verifyChainEntryV3`.** Calling
|
|
313
|
+
`verifyChainEntryV3(entry, signature, contentHash, trustManifest)`
|
|
314
|
+
still works on the 0.3.x track but emits a **one-shot**
|
|
315
|
+
`console.warn` notice on first use per process. Migrate to the
|
|
316
|
+
options-object form. Positional form will be **removed** in 0.4.0.
|
|
317
|
+
|
|
318
|
+
### Changed
|
|
319
|
+
|
|
320
|
+
- **Double-NFC encoding eliminated (M6).** `V3Encoder` gained a
|
|
321
|
+
constructor flag `{ assumeNormalized: true }` that skips the
|
|
322
|
+
redundant `s.normalize("NFC")` call in `appendStr` on the
|
|
323
|
+
assumption that the caller has already validated. The internal
|
|
324
|
+
`chainCanonicalV3` pipeline now constructs its encoder with this
|
|
325
|
+
flag, saving one `String.normalize` call per field on the hot path.
|
|
326
|
+
`validateV3StringField` now rejects inputs that are not already in
|
|
327
|
+
Unicode NFC form to make the flag sound (defense-in-depth — every
|
|
328
|
+
v3 string field today is restricted to ASCII via upstream regex, so
|
|
329
|
+
the gate is unreachable on well-formed inputs and no real caller
|
|
330
|
+
is affected). Canonical bytes are unchanged for all NFC inputs;
|
|
331
|
+
golden vectors and cross-language pins still pass.
|
|
332
|
+
|
|
333
|
+
### References
|
|
334
|
+
|
|
335
|
+
- Ticket: `tickets/code_complete/dsar_chain_f_post_review_followups.md`
|
|
336
|
+
- Parent: `dsar_chain_f_review_pre_emptive` (closed 2026-05-29)
|
|
337
|
+
|
|
338
|
+
## [0.3.0] — 2026-05-29
|
|
339
|
+
|
|
340
|
+
This release closes the six post-V1/V2-audit JS SDK fix tickets from the
|
|
341
|
+
`dsar_js_v1v2_audit_pass` sweep. One TypeScript-typing widening is a
|
|
342
|
+
breaking change at the type level (no runtime behaviour change for
|
|
343
|
+
in-range values), so this lands as a minor bump on the 0.x track per the
|
|
344
|
+
release charter.
|
|
345
|
+
|
|
346
|
+
### Breaking (types only)
|
|
347
|
+
|
|
348
|
+
- **`Anchor` int64 fields widened to `bigint | number`.**
|
|
349
|
+
`Anchor.entryCount`, `Anchor.createdAtMs`, and `Anchor.verifiedThrough`
|
|
350
|
+
are now typed `bigint | number` so int64 values exceeding
|
|
351
|
+
`Number.MAX_SAFE_INTEGER` (2^53 − 1) can be represented losslessly.
|
|
352
|
+
Real-world millisecond timestamps and entry counts continue to fit
|
|
353
|
+
in `number` and require no caller change. Callers that explicitly
|
|
354
|
+
declared variables as `number` will see a type error and should
|
|
355
|
+
widen to `bigint | number` or narrow with a `typeof === "number"`
|
|
356
|
+
guard. `parseAnchor` accepts JSON `number` (legacy) **or** JSON
|
|
357
|
+
`string` (full int64 precision) for int64 fields. The
|
|
358
|
+
`canonicalizeAnchor` / `canonicalizeAnchorV4` encoders manually
|
|
359
|
+
assemble the canonical JSON so `bigint` values emit byte-identically
|
|
360
|
+
to Go/Python without going through the lossy `JSON.stringify(bigint)`
|
|
361
|
+
path (which throws). `canonicalizeAnchorForVersion` is now exported
|
|
362
|
+
from the package root. Ticket: `dsar_js_anchor_int64_widening`.
|
|
363
|
+
|
|
364
|
+
### Security / Privacy
|
|
365
|
+
|
|
366
|
+
- **`InvalidProcessingMode` no longer echoes caller input** into its
|
|
367
|
+
error message. The message is now a fixed string
|
|
368
|
+
`"unknown processing mode (input redacted)"` so callers cannot
|
|
369
|
+
accidentally surface untrusted bytes through logs. Enforces Privacy
|
|
370
|
+
Invariant 4 at the SDK boundary. Ticket:
|
|
371
|
+
`dsar_js_processing_mode_error_echo`.
|
|
372
|
+
- **Defensive copy on trust-manifest and embedded public keys.**
|
|
373
|
+
`TrustManifest.publicKey()` and `getPublicKey()` now return a fresh
|
|
374
|
+
`Uint8Array` per call so caller mutation can no longer corrupt the
|
|
375
|
+
cached/embedded key bytes. Ticket:
|
|
376
|
+
`dsar_js_trust_manifest_key_mutability`.
|
|
377
|
+
- **Lone-surrogate gate on canonical-encoder string inputs.**
|
|
378
|
+
`consentCanonical` and `encodeProcessingModeCanonicalFields` now
|
|
379
|
+
reject strings containing unpaired UTF-16 surrogates with a
|
|
380
|
+
redacted-byte error, preventing `TextEncoder`'s silent
|
|
381
|
+
U+FFFD-substitution from diverging the JS encoder from the Go /
|
|
382
|
+
Python producers. Well-formed surrogate pairs (emoji etc.) continue
|
|
383
|
+
to pass through. Ticket: `dsar_js_consent_lone_surrogate_gate`.
|
|
384
|
+
|
|
385
|
+
### Added
|
|
386
|
+
|
|
387
|
+
- **`redactedView` module.** Four helpers — `redactedAnchor`,
|
|
388
|
+
`redactedConsentChainEntry`, `redactedExportedEntry`,
|
|
389
|
+
`redactedOfflineBreak` — return frozen opaque markers
|
|
390
|
+
(`{ __type: "<TypeName>", __redacted: true }`) safe to pass into
|
|
391
|
+
`console.log`, `JSON.stringify`, structured loggers, or template
|
|
392
|
+
literals. The producer interfaces are plain object literals (cannot
|
|
393
|
+
own a `toJSON` override), so the WARNING comments on `Anchor`,
|
|
394
|
+
`ConsentChainEntry`, `ExportedEntry`, and `OfflineBreak` now point
|
|
395
|
+
callers at the matching helper. Ticket:
|
|
396
|
+
`dsar_js_consent_anchor_json_stringify_leak`.
|
|
397
|
+
|
|
398
|
+
### Internal
|
|
399
|
+
|
|
400
|
+
- **Consolidated constant-time comparator in `verify.ts`.** The local
|
|
401
|
+
string-XOR `constantTimeEqual` was replaced with the shared
|
|
402
|
+
`internal/constantTime.ts` byte comparator (which prefers Node's
|
|
403
|
+
C-level `crypto.timingSafeEqual` when available and falls back to a
|
|
404
|
+
non-short-circuiting pure-JS loop in browsers). A thin
|
|
405
|
+
`chainHashEqual` wrapper handles the hex-string → UTF-8 byte
|
|
406
|
+
encoding so the call sites stay ergonomic. Strictly better timing
|
|
407
|
+
profile on Node; no behavioural change on browsers. Ticket:
|
|
408
|
+
`dsar_js_verify_consolidate_constant_time`.
|
|
409
|
+
|
|
410
|
+
### Migration
|
|
411
|
+
|
|
412
|
+
If you previously declared anchor int64 fields as `number`:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
// Before
|
|
416
|
+
const count: number = anchor.entryCount;
|
|
417
|
+
|
|
418
|
+
// After (choose one)
|
|
419
|
+
const count: bigint | number = anchor.entryCount;
|
|
420
|
+
// or, if you know the value fits in Number:
|
|
421
|
+
const count = typeof anchor.entryCount === "bigint"
|
|
422
|
+
? Number(anchor.entryCount)
|
|
423
|
+
: anchor.entryCount;
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
For producers emitting int64 values that can exceed
|
|
427
|
+
`Number.MAX_SAFE_INTEGER`, ship them as JSON strings; `parseAnchor`
|
|
428
|
+
will promote them to `bigint`. For values that fit in `number`, the
|
|
429
|
+
existing JSON-number form continues to work unchanged.
|
|
430
|
+
|
|
431
|
+
## [0.2.2] — 2026-05-29
|
|
432
|
+
|
|
433
|
+
### Added
|
|
434
|
+
|
|
435
|
+
- **Browser bundle.** A dedicated browser-target build now ships
|
|
436
|
+
alongside the Node bundle:
|
|
437
|
+
- `dist/browser/index.js` (ESM) — for bundlers (Vite, webpack, esbuild, Rollup)
|
|
438
|
+
- `dist/browser/index.global.js` (IIFE) — for direct `<script>` includes,
|
|
439
|
+
exposes a `window.AleutianVerification` global
|
|
440
|
+
- `dist/browser/index.d.ts` — typings
|
|
441
|
+
- `package.json` `exports` map gains a `"browser"` conditional, so
|
|
442
|
+
bundlers automatically resolve to the browser bundle when building
|
|
443
|
+
for a browser target. A `"./browser"` sub-path is also exposed for
|
|
444
|
+
explicit opt-in.
|
|
445
|
+
- `src/internal/constantTime.ts` — shared cross-runtime constant-time
|
|
446
|
+
byte-array equality. `verifyV3.ts` and `consent.ts` both consume it;
|
|
447
|
+
the previous duplication and `consent.ts`'s top-level static
|
|
448
|
+
`import … from "crypto"` (a module-load blocker in browsers) are
|
|
449
|
+
gone.
|
|
450
|
+
- `src/anchorLoaders.ts` — Node-only `loadAnchor` /
|
|
451
|
+
`loadExportedEntries` split out of `anchor.ts` so the browser bundle
|
|
452
|
+
does not pull `await import("fs/promises")` into its reachable
|
|
453
|
+
graph. Re-exported from `src/index.ts` for back-compat; intentionally
|
|
454
|
+
not re-exported from the browser entry.
|
|
455
|
+
- `test/browserBundle.smoke.test.ts` — happy-dom-environment smoke
|
|
456
|
+
that imports the built `dist/browser/index.js`, re-encodes a
|
|
457
|
+
Go-baked golden V3 fixture, and asserts byte-identical content
|
|
458
|
+
hash. Also pins that the webhook surface is absent from the browser
|
|
459
|
+
bundle.
|
|
460
|
+
|
|
461
|
+
### Changed
|
|
462
|
+
|
|
463
|
+
- `consent.ts` `verifyConsentEntry`: `Buffer.from(s, "utf8")` →
|
|
464
|
+
`new TextEncoder().encode(s)` for cross-runtime byte construction.
|
|
465
|
+
No behavioral change in Node; unblocks browser usage.
|
|
466
|
+
|
|
467
|
+
### Notes
|
|
468
|
+
|
|
469
|
+
- The Node bundle (`dist/index.js`, `dist/index.cjs`) is **unchanged**
|
|
470
|
+
in API or behavior — webhook verification, file-system loaders, and
|
|
471
|
+
every other previously-exported symbol remain on the Node entry.
|
|
472
|
+
- Webhook verification is intentionally absent from the browser bundle
|
|
473
|
+
(server→server only; existing API is sync HMAC-SHA-512 with no
|
|
474
|
+
async-free browser equivalent). See README "Browser Support" for
|
|
475
|
+
detail.
|
|
476
|
+
- See `dsar_js_browser_target_audit` for the audit and design.
|
|
477
|
+
|
|
478
|
+
## [0.2.1] — 2026-05-29
|
|
479
|
+
|
|
480
|
+
### Changed
|
|
481
|
+
|
|
482
|
+
- **License flipped to Apache 2.0.** Resolves the prior `UNLICENSED` state
|
|
483
|
+
documented in the 0.2.0 notes. The SDK is now openly distributed under
|
|
484
|
+
Apache License 2.0 — `package.json` `license` field updated, full LICENSE
|
|
485
|
+
file added at the module root, per-file Apache 2.0 short-form headers
|
|
486
|
+
applied across all source and test files. Decision locked via
|
|
487
|
+
`sdk_license_unification` ticket Option D (per-module rule in
|
|
488
|
+
CLAUDE.md §2.2). Aligns with the Go SDK and Python SDK, which were
|
|
489
|
+
flipped to Apache 2.0 in the same change. Customers integrating the
|
|
490
|
+
prior 0.2.0 release should re-resolve from this version to pick up the
|
|
491
|
+
Apache 2.0 license file in their build artifacts; no source or
|
|
492
|
+
behavioral changes in this release.
|
|
493
|
+
|
|
494
|
+
## [0.2.0] — 2026-05-28
|
|
495
|
+
|
|
496
|
+
Added full V3 SAR (Data Subject Access Request) chain entry verification at
|
|
497
|
+
byte-level parity with the Go and Python SDK implementations.
|
|
498
|
+
|
|
499
|
+
### Added
|
|
500
|
+
|
|
501
|
+
**`canonicalV3` module** — TS port of the V3 canonical encoder
|
|
502
|
+
|
|
503
|
+
- `chainCanonicalV3(entry: ChainEntryV3): Uint8Array` — encode a typed V3
|
|
504
|
+
chain entry to the canonical TLV byte sequence per pqc_05c v3 §3 + §4.
|
|
505
|
+
- `computeV3ContentHash(canonical: Uint8Array): Promise<Uint8Array>` —
|
|
506
|
+
SHA-512(`aleutian.chain.entry.v3:` || canonical).
|
|
507
|
+
- `encodeAndHashV3(entry)` — one-call convenience pairing the above.
|
|
508
|
+
- `validateV3Entry(entry: ChainEntryV3): void` — typed V3 entry validator;
|
|
509
|
+
throws `ErrInvalidEntryV3` on any failure. Called automatically by
|
|
510
|
+
`chainCanonicalV3` before encoding.
|
|
511
|
+
- `V3Encoder` class — exported for direct unit testing of encoder mechanics
|
|
512
|
+
(NFC normalization, BE encoding, length cap) without routing through the
|
|
513
|
+
full pipeline.
|
|
514
|
+
- Constants: `DOMAIN_PREFIX_V3`, `DOMAIN_PREFIX_V3_BYTES`,
|
|
515
|
+
`MAX_V3_STRING_FIELD_BYTES = 256`, `MAX_CANONICAL_BYTES_V3 = 4096`,
|
|
516
|
+
`V3_SHA512_HEX_LEN = 128`, `EXTENDED_SANITY_BOUND_MS`.
|
|
517
|
+
|
|
518
|
+
**`verifyV3` module** — V3 signature verifier
|
|
519
|
+
|
|
520
|
+
- `verifyChainEntryV3(entry, signature, contentHash, trustManifest)` — the
|
|
521
|
+
single customer-facing verifier entry point. Resolves on success; throws
|
|
522
|
+
one of the typed `Err*V3` subclasses on failure.
|
|
523
|
+
- `TrustManifestV3.fromMap({...})` — construct an in-process trust manifest
|
|
524
|
+
with **eager** ML-DSA-65 byte-length validation + defensive key copies.
|
|
525
|
+
- `constantTimeEqual(a, b)` — pure-JS constant-time byte-array equality
|
|
526
|
+
(browser-safe; no `crypto.timingSafeEqual` available there).
|
|
527
|
+
- Constant: `CONTENT_HASH_V3_BYTES = 64`.
|
|
528
|
+
|
|
529
|
+
**Error hierarchy** — all extend `ErrInvalidEntryV3`
|
|
530
|
+
|
|
531
|
+
- `ErrInvalidEntryV3` — encoder/validator base sentinel.
|
|
532
|
+
- `ErrContentHashMismatchV3` — re-encoded hash does not match the claim.
|
|
533
|
+
- `ErrSignatureInvalidV3` — signature parses but does not verify.
|
|
534
|
+
- `ErrSignatureMalformedV3` — signature has wrong shape (length, type).
|
|
535
|
+
- `ErrUnknownSigningKeyV3` — `signingKeyId` not in the trust manifest.
|
|
536
|
+
- `ErrPublicKeyMalformedV3` — trust-manifest public key has wrong shape.
|
|
537
|
+
|
|
538
|
+
**5 SAR entry type interfaces + supporting enums**
|
|
539
|
+
|
|
540
|
+
- `SARIntentEntryV3`, `SARAcknowledgedEntryV3`, `SARFulfilledEntryV3`
|
|
541
|
+
(includes `certificateId` as the alphabetical-first body field per
|
|
542
|
+
`dsar_fulfill_b_pre_02e`), `SARRejectedEntryV3`, `SARExtendedEntryV3`.
|
|
543
|
+
- `ChainEntryV3` discriminated union over `entryType`.
|
|
544
|
+
- `EventTypeV3`, `SARTypeV3`, `SARFulfillmentKindV3`, `SARSourceChannelV3`
|
|
545
|
+
literal types.
|
|
546
|
+
- `TenantHMACDigestV3` branded type + `tenantHMACDigestFromHex` /
|
|
547
|
+
`tenantHMACDigestFromRaw` / `tenantHMACDigestZero` / `tenantHMACDigestIsZero`
|
|
548
|
+
constructors and predicates.
|
|
549
|
+
- `CONCRETE_SAR_ENTRY_TYPE_TAGS` runtime-immutable allowlist
|
|
550
|
+
(`Object.freeze` + `add`/`delete`/`clear` overridden to throw).
|
|
551
|
+
- `hasValidChainEntryTag(value)` — runtime guard against
|
|
552
|
+
structural-typing impostors. `isChainEntryV3` retained as a
|
|
553
|
+
deprecation alias.
|
|
554
|
+
|
|
555
|
+
### Changed
|
|
556
|
+
|
|
557
|
+
- **Cross-language byte-identical canonical bytes against
|
|
558
|
+
`sdk/verification-go`** — proven by a new fixture-parity test
|
|
559
|
+
(`test/goldenV3.test.ts`) that loads the Go-baked golden vectors and
|
|
560
|
+
asserts byte-for-byte equality on canonical bytes AND content hashes
|
|
561
|
+
for all 5 SAR entry types (7 vectors total).
|
|
562
|
+
- **Privacy posture hardening** (Privacy Inv 4 / chain_d H2 throughout):
|
|
563
|
+
error messages do NOT echo caller-supplied values (offending field
|
|
564
|
+
contents, post-NFC byte lengths, claimed/computed hash bytes, or
|
|
565
|
+
`signingKeyId`). Regression-guarded by dedicated tests.
|
|
566
|
+
- **Hardened cross-language constant pins** — `MAX_CANONICAL_BYTES_V3 = 4096`
|
|
567
|
+
and SHA-512 hex length pinned identically to Go and Python SDKs.
|
|
568
|
+
- **Internal v3 regex pinning** — V3 regex patterns are now string-equality
|
|
569
|
+
pinned against the producer source-of-truth shape, so silent drift on
|
|
570
|
+
either side fails CI.
|
|
571
|
+
|
|
572
|
+
### Cross-SDK status
|
|
573
|
+
|
|
574
|
+
After this release, all three Aleutian verification SDKs are at full V3 SAR
|
|
575
|
+
parity:
|
|
576
|
+
|
|
577
|
+
| Capability | Go | Python | JS (0.2.0) |
|
|
578
|
+
|---|---|---|---|
|
|
579
|
+
| Canonical encoder | ✓ | ✓ | ✓ |
|
|
580
|
+
| Validators | ✓ | ✓ | ✓ |
|
|
581
|
+
| Verifier | ✓ | ✓ | ✓ |
|
|
582
|
+
| TrustManifest | ✓ | ✓ | ✓ |
|
|
583
|
+
| Cross-language fixture parity | ✓ | ✓ | ✓ |
|
|
584
|
+
|
|
585
|
+
### Notes
|
|
586
|
+
|
|
587
|
+
- License posture (`UNLICENSED` → AGPL-3.0-or-later) remains pending the
|
|
588
|
+
cross-SDK `sdk_license_unification` decision and is intentionally NOT
|
|
589
|
+
flipped in this release.
|
|
590
|
+
- Bundled fixtures live at `testdata/` and are exercised by the in-package
|
|
591
|
+
cross-language parity test.
|
|
592
|
+
|
|
593
|
+
## [0.1.0] — Prior
|
|
594
|
+
|
|
595
|
+
V1/V2 anchor, chain, tombstone, and webhook verification primitives.
|
|
596
|
+
SHA-512-based chain hashing (post-`dsar_chain_f_pre_js_webhook_sha512`).
|
|
597
|
+
ML-DSA-65 standalone verifier for anchor signatures.
|