@blamejs/pki 0.1.26 → 0.1.28
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 +27 -0
- package/README.md +2 -0
- package/index.js +14 -0
- package/lib/asn1-der.js +3 -0
- package/lib/cbor-det.js +611 -0
- package/lib/constants.js +39 -0
- package/lib/framework-error.js +21 -0
- package/lib/merkle.js +345 -0
- package/lib/oid.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.1.28 — 2026-07-11
|
|
8
|
+
|
|
9
|
+
Merkle transparency proof verification joins the toolkit as pki.merkle.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.merkle.leafHash / nodeHash / emptyRootHash -- the RFC 6962 / RFC 9162 tree hashes: a leaf is SHA-256(0x00 || entry), an interior node is SHA-256(0x01 || left || right), the empty tree is SHA-256(""). The domain-separation prefixes are applied unconditionally.
|
|
14
|
+
- pki.merkle.verifyInclusion({ leafIndex, treeSize, leafHash, proof, rootHash }) -- verify an RFC 6962 / RFC 9162 audit proof by folding the leaf up the audit path and constant-time-comparing the reconstructed root to a trusted root. Returns true only when the proof binds the leaf to the root.
|
|
15
|
+
- pki.merkle.verifyConsistency({ oldSize, newSize, oldRoot, newRoot, proof }) -- verify an append-only consistency proof by reconstructing BOTH the old and the new root and constant-time-comparing each; the append-only guarantee lives in the old-root leg.
|
|
16
|
+
- The error taxonomy gains MerkleError (merkle/*). A node-count ceiling (C.LIMITS.MERKLE_MAX_PROOF_NODES) rejects a pathologically long proof before any hashing; the precise per-proof guard is the geometry check in each verifier.
|
|
17
|
+
- Fuzz target merkle-verify (both fold algorithms and the hash producers over adversarial coordinates, hashes, and proofs) joins the per-PR and nightly fuzz matrices with a seed corpus.
|
|
18
|
+
|
|
19
|
+
## v0.1.27 — 2026-07-10
|
|
20
|
+
|
|
21
|
+
A strict deterministic-CBOR codec joins the toolkit as pki.cbor.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- pki.cbor.decode -- the RFC 8949 core-deterministic CBOR decoder. It returns a node carrying the major type, the argument (a lossless BigInt), zero-copy content / bytes views (the raw ranges an external verifier hashes), and children (array elements, ordered map key/value pairs, or a tag's one inner item). Every non-canonical shape fails closed with a stable cbor/* code; maxBytes, maxDepth, maxItems, and a per-bignum byte cap bound the work before allocation (so a container declaring millions of tiny elements fails closed rather than exhausting memory); allowTrailing decodes the first item of a CBOR Sequence.
|
|
26
|
+
- The read.* leaf readers over a decoded node: read.uint / read.nint / read.int (uniform BigInt), read.byteString (zero-copy Buffer), read.textString (strict UTF-8), read.array, read.map (ordered key/value node pairs), read.boolean / read.nullValue / read.undefinedValue, read.float (half / single / double), and the tagged forms read.biguint (RFC 8949 tag 2 unsigned bignum, minimality and byte cap enforced), read.time (RFC 8949 tag 1 epoch time, bounded to the valid Date range), and read.oid (RFC 9090 tag 111, decoded through the shared OID-content codec so a malformed body surfaces the existing oid/* codes).
|
|
27
|
+
- The error taxonomy gains CborError (cbor/*). The decoder is profile-parameterized, so a future CTAP2 canonical profile is a data addition rather than a new code path.
|
|
28
|
+
- Fuzz target cbor-det-parse (the decode head-well-formedness and minimal-argument checks, the map ordering / uniqueness verify, the shortest-float rule, the strict-UTF-8 gate, and the size / depth / bignum caps, in both whole-buffer and CBOR-Sequence modes) joins the per-PR and nightly fuzz matrices with a seed corpus.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- pki.oid.paramsMustBeAbsent graduated from experimental to stable: its dotted-OID-to-boolean surface has been unchanged since 0.1.21 and is exercised end-to-end by the algorithm-identifier decoder every format shares.
|
|
33
|
+
|
|
7
34
|
## v0.1.26 — 2026-07-10
|
|
8
35
|
|
|
9
36
|
Test-coverage measurement and the OpenSSF Best Practices badge.
|
package/README.md
CHANGED
|
@@ -199,6 +199,7 @@ is callable today; nothing below is a stub.
|
|
|
199
199
|
| Namespace | What it does |
|
|
200
200
|
|---|---|
|
|
201
201
|
| `pki.asn1` | Strict, bounded DER codec — `decode` (zero-copy node tree), `encode`, `build.*` canonical-DER value builders, `read.*` typed readers, `TAGS`, OID-content encode/decode |
|
|
202
|
+
| `pki.cbor` | Strict, bounded RFC 8949 deterministic CBOR codec — `decode` (zero-copy node tree) + `read.*` typed leaf readers, fail-closed on every non-canonical shape (indefinite length, non-minimal argument, unsorted / duplicate map keys, non-shortest float, trailing bytes) |
|
|
202
203
|
| `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
|
|
203
204
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation (KEM encapsulation on the roadmap). Zero-dependency, OpenSSL-interoperable |
|
|
204
205
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
@@ -221,6 +222,7 @@ is callable today; nothing below is a stub.
|
|
|
221
222
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
222
223
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
|
|
223
224
|
| `pki.ct` | Parse RFC 6962 Certificate Transparency SCT lists — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage for external verification. Structure decoded, crypto surfaced raw, bounded decode, fail-closed — `parseSctList`, `reconstructSignedData` |
|
|
225
|
+
| `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
|
|
224
226
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
225
227
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError`, each carrying a stable `code` in `domain/reason` form |
|
|
226
228
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
package/index.js
CHANGED
|
@@ -30,11 +30,13 @@
|
|
|
30
30
|
var constants = require("./lib/constants");
|
|
31
31
|
var errors = require("./lib/framework-error");
|
|
32
32
|
var asn1 = require("./lib/asn1-der");
|
|
33
|
+
var cbor = require("./lib/cbor-det");
|
|
33
34
|
var oid = require("./lib/oid");
|
|
34
35
|
var webcrypto = require("./lib/webcrypto");
|
|
35
36
|
var schema = require("./lib/schema-all");
|
|
36
37
|
var path = require("./lib/path-validate");
|
|
37
38
|
var ct = require("./lib/ct");
|
|
39
|
+
var merkle = require("./lib/merkle");
|
|
38
40
|
var est = require("./lib/est");
|
|
39
41
|
var jose = require("./lib/jose");
|
|
40
42
|
var acme = require("./lib/acme");
|
|
@@ -47,6 +49,12 @@ module.exports = {
|
|
|
47
49
|
errors: errors,
|
|
48
50
|
// `asn1` is the strict DER codec (decode/encode/build/read/TAGS).
|
|
49
51
|
asn1: asn1,
|
|
52
|
+
// `cbor` is the strict, fail-closed RFC 8949 deterministic CBOR codec
|
|
53
|
+
// (decode + read.* leaf readers), sibling to `asn1`. It rejects every
|
|
54
|
+
// non-deterministic shape -- indefinite length, a non-minimal argument,
|
|
55
|
+
// unsorted / duplicate map keys, a non-shortest float, trailing bytes --
|
|
56
|
+
// before it walks a byte, and surfaces zero-copy bytes / content views.
|
|
57
|
+
cbor: cbor,
|
|
50
58
|
oid: oid,
|
|
51
59
|
// `schema` is the family: the L2 structure-schema engine (schema.engine) and
|
|
52
60
|
// the per-format parsers (schema.x509, ...) with detect-and-route schema.parse.
|
|
@@ -58,6 +66,12 @@ module.exports = {
|
|
|
58
66
|
// SCT-list extension a certificate / OCSP response carries; the signature is
|
|
59
67
|
// surfaced raw for external verification (pki.ct.reconstructSignedData).
|
|
60
68
|
ct: ct,
|
|
69
|
+
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
70
|
+
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
71
|
+
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
|
72
|
+
// verifyConsistency fold an audit / consistency proof and constant-time-
|
|
73
|
+
// compare to a checkpoint root. Pure sync hashing, fail-closed, transport-free.
|
|
74
|
+
merkle: merkle,
|
|
61
75
|
// `est` is RFC 7030 / 8951 / 9908 Enrollment over Secure Transport -- the
|
|
62
76
|
// transport-agnostic client codecs (base64 transfer, multipart splitter),
|
|
63
77
|
// certs-only + serverkeygen validators over CMS, the enroll-attribute builders,
|
package/lib/asn1-der.js
CHANGED
|
@@ -168,6 +168,9 @@ function decode(input, opts) {
|
|
|
168
168
|
var buf = _asBuffer(input, "decode");
|
|
169
169
|
var maxBytes = _capOpt(opts.maxBytes, "maxBytes", constants.LIMITS.DER_MAX_BYTES);
|
|
170
170
|
var maxDepth = _capOpt(opts.maxDepth, "maxDepth", constants.LIMITS.DER_MAX_DEPTH);
|
|
171
|
+
if (maxDepth > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
|
|
172
|
+
throw new TypeError("decode: maxDepth " + maxDepth + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
|
|
173
|
+
}
|
|
171
174
|
if (buf.length > maxBytes) {
|
|
172
175
|
throw new Asn1Error("asn1/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
|
|
173
176
|
}
|
package/lib/cbor-det.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.cbor
|
|
6
|
+
* @nav Core
|
|
7
|
+
* @title CBOR (deterministic)
|
|
8
|
+
* @order 35
|
|
9
|
+
* @slug cbor
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* A strict, fail-closed decoder for RFC 8949 Concise Binary Object
|
|
13
|
+
* Representation, restricted to the Core Deterministic Encoding profile
|
|
14
|
+
* (RFC 8949 sec. 4.2). It is the binary sibling of `pki.asn1`: an in-house
|
|
15
|
+
* codec that owns its stack, rejects every non-canonical shape before it
|
|
16
|
+
* returns a value, and bounds size and depth before it walks a byte.
|
|
17
|
+
*
|
|
18
|
+
* Anything a lenient CBOR reader would tolerate -- an indefinite-length
|
|
19
|
+
* item, a non-minimal ("preferred") integer / length / tag argument, a
|
|
20
|
+
* non-shortest or non-canonical-NaN float, out-of-order or duplicate map
|
|
21
|
+
* keys, ill-formed UTF-8, or trailing bytes -- is a permanent `CborError`
|
|
22
|
+
* here, because deterministic CBOR is a canonical encoding and a producer
|
|
23
|
+
* that violates it produced invalid bytes. There is no lenient mode.
|
|
24
|
+
*
|
|
25
|
+
* `decode` returns a navigable node tree with zero-copy `bytes` / `content`
|
|
26
|
+
* views (the raw ranges an external verifier hashes); the `read.*` leaf
|
|
27
|
+
* readers turn a node into a JS value (a `BigInt` for every integer, a
|
|
28
|
+
* `Buffer` for a byte string, a `Date` for an epoch time, a dotted string
|
|
29
|
+
* for a tagged OID). It is the primitive the CBOR-encoded PKI surfaces
|
|
30
|
+
* (C509 certificates, COSE / CWT) will compose.
|
|
31
|
+
*
|
|
32
|
+
* @card
|
|
33
|
+
* `decode` (bounded, fail-closed, deterministic-only) + the `read.*` typed
|
|
34
|
+
* leaf readers over its node tree.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var constants = require("./constants");
|
|
38
|
+
var frameworkError = require("./framework-error");
|
|
39
|
+
var asn1 = require("./asn1-der");
|
|
40
|
+
|
|
41
|
+
var CborError = frameworkError.CborError;
|
|
42
|
+
|
|
43
|
+
// Reusable little-work scratch for IEEE-754 float decode + the shortest-form
|
|
44
|
+
// round-trip checks. Single-threaded synchronous use, reset on every write.
|
|
45
|
+
var _fbuf = new ArrayBuffer(8);
|
|
46
|
+
var _fdv = new DataView(_fbuf);
|
|
47
|
+
|
|
48
|
+
// Strict UTF-8 validator (fatal); a lone continuation / truncated sequence
|
|
49
|
+
// throws instead of substituting U+FFFD -- the fail-open class the toolkit
|
|
50
|
+
// refuses, mirroring asn1-der's strict text decode.
|
|
51
|
+
var _utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
52
|
+
|
|
53
|
+
// The ECMAScript Date-valid window is +/- 8,640,000,000,000,000 ms; in seconds
|
|
54
|
+
// that is +/- 8.64e12. read.time bounds an epoch value to this window BEFORE
|
|
55
|
+
// narrowing the BigInt to a Number, so the narrowing is lossless (well under
|
|
56
|
+
// 2^53) and the millisecond result stays a safe integer.
|
|
57
|
+
var _MAX_EPOCH_SECONDS = 8640000000000n;
|
|
58
|
+
|
|
59
|
+
function _asBuffer(input, who) {
|
|
60
|
+
if (Buffer.isBuffer(input)) return input;
|
|
61
|
+
if (input instanceof Uint8Array) return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
62
|
+
throw new CborError("cbor/not-buffer", who + ": expected a Buffer / Uint8Array");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Config-time cap validation: a bad cap is an authoring bug, so it throws a
|
|
66
|
+
// TypeError (not a CborError) exactly like asn1-der's _capOpt.
|
|
67
|
+
function _capOpt(v, key, dflt) {
|
|
68
|
+
if (v === undefined) return dflt;
|
|
69
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
|
|
70
|
+
throw new TypeError("decode: " + key + " must be a non-negative integer");
|
|
71
|
+
}
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Determinism profile -> a ruleset. v1 whitelists only "deterministic"
|
|
76
|
+
// (RFC 8949 sec. 4.2 core); an unknown profile is a config-time TypeError, so
|
|
77
|
+
// there is no path to a lenient decode. A future "ctap2" (length-first map
|
|
78
|
+
// ordering) is a data row here, not a switch through the decoder.
|
|
79
|
+
function _profile(name) {
|
|
80
|
+
var p = name === undefined ? "deterministic" : name;
|
|
81
|
+
if (p === "deterministic") return { mapKeyCompare: Buffer.compare };
|
|
82
|
+
throw new TypeError("decode: unknown profile " + JSON.stringify(p) + " (only \"deterministic\" is supported)");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _assertUtf8(buf) {
|
|
86
|
+
try {
|
|
87
|
+
_utf8.decode(buf);
|
|
88
|
+
} catch (_e) {
|
|
89
|
+
throw new CborError("cbor/bad-utf8", "text string is not well-formed UTF-8");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Decode the raw IEEE-754 bits (carried in the argument) to a JS double, per
|
|
94
|
+
// the additional-info width: 25 half, 26 single, 27 double.
|
|
95
|
+
function _readFloatBits(argument, ai) {
|
|
96
|
+
if (ai === 25) { _fdv.setUint16(0, Number(argument)); return _fdv.getFloat16(0); }
|
|
97
|
+
if (ai === 26) { _fdv.setUint32(0, Number(argument)); return _fdv.getFloat32(0); }
|
|
98
|
+
_fdv.setBigUint64(0, argument); return _fdv.getFloat64(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Is d exactly representable as a half float? (round-trip through float16;
|
|
102
|
+
// Object.is keeps -0 and the NaN/Inf cases honest).
|
|
103
|
+
function _halfRoundtrips(d) {
|
|
104
|
+
_fdv.setFloat16(0, d);
|
|
105
|
+
return Object.is(_fdv.getFloat16(0), d);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Is d exactly representable as a single float?
|
|
109
|
+
function _singleRoundtrips(d) {
|
|
110
|
+
return Object.is(Math.fround(d), d);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Recursive-descent decode of one CBOR item over [start, limit): fail-closed,
|
|
114
|
+
// depth-budgeted, zero-copy. Mirrors asn1-der's _decodeTLV.
|
|
115
|
+
function _decodeItem(buf, start, limit, depth, maxD, rules, state) {
|
|
116
|
+
if (depth > maxD) throw new CborError("cbor/too-deep", "nesting exceeds depth cap " + maxD);
|
|
117
|
+
state.n += 1;
|
|
118
|
+
if (state.n > state.max) throw new CborError("cbor/too-many-items", "decoded item count exceeds cap " + state.max);
|
|
119
|
+
var p = start;
|
|
120
|
+
if (p >= limit) throw new CborError("cbor/truncated", "expected an initial byte");
|
|
121
|
+
var ib = buf[p]; p += 1;
|
|
122
|
+
var mt = ib >> 5;
|
|
123
|
+
var ai = ib & 0x1f;
|
|
124
|
+
|
|
125
|
+
// Head well-formedness, before reading the argument.
|
|
126
|
+
if (ib === 0xff) throw new CborError("cbor/unexpected-break", "stray break byte (0xff) at an item head");
|
|
127
|
+
if (ai === 28 || ai === 29 || ai === 30) throw new CborError("cbor/reserved-ai", "reserved additional-info " + ai);
|
|
128
|
+
if (ai === 31) {
|
|
129
|
+
if (mt === 2 || mt === 3 || mt === 4 || mt === 5) {
|
|
130
|
+
throw new CborError("cbor/indefinite-length", "indefinite-length items are not valid deterministic CBOR");
|
|
131
|
+
}
|
|
132
|
+
throw new CborError("cbor/reserved-ai", "additional-info 31 is undefined for major type " + mt);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The argument, minimal-checked, as a lossless BigInt.
|
|
136
|
+
var argument;
|
|
137
|
+
var contentStart;
|
|
138
|
+
if (ai <= 23) {
|
|
139
|
+
argument = BigInt(ai);
|
|
140
|
+
contentStart = p;
|
|
141
|
+
} else {
|
|
142
|
+
var nBytes = ai === 24 ? 1 : ai === 25 ? 2 : ai === 26 ? 4 : 8;
|
|
143
|
+
if (p + nBytes > limit) throw new CborError("cbor/truncated", "truncated " + nBytes + "-byte argument");
|
|
144
|
+
if (nBytes === 1) argument = BigInt(buf[p]);
|
|
145
|
+
else if (nBytes === 2) argument = BigInt(buf.readUInt16BE(p));
|
|
146
|
+
else if (nBytes === 4) argument = BigInt(buf.readUInt32BE(p));
|
|
147
|
+
else argument = buf.readBigUInt64BE(p);
|
|
148
|
+
p += nBytes;
|
|
149
|
+
// Preferred (shortest) argument -- for every major type but 7, where the
|
|
150
|
+
// "argument" is raw float bits governed by the shortest-float rule instead.
|
|
151
|
+
if (mt !== 7) {
|
|
152
|
+
var minForWidth = nBytes === 1 ? 24n : nBytes === 2 ? 256n : nBytes === 4 ? 65536n : 4294967296n;
|
|
153
|
+
if (argument < minForWidth) {
|
|
154
|
+
throw new CborError("cbor/non-minimal-argument", "argument " + argument + " is not in the shortest head form");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
contentStart = p;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var content = null;
|
|
161
|
+
var children = null;
|
|
162
|
+
var end;
|
|
163
|
+
|
|
164
|
+
if (mt === 0 || mt === 1) {
|
|
165
|
+
end = contentStart;
|
|
166
|
+
} else if (mt === 2 || mt === 3) {
|
|
167
|
+
var len = Number(argument);
|
|
168
|
+
if (contentStart + len > limit) throw new CborError("cbor/truncated", "string content overruns the buffer");
|
|
169
|
+
content = buf.subarray(contentStart, contentStart + len);
|
|
170
|
+
if (mt === 3) _assertUtf8(content);
|
|
171
|
+
end = contentStart + len;
|
|
172
|
+
} else if (mt === 4) {
|
|
173
|
+
children = [];
|
|
174
|
+
var cp = contentStart;
|
|
175
|
+
for (var i = 0n; i < argument; i++) {
|
|
176
|
+
var el = _decodeItem(buf, cp, limit, depth + 1, maxD, rules, state);
|
|
177
|
+
children.push(el.node);
|
|
178
|
+
cp = el.end;
|
|
179
|
+
}
|
|
180
|
+
end = cp;
|
|
181
|
+
} else if (mt === 5) {
|
|
182
|
+
children = [];
|
|
183
|
+
var mp = contentStart;
|
|
184
|
+
for (var j = 0n; j < argument; j++) {
|
|
185
|
+
var k = _decodeItem(buf, mp, limit, depth + 1, maxD, rules, state);
|
|
186
|
+
mp = k.end;
|
|
187
|
+
var v = _decodeItem(buf, mp, limit, depth + 1, maxD, rules, state);
|
|
188
|
+
mp = v.end;
|
|
189
|
+
if (children.length > 0) {
|
|
190
|
+
var cmp = rules.mapKeyCompare(children[children.length - 1][0].bytes, k.node.bytes);
|
|
191
|
+
if (cmp > 0) throw new CborError("cbor/unsorted-map-keys", "map keys are not in bytewise-ascending order");
|
|
192
|
+
if (cmp === 0) throw new CborError("cbor/duplicate-map-key", "duplicate map key");
|
|
193
|
+
}
|
|
194
|
+
children.push([k.node, v.node]);
|
|
195
|
+
}
|
|
196
|
+
end = mp;
|
|
197
|
+
} else if (mt === 6) {
|
|
198
|
+
var inner = _decodeItem(buf, contentStart, limit, depth + 1, maxD, rules, state);
|
|
199
|
+
children = [inner.node];
|
|
200
|
+
end = inner.end;
|
|
201
|
+
} else {
|
|
202
|
+
// mt === 7: simple value or float.
|
|
203
|
+
if (ai <= 23) {
|
|
204
|
+
end = contentStart;
|
|
205
|
+
} else if (ai === 24) {
|
|
206
|
+
if (argument < 32n) {
|
|
207
|
+
throw new CborError("cbor/reserved-simple", "simple value " + argument + " must use the single-byte immediate form");
|
|
208
|
+
}
|
|
209
|
+
end = contentStart;
|
|
210
|
+
} else {
|
|
211
|
+
var d = _readFloatBits(argument, ai);
|
|
212
|
+
if (Number.isNaN(d)) {
|
|
213
|
+
if (!(ai === 25 && argument === 0x7e00n)) {
|
|
214
|
+
throw new CborError("cbor/non-canonical-nan", "a NaN must be the canonical half float 0xf97e00");
|
|
215
|
+
}
|
|
216
|
+
} else if (ai === 26) {
|
|
217
|
+
if (_halfRoundtrips(d)) throw new CborError("cbor/non-minimal-float", "a half-representable value must use the half form");
|
|
218
|
+
} else if (ai === 27) {
|
|
219
|
+
if (_halfRoundtrips(d) || _singleRoundtrips(d)) {
|
|
220
|
+
throw new CborError("cbor/non-minimal-float", "a value representable in a narrower float must use it");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
end = contentStart;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var node = {
|
|
228
|
+
majorType: mt,
|
|
229
|
+
ai: ai,
|
|
230
|
+
argument: argument,
|
|
231
|
+
content: content,
|
|
232
|
+
children: children,
|
|
233
|
+
header: { start: start, end: contentStart },
|
|
234
|
+
contentStart: contentStart,
|
|
235
|
+
contentEnd: end,
|
|
236
|
+
length: end - contentStart,
|
|
237
|
+
bytes: buf.subarray(start, end),
|
|
238
|
+
};
|
|
239
|
+
return { node: node, end: end };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @primitive pki.cbor.decode
|
|
244
|
+
* @signature pki.cbor.decode(bytes, opts?) -> node
|
|
245
|
+
* @since 0.1.27
|
|
246
|
+
* @status experimental
|
|
247
|
+
* @spec RFC 8949 sec. 4.2 (core deterministic encoding)
|
|
248
|
+
*
|
|
249
|
+
* Decode one Deterministically Encoded CBOR item into a navigable node tree.
|
|
250
|
+
* Every non-canonical shape is refused: an indefinite length, a non-minimal
|
|
251
|
+
* argument, unsorted or duplicate map keys, a non-shortest or non-canonical-
|
|
252
|
+
* NaN float, ill-formed UTF-8, a reserved additional-info value, or (unless
|
|
253
|
+
* `allowTrailing`) leftover bytes after the top-level item. Size, depth, and
|
|
254
|
+
* total item count are bounded, so a high-fanout container fails closed rather
|
|
255
|
+
* than exhausting memory. A node carries `majorType`, the `argument`
|
|
256
|
+
* (a lossless BigInt), a zero-copy `content` / `bytes` view, and `children`
|
|
257
|
+
* (array elements, map key/value pairs, or a tag's one inner item).
|
|
258
|
+
*
|
|
259
|
+
* @opts
|
|
260
|
+
* maxBytes: number, // default: C.LIMITS.CBOR_MAX_BYTES (16 MiB)
|
|
261
|
+
* maxDepth: number, // default: C.LIMITS.CBOR_MAX_DEPTH (64)
|
|
262
|
+
* maxItems: number, // default: C.LIMITS.CBOR_MAX_ITEMS (1,000,000 total decoded items)
|
|
263
|
+
* allowTrailing: boolean, // default: false -- true returns the first item and permits bytes after it (CBOR Sequence)
|
|
264
|
+
* profile: string, // default: "deterministic" (the only value v1 accepts)
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* var node = pki.cbor.decode(Buffer.from("83010203", "hex"));
|
|
268
|
+
* node.majorType; // 4 (array)
|
|
269
|
+
* pki.cbor.read.uint(node.children[0]); // 1n
|
|
270
|
+
*/
|
|
271
|
+
function decode(input, opts) {
|
|
272
|
+
opts = opts || {};
|
|
273
|
+
var buf = _asBuffer(input, "decode");
|
|
274
|
+
var maxBytes = _capOpt(opts.maxBytes, "maxBytes", constants.LIMITS.CBOR_MAX_BYTES);
|
|
275
|
+
var maxDepth = _capOpt(opts.maxDepth, "maxDepth", constants.LIMITS.CBOR_MAX_DEPTH);
|
|
276
|
+
if (maxDepth > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
|
|
277
|
+
throw new TypeError("decode: maxDepth " + maxDepth + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
|
|
278
|
+
}
|
|
279
|
+
var maxItems = _capOpt(opts.maxItems, "maxItems", constants.LIMITS.CBOR_MAX_ITEMS);
|
|
280
|
+
var rules = _profile(opts.profile);
|
|
281
|
+
if (buf.length > maxBytes) throw new CborError("cbor/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
|
|
282
|
+
var r = _decodeItem(buf, 0, buf.length, 0, maxDepth, rules, { n: 0, max: maxItems });
|
|
283
|
+
if (!opts.allowTrailing && r.end !== buf.length) {
|
|
284
|
+
throw new CborError("cbor/trailing-bytes", (buf.length - r.end) + " trailing byte(s) after the top-level item");
|
|
285
|
+
}
|
|
286
|
+
return r.node;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function _expectMajor(node, mt, who) {
|
|
290
|
+
if (!node || node.majorType !== mt) {
|
|
291
|
+
throw new CborError("cbor/unexpected-major", who + ": expected major type " + mt);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Assert a tag node with a specific tag number, and return its one inner item.
|
|
296
|
+
function _tagInner(node, tagNum, who) {
|
|
297
|
+
if (!node || node.majorType !== 6 || node.argument !== BigInt(tagNum)) {
|
|
298
|
+
throw new CborError("cbor/unexpected-tag", who + ": expected tag " + tagNum);
|
|
299
|
+
}
|
|
300
|
+
return node.children[0];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @primitive pki.cbor.read.uint
|
|
305
|
+
* @signature pki.cbor.read.uint(node) -> 0n
|
|
306
|
+
* @since 0.1.27
|
|
307
|
+
* @status experimental
|
|
308
|
+
* @spec RFC 8949 sec. 3.1 (major type 0)
|
|
309
|
+
*
|
|
310
|
+
* The unsigned integer value of a major-type-0 node, as a BigInt (uniform
|
|
311
|
+
* for every magnitude, so the type never varies with the value). Throws
|
|
312
|
+
* `cbor/unexpected-major` on any other major type.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* pki.cbor.read.uint(node); // -> 0n
|
|
316
|
+
*/
|
|
317
|
+
function readUint(node) {
|
|
318
|
+
_expectMajor(node, 0, "read.uint");
|
|
319
|
+
return node.argument;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @primitive pki.cbor.read.nint
|
|
324
|
+
* @signature pki.cbor.read.nint(node) -> -1n
|
|
325
|
+
* @since 0.1.27
|
|
326
|
+
* @status experimental
|
|
327
|
+
* @spec RFC 8949 sec. 3.1 (major type 1)
|
|
328
|
+
*
|
|
329
|
+
* The negative integer value of a major-type-1 node, as a BigInt
|
|
330
|
+
* (value = -1 - argument). Throws `cbor/unexpected-major` otherwise.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* pki.cbor.read.nint(node); // -> -1n
|
|
334
|
+
*/
|
|
335
|
+
function readNint(node) {
|
|
336
|
+
_expectMajor(node, 1, "read.nint");
|
|
337
|
+
return -1n - node.argument;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @primitive pki.cbor.read.int
|
|
342
|
+
* @signature pki.cbor.read.int(node) -> -1n
|
|
343
|
+
* @since 0.1.27
|
|
344
|
+
* @status experimental
|
|
345
|
+
* @spec RFC 8949 sec. 3.1 (major types 0 and 1)
|
|
346
|
+
*
|
|
347
|
+
* The signed integer value of a major-type-0 or -1 node, as a BigInt. Throws
|
|
348
|
+
* `cbor/unexpected-major` on any other major type.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* pki.cbor.read.int(node); // -> -1n
|
|
352
|
+
*/
|
|
353
|
+
function readInt(node) {
|
|
354
|
+
if (node && node.majorType === 0) return node.argument;
|
|
355
|
+
if (node && node.majorType === 1) return -1n - node.argument;
|
|
356
|
+
throw new CborError("cbor/unexpected-major", "read.int: expected major type 0 or 1");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @primitive pki.cbor.read.byteString
|
|
361
|
+
* @signature pki.cbor.read.byteString(node) -> Buffer
|
|
362
|
+
* @since 0.1.27
|
|
363
|
+
* @status experimental
|
|
364
|
+
* @spec RFC 8949 sec. 3.1 (major type 2)
|
|
365
|
+
*
|
|
366
|
+
* The zero-copy `Buffer` content of a major-type-2 byte string. Throws
|
|
367
|
+
* `cbor/unexpected-major` otherwise.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* pki.cbor.read.byteString(node); // -> <Buffer 01 02 03 04>
|
|
371
|
+
*/
|
|
372
|
+
function readByteString(node) {
|
|
373
|
+
_expectMajor(node, 2, "read.byteString");
|
|
374
|
+
return node.content;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @primitive pki.cbor.read.textString
|
|
379
|
+
* @signature pki.cbor.read.textString(node) -> "text"
|
|
380
|
+
* @since 0.1.27
|
|
381
|
+
* @status experimental
|
|
382
|
+
* @spec RFC 8949 sec. 3.1 (major type 3)
|
|
383
|
+
*
|
|
384
|
+
* The string value of a major-type-3 text string (already validated as
|
|
385
|
+
* well-formed UTF-8 at decode). Throws `cbor/unexpected-major` otherwise.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* pki.cbor.read.textString(node); // -> "a"
|
|
389
|
+
*/
|
|
390
|
+
function readTextString(node) {
|
|
391
|
+
_expectMajor(node, 3, "read.textString");
|
|
392
|
+
return node.content.toString("utf8");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @primitive pki.cbor.read.array
|
|
397
|
+
* @signature pki.cbor.read.array(node) -> [node, ...]
|
|
398
|
+
* @since 0.1.27
|
|
399
|
+
* @status experimental
|
|
400
|
+
* @spec RFC 8949 sec. 3.1 (major type 4)
|
|
401
|
+
*
|
|
402
|
+
* The element nodes of a major-type-4 array. Throws `cbor/unexpected-major`
|
|
403
|
+
* otherwise.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* pki.cbor.read.array(node); // -> [node, node, node]
|
|
407
|
+
*/
|
|
408
|
+
function readArray(node) {
|
|
409
|
+
_expectMajor(node, 4, "read.array");
|
|
410
|
+
return node.children;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @primitive pki.cbor.read.map
|
|
415
|
+
* @signature pki.cbor.read.map(node) -> [[keyNode, valueNode], ...]
|
|
416
|
+
* @since 0.1.27
|
|
417
|
+
* @status experimental
|
|
418
|
+
* @spec RFC 8949 sec. 3.1 (major type 5)
|
|
419
|
+
*
|
|
420
|
+
* The ordered key/value node pairs of a major-type-5 map (ordering and
|
|
421
|
+
* uniqueness already enforced at decode). Throws `cbor/unexpected-major`
|
|
422
|
+
* otherwise.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* pki.cbor.read.map(node); // -> [[keyNode, valueNode]]
|
|
426
|
+
*/
|
|
427
|
+
function readMap(node) {
|
|
428
|
+
_expectMajor(node, 5, "read.map");
|
|
429
|
+
return node.children;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @primitive pki.cbor.read.boolean
|
|
434
|
+
* @signature pki.cbor.read.boolean(node) -> false
|
|
435
|
+
* @since 0.1.27
|
|
436
|
+
* @status experimental
|
|
437
|
+
* @spec RFC 8949 sec. 3.3 (simple values 20 / 21)
|
|
438
|
+
*
|
|
439
|
+
* The boolean value of a simple-value node (false=20, true=21). Throws
|
|
440
|
+
* `cbor/unexpected-major` on a non-simple node, `cbor/bad-simple` on any
|
|
441
|
+
* other simple value.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* pki.cbor.read.boolean(node); // -> true
|
|
445
|
+
*/
|
|
446
|
+
function readBoolean(node) {
|
|
447
|
+
_expectMajor(node, 7, "read.boolean");
|
|
448
|
+
if (node.ai === 20) return false;
|
|
449
|
+
if (node.ai === 21) return true;
|
|
450
|
+
throw new CborError("cbor/bad-simple", "read.boolean: simple value " + node.ai + " is not a boolean");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* @primitive pki.cbor.read.nullValue
|
|
455
|
+
* @signature pki.cbor.read.nullValue(node) -> null
|
|
456
|
+
* @since 0.1.27
|
|
457
|
+
* @status experimental
|
|
458
|
+
* @spec RFC 8949 sec. 3.3 (simple value 22)
|
|
459
|
+
*
|
|
460
|
+
* `null` for a simple-value-22 node. Throws `cbor/unexpected-major` on a
|
|
461
|
+
* non-simple node, `cbor/bad-simple` on any other simple value.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* pki.cbor.read.nullValue(node); // -> null
|
|
465
|
+
*/
|
|
466
|
+
function readNull(node) {
|
|
467
|
+
_expectMajor(node, 7, "read.nullValue");
|
|
468
|
+
if (node.ai !== 22) throw new CborError("cbor/bad-simple", "read.nullValue: simple value " + node.ai + " is not null");
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @primitive pki.cbor.read.undefinedValue
|
|
474
|
+
* @signature pki.cbor.read.undefinedValue(node) -> undefined
|
|
475
|
+
* @since 0.1.27
|
|
476
|
+
* @status experimental
|
|
477
|
+
* @spec RFC 8949 sec. 3.3 (simple value 23)
|
|
478
|
+
*
|
|
479
|
+
* `undefined` for a simple-value-23 node. Throws `cbor/unexpected-major` on a
|
|
480
|
+
* non-simple node, `cbor/bad-simple` on any other simple value.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* pki.cbor.read.undefinedValue(node); // -> undefined
|
|
484
|
+
*/
|
|
485
|
+
function readUndefined(node) {
|
|
486
|
+
_expectMajor(node, 7, "read.undefinedValue");
|
|
487
|
+
if (node.ai !== 23) throw new CborError("cbor/bad-simple", "read.undefinedValue: simple value " + node.ai + " is not undefined");
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @primitive pki.cbor.read.float
|
|
493
|
+
* @signature pki.cbor.read.float(node) -> 1.5
|
|
494
|
+
* @since 0.1.27
|
|
495
|
+
* @status experimental
|
|
496
|
+
* @spec RFC 8949 sec. 3.1 (major type 7 floats)
|
|
497
|
+
*
|
|
498
|
+
* The JS number (double) of a half / single / double float node (the
|
|
499
|
+
* shortest-form and canonical-NaN rules already enforced at decode). Throws
|
|
500
|
+
* `cbor/unexpected-major` on a non-major-7 node, `cbor/bad-simple` on a
|
|
501
|
+
* simple value that is not a float.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* pki.cbor.read.float(node); // -> 1.5
|
|
505
|
+
*/
|
|
506
|
+
function readFloat(node) {
|
|
507
|
+
_expectMajor(node, 7, "read.float");
|
|
508
|
+
if (node.ai !== 25 && node.ai !== 26 && node.ai !== 27) {
|
|
509
|
+
throw new CborError("cbor/bad-simple", "read.float: node is a simple value, not a float");
|
|
510
|
+
}
|
|
511
|
+
return _readFloatBits(node.argument, node.ai);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @primitive pki.cbor.read.biguint
|
|
516
|
+
* @signature pki.cbor.read.biguint(node) -> 18446744073709551616n
|
|
517
|
+
* @since 0.1.27
|
|
518
|
+
* @status experimental
|
|
519
|
+
* @spec RFC 8949 sec. 3.4.3 (tag 2 unsigned bignum)
|
|
520
|
+
*
|
|
521
|
+
* The BigInt value of a tag-2 unsigned bignum (a big-endian magnitude byte
|
|
522
|
+
* string, no sign octet). Enforces the byte cap
|
|
523
|
+
* (`cbor/biguint-too-large`), the no-leading-zero and prefer-basic-int
|
|
524
|
+
* minimality rules (`cbor/non-minimal-biguint`), and the wrapped content type
|
|
525
|
+
* (`cbor/bad-tag-content`); a wrong / absent tag throws `cbor/unexpected-tag`.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* pki.cbor.read.biguint(node); // -> 18446744073709551616n
|
|
529
|
+
*/
|
|
530
|
+
function readBiguint(node) {
|
|
531
|
+
var inner = _tagInner(node, 2, "read.biguint");
|
|
532
|
+
if (inner.majorType !== 2) throw new CborError("cbor/bad-tag-content", "read.biguint: tag 2 must wrap a byte string");
|
|
533
|
+
var c = inner.content;
|
|
534
|
+
if (c.length > constants.LIMITS.CBOR_MAX_BIGUINT_BYTES) {
|
|
535
|
+
throw new CborError("cbor/biguint-too-large", "bignum " + c.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BIGUINT_BYTES);
|
|
536
|
+
}
|
|
537
|
+
if (c.length > 0 && c[0] === 0x00) throw new CborError("cbor/non-minimal-biguint", "bignum has a leading zero byte");
|
|
538
|
+
if (c.length <= 8) throw new CborError("cbor/non-minimal-biguint", "a value that fits a basic integer must not use a bignum");
|
|
539
|
+
return BigInt("0x" + c.toString("hex"));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @primitive pki.cbor.read.time
|
|
544
|
+
* @signature pki.cbor.read.time(node) -> Date
|
|
545
|
+
* @since 0.1.27
|
|
546
|
+
* @status experimental
|
|
547
|
+
* @spec RFC 8949 sec. 3.4.2 (tag 1 epoch date/time)
|
|
548
|
+
*
|
|
549
|
+
* The `Date` of a tag-1 epoch time (seconds since 1970-01-01T00:00Z, integer).
|
|
550
|
+
* Throws `cbor/unexpected-tag` on a wrong / absent tag, `cbor/bad-tag-content`
|
|
551
|
+
* when tag 1 does not wrap an integer, `cbor/bad-time` on an out-of-range value.
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* pki.cbor.read.time(node); // -> Date 2013-03-21T20:04:00.000Z
|
|
555
|
+
*/
|
|
556
|
+
function readTime(node) {
|
|
557
|
+
var inner = _tagInner(node, 1, "read.time");
|
|
558
|
+
if (inner.majorType !== 0 && inner.majorType !== 1) {
|
|
559
|
+
throw new CborError("cbor/bad-tag-content", "read.time: tag 1 must wrap an integer");
|
|
560
|
+
}
|
|
561
|
+
var secs = inner.majorType === 0 ? inner.argument : (-1n - inner.argument);
|
|
562
|
+
if (secs < -_MAX_EPOCH_SECONDS || secs > _MAX_EPOCH_SECONDS) {
|
|
563
|
+
throw new CborError("cbor/bad-time", "epoch time out of range");
|
|
564
|
+
}
|
|
565
|
+
var ns = Number(secs);
|
|
566
|
+
var d = new Date(ns < 0 ? -constants.TIME.seconds(-ns) : constants.TIME.seconds(ns));
|
|
567
|
+
if (isNaN(d.getTime())) throw new CborError("cbor/bad-time", "epoch time out of range");
|
|
568
|
+
return d;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* @primitive pki.cbor.read.oid
|
|
573
|
+
* @signature pki.cbor.read.oid(node) -> "2.5.4.3"
|
|
574
|
+
* @since 0.1.27
|
|
575
|
+
* @status experimental
|
|
576
|
+
* @spec RFC 9090 (tag 111 CBOR OID)
|
|
577
|
+
*
|
|
578
|
+
* The dotted OID string of a tag-111 CBOR OID (a byte string carrying the BER
|
|
579
|
+
* object-identifier content octets, decoded through the shared
|
|
580
|
+
* `asn1.decodeOidContent`, so a malformed body surfaces the existing `oid/*`
|
|
581
|
+
* codes). Throws `cbor/unexpected-tag` on a wrong / absent tag,
|
|
582
|
+
* `cbor/bad-tag-content` when tag 111 does not wrap a byte string.
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* pki.cbor.read.oid(node); // -> "2.5.4.3"
|
|
586
|
+
*/
|
|
587
|
+
function readOid(node) {
|
|
588
|
+
var inner = _tagInner(node, 111, "read.oid");
|
|
589
|
+
if (inner.majorType !== 2) throw new CborError("cbor/bad-tag-content", "read.oid: tag 111 must wrap a byte string");
|
|
590
|
+
return asn1.decodeOidContent(inner.content);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
module.exports = {
|
|
594
|
+
decode: decode,
|
|
595
|
+
read: {
|
|
596
|
+
uint: readUint,
|
|
597
|
+
nint: readNint,
|
|
598
|
+
int: readInt,
|
|
599
|
+
byteString: readByteString,
|
|
600
|
+
textString: readTextString,
|
|
601
|
+
array: readArray,
|
|
602
|
+
map: readMap,
|
|
603
|
+
boolean: readBoolean,
|
|
604
|
+
nullValue: readNull,
|
|
605
|
+
undefinedValue: readUndefined,
|
|
606
|
+
float: readFloat,
|
|
607
|
+
biguint: readBiguint,
|
|
608
|
+
time: readTime,
|
|
609
|
+
oid: readOid,
|
|
610
|
+
},
|
|
611
|
+
};
|
package/lib/constants.js
CHANGED
|
@@ -132,9 +132,37 @@ var BYTES = {
|
|
|
132
132
|
var LIMITS = {
|
|
133
133
|
DER_MAX_BYTES: BYTES.mib(16),
|
|
134
134
|
DER_MAX_DEPTH: 64,
|
|
135
|
+
// Hard stack-safe ceiling on the recursion depth of the DER / CBOR
|
|
136
|
+
// decoders, independent of the (operator-tunable) maxDepth. Both decoders
|
|
137
|
+
// are recursive descent; a maxDepth raised above the engine's native call-
|
|
138
|
+
// stack limit would let deeply nested input overflow the C stack with a raw
|
|
139
|
+
// RangeError instead of the typed too-deep verdict, defeating the fail-closed
|
|
140
|
+
// contract. A maxDepth above this ceiling is refused at config time. 256 is
|
|
141
|
+
// four times the default cap and far deeper than any real PKI / CBOR nesting
|
|
142
|
+
// (a certificate is ~6 levels; the deepest bounded re-decode is 16), while
|
|
143
|
+
// staying well below the overflow threshold on any supported platform.
|
|
144
|
+
MAX_DECODE_DEPTH_CEILING: 256,
|
|
135
145
|
PEM_MAX_BYTES: BYTES.mib(16),
|
|
136
146
|
DER_MAX_INTEGER_BYTES: BYTES.kib(16),
|
|
137
147
|
OID_MAX_SUBIDENTIFIER_BYTES: 32,
|
|
148
|
+
// Deterministic-CBOR codec ceilings (RFC 8949), the DER neighbours' siblings:
|
|
149
|
+
// a whole-document cap refused before the walk, a nesting cap, and a per-value
|
|
150
|
+
// bignum ceiling the document cap can't provide. Unlike DER_MAX_INTEGER_BYTES,
|
|
151
|
+
// the bignum cap carries NO +1 sign octet -- a CBOR tag-2/3 bignum body is pure
|
|
152
|
+
// unsigned big-endian magnitude (RFC 8949 sec. 3.4.3), so 16 KiB is the exact
|
|
153
|
+
// byte ceiling (covers an RSA-131072 modulus).
|
|
154
|
+
CBOR_MAX_BYTES: BYTES.mib(16),
|
|
155
|
+
CBOR_MAX_DEPTH: 64,
|
|
156
|
+
CBOR_MAX_BIGUINT_BYTES: BYTES.kib(16),
|
|
157
|
+
// Total decoded-item ceiling. A definite-length array or map can declare a
|
|
158
|
+
// huge element count that stays under the byte cap yet allocates one node per
|
|
159
|
+
// element -- e.g. a 16 MiB input of `9a00fffffb` followed by ~16 million
|
|
160
|
+
// one-byte items would build ~16 million nodes and exhaust memory before a
|
|
161
|
+
// typed verdict. The decoder counts every item it builds and refuses past this
|
|
162
|
+
// cap, so a high-fanout bomb fails closed with cbor/too-many-items instead of
|
|
163
|
+
// OOM-ing. 1,000,000 is far above any real CBOR-encoded PKI structure (a C509
|
|
164
|
+
// certificate or COSE message is hundreds of items) while bounding the tree.
|
|
165
|
+
CBOR_MAX_ITEMS: 1000000,
|
|
138
166
|
// Certificate Transparency SCT-list bounds (RFC 6962 sec. 3.3). The outer sct_list
|
|
139
167
|
// vector carries a 2-byte length prefix, so a well-formed list body is at most
|
|
140
168
|
// 2^16-1 = 65535 bytes and the full TLS blob (prefix + body) is at most 65537.
|
|
@@ -148,6 +176,17 @@ var LIMITS = {
|
|
|
148
176
|
// 2-5 SCTs; 256 is far above policy.
|
|
149
177
|
SCT_MAX_BYTES: BYTES.kib(64) + 1,
|
|
150
178
|
SCT_MAX_COUNT: 256,
|
|
179
|
+
// Merkle-proof node-count ceiling (RFC 6962 / RFC 9162). A DoS backstop that
|
|
180
|
+
// rejects a pathologically long proof array BEFORE the O(log n) fold does any
|
|
181
|
+
// digest work. 65 is the exact structural maximum: a tree size is a uint64, so
|
|
182
|
+
// an audit (inclusion) path is at most ceil(log2(2^64)) = 64 nodes, but a
|
|
183
|
+
// consistency proof is at most ceil(log2(newSize)) + 1 = 65 nodes (the extra
|
|
184
|
+
// node is the SUBPROOF terminal for a non-power-of-two old size near 2^64). Real
|
|
185
|
+
// CT logs are 2^30-2^40 (30-40-node proofs), so 65 is generous headroom while
|
|
186
|
+
// still bounding hostile work. The PRECISE per-proof guard is the geometry
|
|
187
|
+
// check in each verify (the node count must equal what the coordinates require);
|
|
188
|
+
// this is the coarse cap. A node count, not a byte size.
|
|
189
|
+
MERKLE_MAX_PROOF_NODES: 65,
|
|
151
190
|
// Certification-path length ceiling: bounds the per-cert asymmetric verify
|
|
152
191
|
// work on an untrusted certificate bundle (a real chain is well under this;
|
|
153
192
|
// the operator may override via opts.maxPathCerts).
|
package/lib/framework-error.js
CHANGED
|
@@ -130,6 +130,15 @@ var ConstantsError = defineClass("ConstantsError");
|
|
|
130
130
|
// decoder rejects is permanently invalid.
|
|
131
131
|
var Asn1Error = defineClass("Asn1Error");
|
|
132
132
|
|
|
133
|
+
// CborError -- malformed / non-deterministic CBOR: a reserved or indefinite
|
|
134
|
+
// additional-info value, a stray break, a non-minimal ("preferred") argument,
|
|
135
|
+
// a non-shortest or non-canonical-NaN float, an out-of-order or duplicate map
|
|
136
|
+
// key, a non-minimal / oversized bignum, bad UTF-8, a wrong-typed tag body,
|
|
137
|
+
// leftover trailing bytes, or a depth / size cap exceeded. RFC 8949 core-
|
|
138
|
+
// deterministic encoding (sec. 4.2.1) is canonical, so anything the decoder
|
|
139
|
+
// rejects is permanently invalid.
|
|
140
|
+
var CborError = defineClass("CborError");
|
|
141
|
+
|
|
133
142
|
// OidError -- a malformed object-identifier: fewer than two arcs, a first
|
|
134
143
|
// arc outside 0..2, a second arc >= 40 under arcs 0/1, or a non-minimal
|
|
135
144
|
// base-128 sub-identifier.
|
|
@@ -219,6 +228,16 @@ var SmimeError = defineClass("SmimeError", { withCause: true });
|
|
|
219
228
|
// inner `asn1/*` decode error) as `.cause`.
|
|
220
229
|
var CtError = defineClass("CtError", { withCause: true });
|
|
221
230
|
|
|
231
|
+
// MerkleError -- a malformed input to the RFC 6962 / RFC 9162 Merkle-tree
|
|
232
|
+
// proof-verification core: a tree coordinate that is not a non-negative integer
|
|
233
|
+
// (or a Number above 2^53 where an exact BigInt is required), a leafIndex
|
|
234
|
+
// outside its tree, an inverted consistency window, a hash chunk that is not 32
|
|
235
|
+
// bytes, or a proof whose node count does not match the tree geometry. The final
|
|
236
|
+
// root comparison is a constant-time boolean (root matched / did not), NOT an
|
|
237
|
+
// error -- only a structurally malformed input throws. Carries an underlying
|
|
238
|
+
// leaf fault as `.cause`.
|
|
239
|
+
var MerkleError = defineClass("MerkleError", { withCause: true });
|
|
240
|
+
|
|
222
241
|
// CsrattrsError -- a byte sequence that is not a well-formed RFC 8951 sec. 3.5
|
|
223
242
|
// CsrAttrs (SEQUENCE OF AttrOrOID), or one violating an RFC 9908 semantic rule
|
|
224
243
|
// (a repeated id-ExtensionReq, an extension-request values SET that is not
|
|
@@ -255,6 +274,7 @@ module.exports = {
|
|
|
255
274
|
defineClass: defineClass,
|
|
256
275
|
ConstantsError: ConstantsError,
|
|
257
276
|
Asn1Error: Asn1Error,
|
|
277
|
+
CborError: CborError,
|
|
258
278
|
OidError: OidError,
|
|
259
279
|
PemError: PemError,
|
|
260
280
|
CertificateError: CertificateError,
|
|
@@ -271,6 +291,7 @@ module.exports = {
|
|
|
271
291
|
CmpError: CmpError,
|
|
272
292
|
PathError: PathError,
|
|
273
293
|
CtError: CtError,
|
|
294
|
+
MerkleError: MerkleError,
|
|
274
295
|
SmimeError: SmimeError,
|
|
275
296
|
CsrattrsError: CsrattrsError,
|
|
276
297
|
EstError: EstError,
|
package/lib/merkle.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.merkle
|
|
6
|
+
* @nav Transparency
|
|
7
|
+
* @title Merkle
|
|
8
|
+
* @order 220
|
|
9
|
+
* @slug merkle
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* RFC 6962 (Certificate Transparency) / RFC 9162 (CT 2.0) Merkle-tree hash
|
|
13
|
+
* and proof-verification core -- the load-bearing primitive a static-CT
|
|
14
|
+
* client, a Merkle-Tree-Certificates relying party, and a sigstore / Rekor
|
|
15
|
+
* inclusion check all compose. It is ALL strict verification over SHA-256:
|
|
16
|
+
* zero new crypto.
|
|
17
|
+
*
|
|
18
|
+
* `leafHash` / `nodeHash` / `emptyRootHash` build the tree hashes with the
|
|
19
|
+
* two domain-separation prefixes fixed by the spec -- a leaf is
|
|
20
|
+
* `SHA-256(0x00 || entry)`, an interior node is `SHA-256(0x01 || left ||
|
|
21
|
+
* right)`, the empty tree is `SHA-256("")`. Those `0x00` / `0x01` prefixes
|
|
22
|
+
* are the second-preimage defense: without them a leaf whose bytes equal a
|
|
23
|
+
* valid interior node's preimage could be smuggled in as present.
|
|
24
|
+
*
|
|
25
|
+
* `verifyInclusion` folds an audit path back to a root and constant-time-
|
|
26
|
+
* compares it to a trusted checkpoint root; `verifyConsistency` reconstructs
|
|
27
|
+
* BOTH the old and the new root from a consistency proof (the append-only
|
|
28
|
+
* guarantee lives in the old-root leg). Both are fail-closed: a malformed
|
|
29
|
+
* coordinate, an out-of-range index, an inverted window, a wrong hash length,
|
|
30
|
+
* or a proof whose node count does not match the tree geometry throws a typed
|
|
31
|
+
* `merkle/*` error; the ONLY boolean-`false` result is the final root
|
|
32
|
+
* comparison ("root matched" vs "did not"). A `false` from `verifyInclusion`
|
|
33
|
+
* means "not proven present against this root", never "validly absent" -- an
|
|
34
|
+
* inclusion proof cannot express absence. Tree coordinates are uint64, carried
|
|
35
|
+
* as `BigInt` so a large index is never `Number`-narrowed. This is NOT a DER
|
|
36
|
+
* format: like `pki.ct` it is a companion module reached explicitly, never
|
|
37
|
+
* routed by the detect-and-parse orchestrator.
|
|
38
|
+
*
|
|
39
|
+
* @card
|
|
40
|
+
* `leafHash` / `nodeHash` / `emptyRootHash` + `verifyInclusion` /
|
|
41
|
+
* `verifyConsistency` over sync SHA-256, fail-closed, transport-free.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
var nodeCrypto = require("node:crypto");
|
|
45
|
+
var constants = require("./constants");
|
|
46
|
+
var frameworkError = require("./framework-error");
|
|
47
|
+
|
|
48
|
+
var MerkleError = frameworkError.MerkleError;
|
|
49
|
+
|
|
50
|
+
var SHA256_BYTES = 32;
|
|
51
|
+
var UINT64_MAX = 18446744073709551615n; // 2n ** 64n - 1n
|
|
52
|
+
var LEAF_PREFIX = Buffer.from([0x00]);
|
|
53
|
+
var NODE_PREFIX = Buffer.from([0x01]);
|
|
54
|
+
var EMPTY = Buffer.alloc(0);
|
|
55
|
+
|
|
56
|
+
function _sha256(buf) {
|
|
57
|
+
return nodeCrypto.createHash("sha256").update(buf).digest();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _toBuffer(v, field) {
|
|
61
|
+
if (!Buffer.isBuffer(v) && !(v instanceof Uint8Array)) {
|
|
62
|
+
throw new MerkleError("merkle/bad-input", field + " must be a Buffer or Uint8Array");
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// A zero-copy view over the same bytes; this also surfaces a detached
|
|
66
|
+
// backing ArrayBuffer (a transferred / structuredClone'd view) as a typed
|
|
67
|
+
// error here rather than a raw TypeError from a later concat / digest.
|
|
68
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
throw new MerkleError("merkle/bad-input", field + " is not a usable byte view", e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _node32(v, field) {
|
|
75
|
+
var buf = _toBuffer(v, field);
|
|
76
|
+
if (buf.length !== SHA256_BYTES) {
|
|
77
|
+
throw new MerkleError("merkle/bad-hash-length", field + " must be exactly 32 bytes, got " + buf.length);
|
|
78
|
+
}
|
|
79
|
+
return buf;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// number | bigint -> a validated BigInt in [0, 2^64). A Number >= 2^53 (where
|
|
83
|
+
// precision is lost), a negative / non-integer, or a non-number-non-bigint
|
|
84
|
+
// throws -- the number-narrows-unbounded-integer discipline for uint64 tree
|
|
85
|
+
// coordinates.
|
|
86
|
+
function _coerceCoord(v, field) {
|
|
87
|
+
var b;
|
|
88
|
+
if (typeof v === "bigint") {
|
|
89
|
+
b = v;
|
|
90
|
+
} else if (typeof v === "number") {
|
|
91
|
+
if (!Number.isSafeInteger(v) || v < 0) {
|
|
92
|
+
throw new MerkleError("merkle/bad-input", field + " must be a non-negative safe integer or a BigInt (a Number >= 2^53 loses precision)");
|
|
93
|
+
}
|
|
94
|
+
b = BigInt(v);
|
|
95
|
+
} else {
|
|
96
|
+
throw new MerkleError("merkle/bad-input", field + " must be a number or a BigInt");
|
|
97
|
+
}
|
|
98
|
+
if (b < 0n || b > UINT64_MAX) {
|
|
99
|
+
throw new MerkleError("merkle/bad-input", field + " must be in [0, 2^64)");
|
|
100
|
+
}
|
|
101
|
+
return b;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _proofNodes(proof) {
|
|
105
|
+
if (!Array.isArray(proof)) throw new MerkleError("merkle/bad-proof", "proof must be an array");
|
|
106
|
+
if (proof.length > constants.LIMITS.MERKLE_MAX_PROOF_NODES) {
|
|
107
|
+
throw new MerkleError("merkle/proof-too-large", "proof has " + proof.length + " nodes, exceeds " + constants.LIMITS.MERKLE_MAX_PROOF_NODES);
|
|
108
|
+
}
|
|
109
|
+
var out = [];
|
|
110
|
+
for (var i = 0; i < proof.length; i++) out.push(_node32(proof[i], "proof[" + i + "]"));
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Constant-time equality of two already-validated equal-length 32-byte buffers.
|
|
115
|
+
function _ctEq(a, b) {
|
|
116
|
+
return a.length === b.length && nodeCrypto.timingSafeEqual(a, b);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @primitive pki.merkle.leafHash
|
|
121
|
+
* @signature pki.merkle.leafHash(entry) -> Buffer
|
|
122
|
+
* @since 0.1.28
|
|
123
|
+
* @status experimental
|
|
124
|
+
* @spec RFC 6962, RFC 9162
|
|
125
|
+
* @related pki.merkle.nodeHash, pki.merkle.verifyInclusion
|
|
126
|
+
*
|
|
127
|
+
* The Merkle leaf hash `MTH({d}) = SHA-256(0x00 || entry)`. The `0x00` prefix
|
|
128
|
+
* is the leaf-domain second-preimage separation and is applied unconditionally.
|
|
129
|
+
* Throws `merkle/bad-input` if `entry` is not a Buffer / Uint8Array.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* pki.merkle.leafHash(Buffer.from("leaf data")); // -> <Buffer 32-byte leaf hash>
|
|
133
|
+
*/
|
|
134
|
+
function leafHash(entry) {
|
|
135
|
+
var e = _toBuffer(entry, "entry");
|
|
136
|
+
return _sha256(Buffer.concat([LEAF_PREFIX, e]));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @primitive pki.merkle.nodeHash
|
|
141
|
+
* @signature pki.merkle.nodeHash(left, right) -> Buffer
|
|
142
|
+
* @since 0.1.28
|
|
143
|
+
* @status experimental
|
|
144
|
+
* @spec RFC 6962, RFC 9162
|
|
145
|
+
* @related pki.merkle.leafHash
|
|
146
|
+
*
|
|
147
|
+
* The Merkle interior-node hash `SHA-256(0x01 || left || right)`. Both operands
|
|
148
|
+
* must be 32-byte hashes; the `0x01` prefix is applied unconditionally. Throws
|
|
149
|
+
* `merkle/bad-input` on a non-buffer operand, `merkle/bad-hash-length` on an
|
|
150
|
+
* operand that is not exactly 32 bytes.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* var l = pki.merkle.leafHash(Buffer.from([0]));
|
|
154
|
+
* var r = pki.merkle.leafHash(Buffer.from([1]));
|
|
155
|
+
* pki.merkle.nodeHash(l, r); // -> <Buffer 32-byte node hash>
|
|
156
|
+
*/
|
|
157
|
+
function nodeHash(left, right) {
|
|
158
|
+
var l = _node32(left, "left");
|
|
159
|
+
var r = _node32(right, "right");
|
|
160
|
+
return _sha256(Buffer.concat([NODE_PREFIX, l, r]));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @primitive pki.merkle.emptyRootHash
|
|
165
|
+
* @signature pki.merkle.emptyRootHash() -> Buffer
|
|
166
|
+
* @since 0.1.28
|
|
167
|
+
* @status experimental
|
|
168
|
+
* @spec RFC 6962, RFC 9162
|
|
169
|
+
* @related pki.merkle.verifyConsistency
|
|
170
|
+
*
|
|
171
|
+
* The Merkle tree head of the empty tree, `MTH({}) = SHA-256("")`
|
|
172
|
+
* (`e3b0c442...b855`). A fresh Buffer each call.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* pki.merkle.emptyRootHash(); // -> <Buffer e3 b0 c4 42 ...>
|
|
176
|
+
*/
|
|
177
|
+
function emptyRootHash() {
|
|
178
|
+
return _sha256(EMPTY);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @primitive pki.merkle.verifyInclusion
|
|
183
|
+
* @signature pki.merkle.verifyInclusion(opts) -> boolean
|
|
184
|
+
* @since 0.1.28
|
|
185
|
+
* @status experimental
|
|
186
|
+
* @spec RFC 6962, RFC 9162
|
|
187
|
+
* @related pki.merkle.leafHash, pki.merkle.verifyConsistency
|
|
188
|
+
*
|
|
189
|
+
* Verify an RFC 6962 / RFC 9162 audit (inclusion) proof: fold `leafHash` up the
|
|
190
|
+
* audit path and constant-time-compare the reconstructed root to `rootHash`.
|
|
191
|
+
* Returns `true` iff the proof binds the leaf to the root; a well-formed proof
|
|
192
|
+
* that does not match returns `false` ("not proven present against this root",
|
|
193
|
+
* NEVER "validly absent"). A malformed input -- a coordinate that is not a
|
|
194
|
+
* non-negative integer (or a Number >= 2^53), `treeSize` 0, `leafIndex >=
|
|
195
|
+
* treeSize`, a non-32-byte hash, or a proof whose node count does not match the
|
|
196
|
+
* tree geometry -- throws a typed `merkle/*` error.
|
|
197
|
+
*
|
|
198
|
+
* @opts
|
|
199
|
+
* leafIndex: number | bigint, // 0-based leaf position (uint64; pass BigInt above 2^53)
|
|
200
|
+
* treeSize: number | bigint, // total leaf count of the tree the root commits to
|
|
201
|
+
* leafHash: Buffer, // 32-byte leaf hash (e.g. from pki.merkle.leafHash)
|
|
202
|
+
* proof: Buffer[], // the audit path, each node a 32-byte hash
|
|
203
|
+
* rootHash: Buffer, // 32-byte trusted checkpoint root
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* var lh = pki.merkle.leafHash(Buffer.from([0]));
|
|
207
|
+
* pki.merkle.verifyInclusion({ leafIndex: 0, treeSize: 1, leafHash: lh, proof: [], rootHash: lh }); // -> true
|
|
208
|
+
*/
|
|
209
|
+
function verifyInclusion(opts) {
|
|
210
|
+
opts = opts || {};
|
|
211
|
+
var leafIndex = _coerceCoord(opts.leafIndex, "leafIndex");
|
|
212
|
+
var treeSize = _coerceCoord(opts.treeSize, "treeSize");
|
|
213
|
+
if (treeSize === 0n) throw new MerkleError("merkle/empty-tree", "an empty tree has no leaves to include");
|
|
214
|
+
if (leafIndex >= treeSize) throw new MerkleError("merkle/index-out-of-range", "leafIndex " + leafIndex + " is not less than treeSize " + treeSize);
|
|
215
|
+
var lh = _node32(opts.leafHash, "leafHash");
|
|
216
|
+
var rootHash = _node32(opts.rootHash, "rootHash");
|
|
217
|
+
var proof = _proofNodes(opts.proof);
|
|
218
|
+
|
|
219
|
+
var fn = leafIndex;
|
|
220
|
+
var sn = treeSize - 1n;
|
|
221
|
+
var r = lh;
|
|
222
|
+
for (var i = 0; i < proof.length; i++) {
|
|
223
|
+
var p = proof[i];
|
|
224
|
+
if (sn === 0n) throw new MerkleError("merkle/bad-proof-length", "proof is longer than the tree geometry allows");
|
|
225
|
+
if ((fn & 1n) === 1n || fn === sn) {
|
|
226
|
+
r = nodeHash(p, r);
|
|
227
|
+
if ((fn & 1n) === 0n) {
|
|
228
|
+
do { fn >>= 1n; sn >>= 1n; } while ((fn & 1n) === 0n && fn !== 0n);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
r = nodeHash(r, p);
|
|
232
|
+
}
|
|
233
|
+
fn >>= 1n;
|
|
234
|
+
sn >>= 1n;
|
|
235
|
+
}
|
|
236
|
+
if (sn !== 0n) throw new MerkleError("merkle/bad-proof-length", "proof is shorter than the tree geometry requires");
|
|
237
|
+
return _ctEq(r, rootHash);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @primitive pki.merkle.verifyConsistency
|
|
242
|
+
* @signature pki.merkle.verifyConsistency(opts) -> boolean
|
|
243
|
+
* @since 0.1.28
|
|
244
|
+
* @status experimental
|
|
245
|
+
* @spec RFC 6962, RFC 9162
|
|
246
|
+
* @related pki.merkle.verifyInclusion, pki.merkle.emptyRootHash
|
|
247
|
+
*
|
|
248
|
+
* Verify an RFC 6962 / RFC 9162 consistency proof between an older tree of
|
|
249
|
+
* `oldSize` leaves (root `oldRoot`) and a newer tree of `newSize` leaves (root
|
|
250
|
+
* `newRoot`). Reconstructs BOTH roots from the proof and constant-time-compares
|
|
251
|
+
* each; returns `true` iff both match (the append-only guarantee lives in the
|
|
252
|
+
* old-root leg -- a proof that yields a valid `newRoot` but the wrong `oldRoot`
|
|
253
|
+
* is a rewritten history and returns `false`). The empty tree is a prefix of
|
|
254
|
+
* every tree, so `oldSize` 0 checks `oldRoot == emptyRootHash()` -- and, when
|
|
255
|
+
* `newSize` is also 0, `newRoot == emptyRootHash()` too (both trees empty); for
|
|
256
|
+
* `newSize > 0` an empty-old proof places no binding on `newRoot` (an empty prior
|
|
257
|
+
* tree carries no append-only guarantee -- authenticate `newRoot` separately,
|
|
258
|
+
* e.g. via its checkpoint signature). Equal non-zero sizes require an empty proof
|
|
259
|
+
* and `oldRoot == newRoot`. A malformed input --
|
|
260
|
+
* `oldSize > newSize`, a non-empty proof where
|
|
261
|
+
* the geometry requires none (or empty where it requires one), a non-32-byte
|
|
262
|
+
* hash, or a wrong node count -- throws a typed `merkle/*` error.
|
|
263
|
+
*
|
|
264
|
+
* @opts
|
|
265
|
+
* oldSize: number | bigint, // leaf count of the older tree (uint64)
|
|
266
|
+
* newSize: number | bigint, // leaf count of the newer tree (>= oldSize)
|
|
267
|
+
* oldRoot: Buffer, // 32-byte root of the older tree
|
|
268
|
+
* newRoot: Buffer, // 32-byte root of the newer tree
|
|
269
|
+
* proof: Buffer[], // the consistency proof, each node a 32-byte hash
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* var r = pki.merkle.leafHash(Buffer.from([0]));
|
|
273
|
+
* pki.merkle.verifyConsistency({ oldSize: 1, newSize: 1, oldRoot: r, newRoot: r, proof: [] }); // -> true
|
|
274
|
+
*/
|
|
275
|
+
function verifyConsistency(opts) {
|
|
276
|
+
opts = opts || {};
|
|
277
|
+
var oldSize = _coerceCoord(opts.oldSize, "oldSize");
|
|
278
|
+
var newSize = _coerceCoord(opts.newSize, "newSize");
|
|
279
|
+
var oldRoot = _node32(opts.oldRoot, "oldRoot");
|
|
280
|
+
var newRoot = _node32(opts.newRoot, "newRoot");
|
|
281
|
+
var proof = _proofNodes(opts.proof);
|
|
282
|
+
|
|
283
|
+
if (oldSize > newSize) throw new MerkleError("merkle/old-size-exceeds-new", "oldSize " + oldSize + " exceeds newSize " + newSize);
|
|
284
|
+
if (oldSize === 0n) {
|
|
285
|
+
if (proof.length !== 0) throw new MerkleError("merkle/bad-proof-length", "an empty older tree admits only the empty consistency proof");
|
|
286
|
+
var er = emptyRootHash();
|
|
287
|
+
var oldEmpty = _ctEq(oldRoot, er);
|
|
288
|
+
// The empty tree is a prefix of every tree, so its own root must be the
|
|
289
|
+
// empty root. When the NEW tree is also empty, its root must be the empty
|
|
290
|
+
// root too (a bogus newRoot must not pass); when newSize > 0 an empty-old
|
|
291
|
+
// proof places no binding on newRoot.
|
|
292
|
+
if (newSize === 0n) {
|
|
293
|
+
var newEmpty = _ctEq(newRoot, er);
|
|
294
|
+
return oldEmpty && newEmpty;
|
|
295
|
+
}
|
|
296
|
+
return oldEmpty;
|
|
297
|
+
}
|
|
298
|
+
if (oldSize === newSize) {
|
|
299
|
+
if (proof.length !== 0) throw new MerkleError("merkle/sizes-equal-nonempty-proof", "equal tree sizes require an empty consistency proof");
|
|
300
|
+
return _ctEq(oldRoot, newRoot);
|
|
301
|
+
}
|
|
302
|
+
if (proof.length === 0) throw new MerkleError("merkle/empty-consistency-proof", "a non-trivial consistency proof must not be empty");
|
|
303
|
+
|
|
304
|
+
var path = proof;
|
|
305
|
+
if ((oldSize & (oldSize - 1n)) === 0n) {
|
|
306
|
+
// oldSize is an exact power of two: the old root is the first proof node
|
|
307
|
+
// (it is not otherwise carried in the proof).
|
|
308
|
+
path = [oldRoot].concat(proof);
|
|
309
|
+
}
|
|
310
|
+
var fn = oldSize - 1n;
|
|
311
|
+
var sn = newSize - 1n;
|
|
312
|
+
while ((fn & 1n) === 1n) { fn >>= 1n; sn >>= 1n; }
|
|
313
|
+
var fr = path[0];
|
|
314
|
+
var sr = path[0];
|
|
315
|
+
for (var i = 1; i < path.length; i++) {
|
|
316
|
+
var c = path[i];
|
|
317
|
+
if (sn === 0n) throw new MerkleError("merkle/bad-proof-length", "consistency proof is longer than the geometry allows");
|
|
318
|
+
if ((fn & 1n) === 1n || fn === sn) {
|
|
319
|
+
fr = nodeHash(c, fr);
|
|
320
|
+
sr = nodeHash(c, sr);
|
|
321
|
+
if ((fn & 1n) === 0n) {
|
|
322
|
+
do { fn >>= 1n; sn >>= 1n; } while ((fn & 1n) === 0n && fn !== 0n);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
sr = nodeHash(sr, c);
|
|
326
|
+
}
|
|
327
|
+
fn >>= 1n;
|
|
328
|
+
sn >>= 1n;
|
|
329
|
+
}
|
|
330
|
+
if (sn !== 0n) throw new MerkleError("merkle/bad-proof-length", "consistency proof is shorter than the geometry requires");
|
|
331
|
+
// Evaluate BOTH constant-time compares unconditionally before combining, so a
|
|
332
|
+
// `&&` never short-circuits the second `timingSafeEqual` (which would leak,
|
|
333
|
+
// via timing, whether the old-root leg matched).
|
|
334
|
+
var okOld = _ctEq(fr, oldRoot);
|
|
335
|
+
var okNew = _ctEq(sr, newRoot);
|
|
336
|
+
return okOld && okNew;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
leafHash: leafHash,
|
|
341
|
+
nodeHash: nodeHash,
|
|
342
|
+
emptyRootHash: emptyRootHash,
|
|
343
|
+
verifyInclusion: verifyInclusion,
|
|
344
|
+
verifyConsistency: verifyConsistency,
|
|
345
|
+
};
|
package/lib/oid.js
CHANGED
|
@@ -500,7 +500,7 @@ var _PARAMS_ABSENT = new Set();
|
|
|
500
500
|
* @primitive pki.oid.paramsMustBeAbsent
|
|
501
501
|
* @signature pki.oid.paramsMustBeAbsent(dotted) -> boolean
|
|
502
502
|
* @since 0.1.21
|
|
503
|
-
* @status
|
|
503
|
+
* @status stable
|
|
504
504
|
* @spec RFC 9909, RFC 9814, RFC 9881, RFC 8410
|
|
505
505
|
* @related pki.oid.name, pki.oid.byName
|
|
506
506
|
*
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e10c86e3-914c-4324-abeb-585344830973",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-11T04:47:27.941Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.28",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.28",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.28",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.28",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|