@blamejs/pki 0.2.0 → 0.2.2

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 CHANGED
@@ -4,6 +4,28 @@ 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.2.2 — 2026-07-12
8
+
9
+ Hybrid Public Key Encryption (RFC 9180) joins the toolkit as pki.hpke.
10
+
11
+ ### Added
12
+
13
+ - pki.hpke.setupS(suiteIds, recipientPublicKey, opts) / pki.hpke.setupR(suiteIds, enc, recipientPrivateKey, opts) establish a sender / recipient HPKE context (RFC 9180 sec. 5.1); the returned context exposes seal(aad, pt) / open(aad, ct) with the sequence-counter nonce and a message-limit guard, and export(exporterContext, L) for the secret-export interface. pki.hpke.seal / pki.hpke.open are the single-shot wrappers (sec. 6). pki.hpke.suites carries the RFC 9180 sec. 7 KEM / KDF / AEAD / MODE code points. Keys are node KeyObjects or serialized bytes; the offered suites are DHKEM P-256, P-521, X25519, and X448, HKDF-SHA256 / HKDF-SHA512, the three AEADs plus export-only, and all four modes. RFC 9180.
14
+ - The error taxonomy gains HpkeError (hpke/*): a malformed, low-order, or otherwise invalid encapsulated or KEM key (hpke/bad-key, so a Diffie-Hellman that fails during derivation surfaces as a typed error, never a raw fault), an unknown or unsupported ciphersuite code point (hpke/unknown-suite, never a silent default), an unsupported mode (hpke/unknown-mode, so an out-of-registry mode is rejected rather than key-scheduled), an authenticated mode invoked without the sender's key (hpke/auth-key-required), inconsistent PSK inputs (hpke/inconsistent-psk), an AEAD tag that does not verify (hpke/open-failed, returning no plaintext), a sequence-number overflow (hpke/message-limit, before any nonce reuse), a seal/open against an export-only suite (hpke/export-only), and a wrong-direction context call (hpke/wrong-role, so a recipient context cannot seal nor a sender context open -- they share a key and base nonce).
15
+
16
+ ## v0.2.1 — 2026-07-11
17
+
18
+ Stateful hash-based signature verification (HSS/LMS) joins the toolkit as pki.shbs.
19
+
20
+ ### Added
21
+
22
+ - pki.shbs.verify(publicKey, message, signature) verifies an HSS (Hierarchical Signature System) signature -- the wire form RFC 9802 (X.509) and RFC 9708 (CMS) carry for id-alg-hss-lms-hashsig -- returning true only if every level of the hierarchy verifies. pki.shbs.verifyLms(publicKey, message, signature) verifies a single-tree LMS signature (the component HSS composes, and a standalone algorithm). Both take the raw octet blobs the parsers surface (a certificate's subjectPublicKeyInfo.publicKey.bytes, tbsBytes, and signatureValue.bytes); a malformed blob -- bad length, an unknown or unapproved typecode, truncation, a typecode the public key does not commit to -- throws a typed ShbsError, and a well-formed but wrong signature returns false. RFC 8554 / RFC 9802 / RFC 9708 / NIST SP 800-208.
23
+ - The OID registry gains id-alg-hss-lms-hashsig (1.2.840.113549.1.9.16.3.17), id-alg-xmss-hashsig, and id-alg-xmssmt-hashsig, all with parameters MUST be absent (RFC 9802 sec. 4) -- a stateful-hash-signature AlgorithmIdentifier carrying any parameters now fails closed at the shared algorithm-identifier gate, inherited by every format the toolkit parses. The error taxonomy gains ShbsError (shbs/*).
24
+
25
+ ### Changed
26
+
27
+ - XMSS / XMSS^MT verification and automatic HSS/LMS verification inside pki.path.validate are not in this release -- see the roadmap. The former awaits an authoritative interoperability test vector (RFC 8391 ships none and NIST ACVP does not yet cover XMSS); the latter awaits a real HSS-signed certificate to prove the certification-path wiring end to end. Operators verify today by handing the raw certificate / CMS blobs to pki.shbs.verify directly.
28
+
7
29
  ## v0.2.0 — 2026-07-11
8
30
 
9
31
  Trust-store ingestion, sharded-CRL revocation, and a hardened input-guard layer.
package/README.md CHANGED
@@ -224,6 +224,8 @@ is callable today; nothing below is a stub.
224
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
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` |
226
226
  | `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
227
+ | `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
228
+ | `pki.hpke` | Hybrid Public Key Encryption (RFC 9180) — the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS`/`setupR` establish a sender/recipient context (KEM encapsulation + HKDF key schedule); the context's `seal`/`open` AEAD-encrypt with a sequence-counter nonce and `export` derives further secrets; `seal`/`open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) × HKDF-SHA256/SHA512 × AES-GCM/ChaCha20Poly1305/export-only × all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM / X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
227
229
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
228
230
  | `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 |
229
231
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
package/index.js CHANGED
@@ -37,6 +37,8 @@ var schema = require("./lib/schema-all");
37
37
  var path = require("./lib/path-validate");
38
38
  var ct = require("./lib/ct");
39
39
  var merkle = require("./lib/merkle");
40
+ var shbs = require("./lib/shbs");
41
+ var hpke = require("./lib/hpke");
40
42
  var est = require("./lib/est");
41
43
  var jose = require("./lib/jose");
42
44
  var acme = require("./lib/acme");
@@ -73,6 +75,15 @@ module.exports = {
73
75
  // verifyConsistency fold an audit / consistency proof and constant-time-
74
76
  // compare to a checkpoint root. Pure sync hashing, fail-closed, transport-free.
75
77
  merkle: merkle,
78
+ // `shbs` verifies stateful hash-based signatures -- HSS/LMS (RFC 8554),
79
+ // carried by RFC 9802 (X.509) and RFC 9708 (CMS), profiled by NIST SP 800-208.
80
+ // VERIFY ONLY by design: stateful signing requires atomic one-time-key index
81
+ // state that belongs in an HSM, so this module never mints a signature.
82
+ shbs: shbs,
83
+ // `hpke` is RFC 9180 Hybrid Public Key Encryption -- the KEM + HKDF key
84
+ // schedule + AEAD context construction behind TLS ECH / MLS / OHTTP. Pure
85
+ // composition over node:crypto; the classical DHKEM suites, all four modes.
86
+ hpke: hpke,
76
87
  // `est` is RFC 7030 / 8951 / 9908 Enrollment over Secure Transport -- the
77
88
  // transport-agnostic client codecs (base64 transfer, multipart splitter),
78
89
  // certs-only + serverkeygen validators over CMS, the enroll-attribute builders,
package/lib/constants.js CHANGED
@@ -237,6 +237,10 @@ var LIMITS = {
237
237
  // relayed response with a huge certs list would drive unbounded pre-auth
238
238
  // signature verifies. 32 is generous headroom over a real chain.
239
239
  OCSP_MAX_CERTS: 32,
240
+ // HSS hierarchy depth (RFC 8554 sec. 6.1: L is 1..8). The HSS verifier caps
241
+ // its per-level parse loop at this from the registry, never at the blob's own
242
+ // Nspk, so a hostile signature cannot drive an unbounded level walk.
243
+ HSS_MAX_LEVELS: 8,
240
244
  // Trust-store ingestion ceilings (Mozilla/NSS certdata.txt + CCADB CSV).
241
245
  // Every cap is refused BEFORE the offending allocation with a typed trust/*
242
246
  // error -- never a silent truncate. A live certdata.txt is a few MiB and a
@@ -281,6 +281,8 @@ var AcmeError = defineClass("AcmeError", { withCause: true });
281
281
  // not a trusted delegator for. Carries the underlying leaf fault (an x509/* or
282
282
  // asn1/* error) as `.cause`.
283
283
  var TrustError = defineClass("TrustError", { withCause: true });
284
+ var ShbsError = defineClass("ShbsError", { withCause: true });
285
+ var HpkeError = defineClass("HpkeError", { withCause: true });
284
286
 
285
287
  module.exports = {
286
288
  PkiError: PkiError,
@@ -304,6 +306,8 @@ module.exports = {
304
306
  CmpError: CmpError,
305
307
  PathError: PathError,
306
308
  CtError: CtError,
309
+ ShbsError: ShbsError,
310
+ HpkeError: HpkeError,
307
311
  MerkleError: MerkleError,
308
312
  SmimeError: SmimeError,
309
313
  CsrattrsError: CsrattrsError,
package/lib/hpke.js ADDED
@@ -0,0 +1,487 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.hpke
6
+ * @nav Protocols
7
+ * @title HPKE
8
+ * @intro Hybrid Public Key Encryption (RFC 9180) -- the standard construction
9
+ * behind TLS Encrypted Client Hello, MLS, and OHTTP that turns a recipient KEM
10
+ * public key into an encapsulated key plus an AEAD-encrypting context. This is
11
+ * the RFC 9180 base construction: the DHKEM suites P-256, P-521, X25519, and
12
+ * X448, HKDF-SHA256 / HKDF-SHA512, the three AEADs plus export-only, and all
13
+ * four modes (base / psk / auth / auth-psk), each proven against the RFC 9180
14
+ * Appendix A known-answer vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered
15
+ * but Appendix A ships no test vector for them, so they are omitted (a request
16
+ * fails closed) until an authoritative KAT is available. Pure composition over
17
+ * node:crypto -- no ASN.1, no schema engine. Post-quantum KEMs (ML-KEM, X-Wing)
18
+ * are a data-row extension the registry is shaped to admit once their
19
+ * specifications stabilize.
20
+ * The RFC 9180 sec. 7 registry code points live in pki.hpke.suites
21
+ * (KEM / KDF / AEAD / MODE), passed to the setup functions to select a suite.
22
+ * @spec RFC 9180
23
+ * @card Encrypt to a KEM public key (RFC 9180; the ECH / MLS / OHTTP primitive).
24
+ */
25
+
26
+ var nodeCrypto = require("crypto");
27
+ var frameworkError = require("./framework-error");
28
+ var guard = require("./guard-all");
29
+
30
+ var HpkeError = frameworkError.HpkeError;
31
+ function _err(code, message, cause) { return new HpkeError(code, message, cause); }
32
+
33
+ // ---- I2OSP + byte helpers ----------------------------------------------------
34
+
35
+ function i2osp(n, len) {
36
+ var b = Buffer.alloc(len);
37
+ var v = BigInt(n);
38
+ for (var i = len - 1; i >= 0; i--) { b[i] = Number(v & 0xffn); v >>= 8n; }
39
+ return b;
40
+ }
41
+ function concat(arr) { return Buffer.concat(arr); }
42
+ function xor(a, b) { var o = Buffer.alloc(a.length); for (var i = 0; i < a.length; i++) o[i] = a[i] ^ b[i]; return o; }
43
+
44
+ var HPKE_V1 = Buffer.from("HPKE-v1", "ascii");
45
+ function L(s) { return Buffer.from(s, "ascii"); }
46
+
47
+ // ---- registries (RFC 9180 sec. 7, Tables 2/3/5 -- data, not switch) ----------
48
+
49
+ // KDF: id -> { hash (node name), Nh }.
50
+ // HKDF-SHA384 (0x0002) and DHKEM(P-384) (0x0011) are registered by RFC 9180 sec.
51
+ // 7 but Appendix A / the cited [TestVectors] file ship no known-answer vector for
52
+ // them, so they are omitted here until an authoritative KAT exists -- a request
53
+ // for either fails closed as hpke/unknown-suite rather than running crypto no
54
+ // test vector proves. They admit as a one-row addition the moment a KAT lands.
55
+ var KDFS = {
56
+ 0x0001: { hash: "sha256", Nh: 32 },
57
+ 0x0003: { hash: "sha512", Nh: 64 },
58
+ };
59
+ // AEAD: id -> { cipher (node name), Nk, Nn, Nt } or exportOnly.
60
+ var AEADS = {
61
+ 0x0001: { cipher: "aes-128-gcm", Nk: 16, Nn: 12, Nt: 16 },
62
+ 0x0002: { cipher: "aes-256-gcm", Nk: 32, Nn: 12, Nt: 16 },
63
+ 0x0003: { cipher: "chacha20-poly1305", Nk: 32, Nn: 12, Nt: 16 },
64
+ 0xFFFF: { exportOnly: true, Nn: 12 },
65
+ };
66
+ // KEM: id -> DHKEM group descriptor. `group` is the node keygen/import family;
67
+ // `kdf` is the KEM-internal KDF (its ExtractAndExpand); Nsecret = that KDF's Nh.
68
+ var KEMS = {
69
+ 0x0010: { kind: "ec", curve: "P-256", nodeCurve: "prime256v1", kdf: 0x0001, Nsecret: 32, Npub: 65 },
70
+ 0x0012: { kind: "ec", curve: "P-521", nodeCurve: "secp521r1", kdf: 0x0003, Nsecret: 64, Npub: 133 },
71
+ 0x0020: { kind: "okp", curve: "X25519", oid: "2b656e", kdf: 0x0001, Nsecret: 32, Npub: 32 },
72
+ 0x0021: { kind: "okp", curve: "X448", oid: "2b656f", kdf: 0x0003, Nsecret: 64, Npub: 56 },
73
+ };
74
+
75
+ var MODE_BASE = 0x00, MODE_PSK = 0x01, MODE_AUTH = 0x02, MODE_AUTH_PSK = 0x03;
76
+
77
+ // Format a code point for an error message without assuming it is a number: a
78
+ // missing suiteIds member arrives as undefined, and undefined.toString(16) would
79
+ // crash the very error path meant to report it.
80
+ function _idStr(id) { return typeof id === "number" ? "0x" + id.toString(16) : String(id); }
81
+ function _kem(id) { var s = KEMS[id]; if (!s) throw _err("hpke/unknown-suite", "unsupported KEM id " + _idStr(id)); return s; }
82
+ function _kdf(id) { var s = KDFS[id]; if (!s) throw _err("hpke/unknown-suite", "unsupported KDF id " + _idStr(id)); return s; }
83
+ function _aead(id) { var s = AEADS[id]; if (!s) throw _err("hpke/unknown-suite", "unsupported AEAD id " + _idStr(id)); return s; }
84
+
85
+ // ---- labeled KDF (RFC 9180 sec. 4): HKDF Extract/Expand SPLIT over HMAC -------
86
+ // suite_id is the caller's ("KEM"||... for the KEM layer, "HPKE"||... for the
87
+ // rest). Extract = HMAC(salt-or-zeros, ikm); Expand = RFC 5869 sec. 2.3 feedback.
88
+
89
+ function _extract(hash, Nh, salt, ikm) {
90
+ var key = (salt && salt.length) ? salt : Buffer.alloc(Nh);
91
+ return nodeCrypto.createHmac(hash, key).update(ikm).digest();
92
+ }
93
+ function _expand(hash, Nh, prk, info, len) {
94
+ if (len > 255 * Nh) throw _err("hpke/export-length", "requested length " + len + " exceeds 255*Nh");
95
+ var out = [], t = Buffer.alloc(0), n = Math.ceil(len / Nh);
96
+ for (var i = 1; i <= n; i++) {
97
+ t = nodeCrypto.createHmac(hash, prk).update(concat([t, info, Buffer.from([i])])).digest();
98
+ out.push(t);
99
+ }
100
+ return concat(out).subarray(0, len);
101
+ }
102
+ function _labeledExtract(kdf, suiteId, salt, label, ikm) {
103
+ return _extract(kdf.hash, kdf.Nh, salt, concat([HPKE_V1, suiteId, L(label), ikm]));
104
+ }
105
+ function _labeledExpand(kdf, suiteId, prk, label, info, len) {
106
+ return _expand(kdf.hash, kdf.Nh, prk, concat([i2osp(len, 2), HPKE_V1, suiteId, L(label), info]), len);
107
+ }
108
+
109
+ // ---- KEM key (de)serialization + Diffie-Hellman (node:crypto) ----------------
110
+
111
+ var OKP_SPKI = { X25519: "302a300506032b656e032100", X448: "3042300506032b656f033900" };
112
+ var OKP_PKCS8 = { X25519: "302e020100300506032b656e04220420", X448: "3046020100300506032b656f043a0438" };
113
+
114
+ // Deserialize a serialized KEM public key (enc / pkRm / pkSm) into a node
115
+ // KeyObject. EC is an uncompressed 0x04 point (SerializePublicKey, sec. 7.1.1);
116
+ // OKP is the raw curve point. A wrong length / shape fails closed.
117
+ function _importPublic(kem, raw) {
118
+ if (!Buffer.isBuffer(raw) || raw.length !== kem.Npub) {
119
+ throw _err("hpke/bad-key", kem.curve + " public key must be " + kem.Npub + " bytes");
120
+ }
121
+ try {
122
+ if (kem.kind === "ec") {
123
+ if (raw[0] !== 0x04) throw _err("hpke/bad-key", "EC public key must be an uncompressed 0x04 point (RFC 9180 sec. 7.1.4)");
124
+ var half = (raw.length - 1) / 2;
125
+ var jwk = { kty: "EC", crv: kem.curve, x: raw.subarray(1, 1 + half).toString("base64url"), y: raw.subarray(1 + half).toString("base64url") };
126
+ return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
127
+ }
128
+ return nodeCrypto.createPublicKey({ key: Buffer.from(OKP_SPKI[kem.curve] + raw.toString("hex"), "hex"), format: "der", type: "spki" });
129
+ } catch (e) {
130
+ if (e instanceof HpkeError) throw e;
131
+ throw _err("hpke/bad-key", "invalid " + kem.curve + " public key", e);
132
+ }
133
+ }
134
+ // Serialize a node public KeyObject back to the RFC 9180 wire encoding.
135
+ function _exportPublic(kem, keyObject) {
136
+ var jwk = keyObject.export({ format: "jwk" });
137
+ if (kem.kind === "ec") return concat([Buffer.from([0x04]), _b64u(jwk.x), _b64u(jwk.y)]);
138
+ return _b64u(jwk.x);
139
+ }
140
+ // Node's own JWK export -- decode its canonical base64url coordinates through the
141
+ // guard so the alphabet/canonicality check is the single shared choke point.
142
+ function _b64u(s) { return guard.encoding.base64url(s, null, _err, "hpke/bad-key", "JWK coordinate"); }
143
+
144
+ // Import a serialized KEM private key (skRm / skEm / skSm) into a node KeyObject,
145
+ // given the matching serialized public key for the EC JWK (or none for OKP).
146
+ function _importPrivate(kem, rawSk, rawPk) {
147
+ try {
148
+ if (kem.kind === "ec") {
149
+ var jwk = { kty: "EC", crv: kem.curve, d: rawSk.toString("base64url") };
150
+ if (rawPk) { jwk.x = rawPk.subarray(1, 1 + (rawPk.length - 1) / 2).toString("base64url"); jwk.y = rawPk.subarray(1 + (rawPk.length - 1) / 2).toString("base64url"); }
151
+ return nodeCrypto.createPrivateKey({ key: jwk, format: "jwk" });
152
+ }
153
+ return nodeCrypto.createPrivateKey({ key: Buffer.from(OKP_PKCS8[kem.curve] + rawSk.toString("hex"), "hex"), format: "der", type: "pkcs8" });
154
+ } catch (e) {
155
+ throw _err("hpke/bad-key", "invalid " + kem.curve + " private key", e);
156
+ }
157
+ }
158
+ // The single KEM Diffie-Hellman choke point. node:crypto throws a raw
159
+ // ERR_OSSL_FAILED_DURING_DERIVATION when the peer point is low-order / invalid
160
+ // (an all-zero X25519 shared secret, a point not on the curve). RFC 9180 sec.
161
+ // 4.1: Decap raises an error on DH failure -- surface it as a typed hpke/bad-key
162
+ // so no raw OpenSSL error escapes any encap / decap path.
163
+ function _dh(privateKey, publicKey) {
164
+ try {
165
+ return nodeCrypto.diffieHellman({ privateKey: privateKey, publicKey: publicKey });
166
+ } catch (e) {
167
+ throw _err("hpke/bad-key", "KEM Diffie-Hellman failed: invalid or low-order public key", e);
168
+ }
169
+ }
170
+ function _generate(kem) {
171
+ if (kem.kind === "ec") return nodeCrypto.generateKeyPairSync("ec", { namedCurve: kem.nodeCurve });
172
+ return nodeCrypto.generateKeyPairSync(kem.curve.toLowerCase());
173
+ }
174
+
175
+ // ---- DHKEM (RFC 9180 sec. 4.1) -----------------------------------------------
176
+
177
+ function _kemSuiteId(kemId) { return concat([L("KEM"), i2osp(kemId, 2)]); }
178
+ function _extractAndExpand(kem, dh, kemContext) {
179
+ var kdf = _kdf(kem.kdf), sid = _kemSuiteId(_kemId(kem));
180
+ var eaePrk = _labeledExtract(kdf, sid, Buffer.alloc(0), "eae_prk", dh);
181
+ return _labeledExpand(kdf, sid, eaePrk, "shared_secret", kemContext, kem.Nsecret);
182
+ }
183
+ function _kemId(kem) { for (var k in KEMS) if (KEMS[k] === kem) return Number(k); return 0; }
184
+
185
+ // Encap(pkR) -> { sharedSecret, enc }. An injected ephemeral (skE, pkEnc) makes
186
+ // the KAT deterministic; otherwise a fresh ephemeral is generated.
187
+ function _ephemeral(kem, eph) {
188
+ if (eph) return { skE: _importPrivate(kem, eph.skm, eph.pkm), enc: _buf(eph.pkm) };
189
+ var kp = _generate(kem);
190
+ return { skE: kp.privateKey, enc: _exportPublic(kem, kp.publicKey) };
191
+ }
192
+ function _encap(kem, pkR, pkRm, eph) {
193
+ var e = _ephemeral(kem, eph);
194
+ var dh = _dh(e.skE, pkR);
195
+ return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm])), enc: e.enc };
196
+ }
197
+ function _decap(kem, enc, skR, pkRm) {
198
+ var pkE = _importPublic(kem, enc);
199
+ var dh = _dh(skR, pkE);
200
+ return _extractAndExpand(kem, dh, concat([enc, pkRm]));
201
+ }
202
+ function _authEncap(kem, pkR, pkRm, skS, pkSm, eph) {
203
+ var e = _ephemeral(kem, eph);
204
+ var dh = concat([_dh(e.skE, pkR), _dh(skS, pkR)]);
205
+ return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm, pkSm])), enc: e.enc };
206
+ }
207
+ function _authDecap(kem, enc, skR, pkRm, pkS, pkSm) {
208
+ var pkE = _importPublic(kem, enc);
209
+ var dh = concat([_dh(skR, pkE), _dh(skR, pkS)]);
210
+ return _extractAndExpand(kem, dh, concat([enc, pkRm, pkSm]));
211
+ }
212
+
213
+ // ---- key schedule (RFC 9180 sec. 5.1) ----------------------------------------
214
+
215
+ function _hpkeSuiteId(kemId, kdfId, aeadId) { return concat([L("HPKE"), i2osp(kemId, 2), i2osp(kdfId, 2), i2osp(aeadId, 2)]); }
216
+
217
+ function _verifyPsk(mode, psk, pskId) {
218
+ var gotPsk = psk.length > 0, gotId = pskId.length > 0;
219
+ if (gotPsk !== gotId) throw _err("hpke/inconsistent-psk", "psk and psk_id must be provided together (RFC 9180 sec. 5.1)");
220
+ if (gotPsk && (mode === MODE_BASE || mode === MODE_AUTH)) throw _err("hpke/inconsistent-psk", "a PSK was provided for a non-PSK mode");
221
+ if (!gotPsk && (mode === MODE_PSK || mode === MODE_AUTH_PSK)) throw _err("hpke/inconsistent-psk", "mode requires a PSK");
222
+ }
223
+
224
+ function _keySchedule(suite, mode, sharedSecret, info, psk, pskId, role) {
225
+ _verifyPsk(mode, psk, pskId);
226
+ var kdf = suite.kdf, sid = suite.suiteId, aead = suite.aead;
227
+ var pskIdHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "psk_id_hash", pskId);
228
+ var infoHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "info_hash", info);
229
+ var ksc = concat([Buffer.from([mode]), pskIdHash, infoHash]);
230
+ var secret = _labeledExtract(kdf, sid, sharedSecret, "secret", psk);
231
+ var exporterSecret = _labeledExpand(kdf, sid, secret, "exp", ksc, kdf.Nh);
232
+ var key = null, baseNonce = null;
233
+ if (!aead.exportOnly) {
234
+ key = _labeledExpand(kdf, sid, secret, "key", ksc, aead.Nk);
235
+ baseNonce = _labeledExpand(kdf, sid, secret, "base_nonce", ksc, aead.Nn);
236
+ }
237
+ return new Context(suite, key, baseNonce, exporterSecret, role);
238
+ }
239
+
240
+ // ---- AEAD context (RFC 9180 sec. 5.2 / 5.3) ----------------------------------
241
+
242
+ // role is "S" (sender: seal only) or "R" (recipient: open only). Sender and
243
+ // recipient derive the same key + base_nonce, so a wrong-direction call would
244
+ // reuse a nonce (RFC 9180 sec. 5.2); export is available to both.
245
+ function Context(suite, key, baseNonce, exporterSecret, role) {
246
+ this._suite = suite; this._key = key; this._baseNonce = baseNonce;
247
+ this._exporterSecret = exporterSecret; this._seq = 0n; this._role = role;
248
+ }
249
+ Context.prototype._nonce = function () {
250
+ var aead = this._suite.aead;
251
+ var seqBytes = i2osp(this._seq, aead.Nn);
252
+ return xor(this._baseNonce, seqBytes);
253
+ };
254
+ Context.prototype._inc = function () {
255
+ var aead = this._suite.aead;
256
+ if (this._seq >= (1n << BigInt(8 * aead.Nn)) - 1n) throw _err("hpke/message-limit", "AEAD sequence number would overflow (RFC 9180 sec. 5.2)");
257
+ this._seq += 1n;
258
+ };
259
+ Context.prototype.seal = function (aad, pt) {
260
+ var aead = this._suite.aead;
261
+ if (this._role !== "S") throw _err("hpke/wrong-role", "seal is only available on a sender context (RFC 9180 sec. 5.2)");
262
+ if (aead.exportOnly) throw _err("hpke/export-only", "seal is not available for an export-only AEAD");
263
+ var nonce = this._nonce();
264
+ var c = nodeCrypto.createCipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
265
+ if (aad && aad.length) c.setAAD(aad);
266
+ var body = concat([c.update(_buf(pt)), c.final()]);
267
+ var ct = concat([body, c.getAuthTag()]);
268
+ this._inc();
269
+ return ct;
270
+ };
271
+ Context.prototype.open = function (aad, ct) {
272
+ var aead = this._suite.aead;
273
+ if (this._role !== "R") throw _err("hpke/wrong-role", "open is only available on a recipient context (RFC 9180 sec. 5.2)");
274
+ if (aead.exportOnly) throw _err("hpke/export-only", "open is not available for an export-only AEAD");
275
+ ct = _buf(ct);
276
+ if (ct.length < aead.Nt) throw _err("hpke/open-failed", "ciphertext is shorter than the AEAD tag");
277
+ var nonce = this._nonce();
278
+ var d = nodeCrypto.createDecipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
279
+ if (aad && aad.length) d.setAAD(aad);
280
+ d.setAuthTag(ct.subarray(ct.length - aead.Nt));
281
+ var pt;
282
+ try { pt = concat([d.update(ct.subarray(0, ct.length - aead.Nt)), d.final()]); }
283
+ catch (e) { throw _err("hpke/open-failed", "AEAD authentication failed (RFC 9180 sec. 5.2)", e); }
284
+ this._inc();
285
+ return pt;
286
+ };
287
+ Context.prototype.export = function (exporterContext, len) {
288
+ if (!Number.isInteger(len) || len < 0) throw _err("hpke/export-length", "export length must be a non-negative integer");
289
+ return _labeledExpand(this._suite.kdf, this._suite.suiteId, this._exporterSecret, "sec", _buf(exporterContext), len);
290
+ };
291
+ function _buf(x) { return Buffer.isBuffer(x) ? x : Buffer.from(x || []); }
292
+
293
+ // ---- suite resolution + setup ------------------------------------------------
294
+
295
+ function _suite(ids) {
296
+ if (!ids || typeof ids !== "object") throw _err("hpke/unknown-suite", "suiteIds must be an object { kem, kdf, aead } from pki.hpke.suites");
297
+ var kemId = ids.kem, kdfId = ids.kdf, aeadId = ids.aead;
298
+ var kem = _kem(kemId), kdf = _kdf(kdfId), aead = _aead(aeadId);
299
+ return { kem: kem, kdf: kdf, aead: aead, kemId: kemId, kdfId: kdfId, aeadId: aeadId, suiteId: _hpkeSuiteId(kemId, kdfId, aeadId) };
300
+ }
301
+
302
+ // A KEM key argument is a node KeyObject or serialized bytes: a public key as a
303
+ // raw buffer (or { pkm }), a private key as { skm, pkm } (raw scalar + public
304
+ // point). A KAT drives the deterministic path via those serialized forms.
305
+ function _recipPublic(suite, pk) {
306
+ if (Buffer.isBuffer(pk)) return { key: _importPublic(suite.kem, pk), pkm: pk };
307
+ if (pk && pk.pkm) return { key: _importPublic(suite.kem, pk.pkm), pkm: pk.pkm };
308
+ // A node KeyObject: derive its wire form. A null/undefined/invalid value must
309
+ // fail closed here rather than let node's export throw a raw error upward.
310
+ try {
311
+ return { key: pk, pkm: _exportPublic(suite.kem, pk) };
312
+ } catch (e) {
313
+ if (e instanceof HpkeError) throw e;
314
+ throw _err("hpke/bad-key", "invalid KEM public key (expected a node KeyObject, a raw buffer, or { pkm })", e);
315
+ }
316
+ }
317
+ function _recipPrivate(suite, sk) {
318
+ if (sk && sk.skm) return { key: _importPrivate(suite.kem, sk.skm, sk.pkm), pkm: sk.pkm };
319
+ // A raw scalar alone cannot be imported (an EC private key needs its public
320
+ // point for the JWK), so the serialized private form is { skm, pkm }; reject a
321
+ // bare buffer, and fail any other bad key closed rather than let node's
322
+ // createPublicKey throw a raw error out of the setup path.
323
+ if (Buffer.isBuffer(sk)) throw _err("hpke/bad-key", "a serialized private key must be provided as { skm, pkm } (raw scalar + public point), not a bare buffer");
324
+ try {
325
+ var pkm = _exportPublic(suite.kem, nodeCrypto.createPublicKey(sk));
326
+ return { key: sk, pkm: pkm };
327
+ } catch (e) {
328
+ if (e instanceof HpkeError) throw e;
329
+ throw _err("hpke/bad-key", "invalid recipient private key (expected a node KeyObject or { skm, pkm })", e);
330
+ }
331
+ }
332
+
333
+ // Resolve and validate the HPKE mode: RFC 9180 sec. 5.1 defines exactly base /
334
+ // psk / auth / auth-psk. An unknown mode must fail closed, never key-schedule
335
+ // with an out-of-registry mode byte.
336
+ function _mode(opts) {
337
+ var mode = (opts.mode == null) ? MODE_BASE : opts.mode;
338
+ if (mode !== MODE_BASE && mode !== MODE_PSK && mode !== MODE_AUTH && mode !== MODE_AUTH_PSK) {
339
+ throw _err("hpke/unknown-mode", "unsupported HPKE mode " + JSON.stringify(mode) + " (RFC 9180 sec. 5.1 defines base / psk / auth / auth-psk)");
340
+ }
341
+ return mode;
342
+ }
343
+
344
+ // pki.hpke.suites -- the RFC 9180 sec. 7 registry code points (KEM / KDF / AEAD /
345
+ // MODE), passed to the setup functions to select a ciphersuite (documented in the
346
+ // @module @intro; a data registry, not a callable primitive).
347
+ var suites = {
348
+ KEM: { DHKEM_P256_HKDF_SHA256: 0x0010, DHKEM_P521_HKDF_SHA512: 0x0012, DHKEM_X25519_HKDF_SHA256: 0x0020, DHKEM_X448_HKDF_SHA512: 0x0021 },
349
+ KDF: { HKDF_SHA256: 0x0001, HKDF_SHA512: 0x0003 },
350
+ AEAD: { AES_128_GCM: 0x0001, AES_256_GCM: 0x0002, CHACHA20_POLY1305: 0x0003, EXPORT_ONLY: 0xFFFF },
351
+ MODE: { BASE: MODE_BASE, PSK: MODE_PSK, AUTH: MODE_AUTH, AUTH_PSK: MODE_AUTH_PSK },
352
+ };
353
+
354
+ // setup*S(suiteIds, pkR, opts) -> { enc, context }. opts: { info, psk, pskId,
355
+ // senderKey (auth), eph (KAT determinism) }. setup*R(suiteIds, enc, skR, opts).
356
+
357
+ /**
358
+ * @primitive pki.hpke.setupS
359
+ * @signature pki.hpke.setupS(suiteIds, recipientPublicKey, opts?) -> { enc, context }
360
+ * @since 0.2.2
361
+ * @status experimental
362
+ * @spec RFC 9180
363
+ * @related pki.hpke.setupR, pki.hpke.seal
364
+ *
365
+ * Establish a sender HPKE context for a recipient KEM public key: encapsulate a
366
+ * shared secret and run the key schedule, returning the encapsulated key `enc`
367
+ * (send it to the recipient) and a `context` whose `.seal(aad, pt)` /
368
+ * `.export(ctx, L)` encrypt and derive further secrets. `suiteIds` is
369
+ * `{ kem, kdf, aead }` from `pki.hpke.suites`; `recipientPublicKey` is a node
370
+ * KeyObject or the serialized public key bytes. `opts.mode` selects base / psk /
371
+ * auth / auth-psk (default base); `opts.info`, `opts.psk`/`opts.pskId`, and
372
+ * `opts.senderKey` (auth modes) supply the corresponding inputs.
373
+ *
374
+ * @example
375
+ * var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
376
+ * var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
377
+ * var sender = pki.hpke.setupS(ids, pkR, { info: Buffer.from("app") });
378
+ * var ct = sender.context.seal(Buffer.from("aad"), Buffer.from("secret"));
379
+ */
380
+ function _setupS(ids, pkR, opts) {
381
+ opts = opts || {};
382
+ var suite = _suite(ids);
383
+ var mode = _mode(opts);
384
+ var r = _recipPublic(suite, pkR);
385
+ var kem = suite.kem, k;
386
+ if (mode === MODE_AUTH || mode === MODE_AUTH_PSK) {
387
+ if (kem.kind !== "ec" && kem.kind !== "okp") throw _err("hpke/auth-unsupported", "the KEM does not support authenticated modes");
388
+ if (opts.senderKey == null) throw _err("hpke/auth-key-required", "an authenticated mode requires opts.senderKey (the sender's KEM private key, RFC 9180 sec. 5.1.3)");
389
+ var s = _recipPrivate(suite, opts.senderKey);
390
+ k = _authEncap(kem, r.key, r.pkm, s.key, s.pkm, opts.eph);
391
+ } else {
392
+ k = _encap(kem, r.key, r.pkm, opts.eph);
393
+ }
394
+ var ctx = _keySchedule(suite, mode, k.sharedSecret, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "S");
395
+ return { enc: k.enc, context: ctx, sharedSecret: k.sharedSecret };
396
+ }
397
+ /**
398
+ * @primitive pki.hpke.setupR
399
+ * @signature pki.hpke.setupR(suiteIds, enc, recipientPrivateKey, opts?) -> context
400
+ * @since 0.2.2
401
+ * @status experimental
402
+ * @spec RFC 9180
403
+ * @related pki.hpke.setupS, pki.hpke.open
404
+ *
405
+ * Establish the recipient HPKE context from the sender's encapsulated key `enc`
406
+ * and the recipient KEM private key (a node KeyObject or `{ skm, pkm }` -- the
407
+ * raw private scalar plus its public point),
408
+ * recovering the same shared secret and key schedule. The returned `context`
409
+ * `.open(aad, ct)` decrypts and `.export(ctx, L)` derives secrets. `opts` mirrors
410
+ * `setupS` (mode / info / psk / pskId), with `opts.senderPublicKey` for the auth
411
+ * modes. A ciphertext whose tag does not verify throws `hpke/open-failed`.
412
+ *
413
+ * @example
414
+ * var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
415
+ * var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
416
+ * var skR = Buffer.from("009f2181fba5f8908632c10ea1137c40a849728fde016c4602458b943a5dc048", "hex");
417
+ * var sender = pki.hpke.setupS(ids, pkR);
418
+ * var recipient = pki.hpke.setupR(ids, sender.enc, { skm: skR, pkm: pkR });
419
+ * var pt = recipient.open(Buffer.alloc(0), sender.context.seal(Buffer.alloc(0), Buffer.from("hi")));
420
+ */
421
+ function _setupR(ids, enc, skR, opts) {
422
+ opts = opts || {};
423
+ var suite = _suite(ids);
424
+ var mode = _mode(opts);
425
+ var r = _recipPrivate(suite, skR);
426
+ var kem = suite.kem, ss;
427
+ if (mode === MODE_AUTH || mode === MODE_AUTH_PSK) {
428
+ if (opts.senderPublicKey == null) throw _err("hpke/auth-key-required", "an authenticated mode requires opts.senderPublicKey (the sender's KEM public key, RFC 9180 sec. 5.1.3)");
429
+ var pkS = _recipPublic(suite, opts.senderPublicKey);
430
+ ss = _authDecap(kem, _buf(enc), r.key, r.pkm, pkS.key, pkS.pkm);
431
+ } else {
432
+ ss = _decap(kem, _buf(enc), r.key, r.pkm);
433
+ }
434
+ return _keySchedule(suite, mode, ss, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "R");
435
+ }
436
+
437
+ /**
438
+ * @primitive pki.hpke.seal
439
+ * @signature pki.hpke.seal(suiteIds, recipientPublicKey, opts, aad, pt) -> { enc, ct }
440
+ * @since 0.2.2
441
+ * @status experimental
442
+ * @spec RFC 9180
443
+ * @related pki.hpke.open, pki.hpke.setupS
444
+ *
445
+ * Single-shot HPKE encryption (RFC 9180 sec. 6): set up a sender context and
446
+ * encrypt one plaintext, returning the encapsulated key `enc` and ciphertext
447
+ * `ct`. Equivalent to `setupS` followed by one `context.seal`. `opts` is the
448
+ * `setupS` options object (mode / info / psk / senderKey).
449
+ *
450
+ * @example
451
+ * var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
452
+ * var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
453
+ * var out = pki.hpke.seal(ids, pkR, {}, Buffer.from("aad"), Buffer.from("msg"));
454
+ */
455
+ function seal(ids, pkR, opts, aad, pt) {
456
+ var s = _setupS(ids, pkR, opts);
457
+ return { enc: s.enc, ct: s.context.seal(_buf(aad), _buf(pt)) };
458
+ }
459
+
460
+ /**
461
+ * @primitive pki.hpke.open
462
+ * @signature pki.hpke.open(suiteIds, enc, recipientPrivateKey, opts, aad, ct) -> pt
463
+ * @since 0.2.2
464
+ * @status experimental
465
+ * @spec RFC 9180
466
+ * @related pki.hpke.seal, pki.hpke.setupR
467
+ *
468
+ * Single-shot HPKE decryption (RFC 9180 sec. 6): set up a recipient context from
469
+ * `enc` and decrypt one ciphertext, returning the plaintext. A tag that does not
470
+ * verify throws `hpke/open-failed` and returns no plaintext.
471
+ *
472
+ * @example
473
+ * var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
474
+ * var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
475
+ * var skR = Buffer.from("009f2181fba5f8908632c10ea1137c40a849728fde016c4602458b943a5dc048", "hex");
476
+ * var o = pki.hpke.seal(ids, pkR, {}, Buffer.alloc(0), Buffer.from("m"));
477
+ * var pt = pki.hpke.open(ids, o.enc, { skm: skR, pkm: pkR }, {}, Buffer.alloc(0), o.ct);
478
+ */
479
+ function open(ids, enc, skR, opts, aad, ct) {
480
+ return _setupR(ids, enc, skR, opts).open(_buf(aad), _buf(ct));
481
+ }
482
+
483
+ module.exports = {
484
+ suites: suites,
485
+ setupS: _setupS, setupR: _setupR,
486
+ seal: seal, open: open,
487
+ };
package/lib/oid.js CHANGED
@@ -201,7 +201,14 @@ var FAMILIES = {
201
201
  // CEK-HKDF content-encryption wrapper (RFC 9709) a KEMRecipientInfo names, plus
202
202
  // the RSA-KEM SPKI algorithm (RFC 9690). Parameters are absent for the HKDFs.
203
203
  smimeAlg: { base: [1, 2, 840, 113549, 1, 9, 16, 3], of: {
204
- "id-rsa-kem": 14, hkdfWithSha256: 28, hkdfWithSha384: 29, hkdfWithSha512: 30, cekHkdfSha256: 31 } },
204
+ "id-rsa-kem": 14, "id-alg-hss-lms-hashsig": 17,
205
+ hkdfWithSha256: 28, hkdfWithSha384: 29, hkdfWithSha512: 30, cekHkdfSha256: 31 } },
206
+
207
+ // PKIX algorithms arc -- the stateful hash-based signature algorithm
208
+ // identifiers (RFC 9802 sec. 4). HSS/LMS additionally has the SMIME
209
+ // id-alg-hss-lms-hashsig OID above (RFC 9708 / RFC 9802 share it).
210
+ pkixAlg: { base: [1, 3, 6, 1, 5, 5, 7, 6], of: {
211
+ "id-alg-xmss-hashsig": 34, "id-alg-xmssmt-hashsig": 35 } },
205
212
 
206
213
  // RSA-KEM key-transport algorithm (RFC 9690, obsoletes RFC 5990) on the ISO
207
214
  // 18033-2 arc -- the kem OID an RSA KEMRecipientInfo carries (distinct from the
@@ -501,6 +508,9 @@ var _PARAMS_ABSENT = new Set();
501
508
  // HKDF key-derivation identifiers (RFC 8619 sec. 2: when any of these appear
502
509
  // within AlgorithmIdentifier, the parameters component SHALL be absent).
503
510
  "hkdfWithSha256", "hkdfWithSha384", "hkdfWithSha512",
511
+ // Stateful hash-based signatures (RFC 9802 sec. 4 / RFC 9708): the parameters
512
+ // field MUST be absent for HSS/LMS, XMSS, and XMSS^MT public keys and signatures.
513
+ "id-alg-hss-lms-hashsig", "id-alg-xmss-hashsig", "id-alg-xmssmt-hashsig",
504
514
  ].forEach(function (nm) {
505
515
  var d = byName(nm);
506
516
  // A seed-list typo must fail at module load -- admitting undefined would
package/lib/shbs.js ADDED
@@ -0,0 +1,348 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.shbs
6
+ * @nav Signatures
7
+ * @title Stateful hash-based
8
+ * @intro Stateful hash-based signature VERIFICATION -- HSS/LMS (RFC 8554),
9
+ * carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST
10
+ * SP 800-208. VERIFY ONLY, by deliberate design: stateful hash-based SIGNING
11
+ * is catastrophic to get wrong -- each one-time key must be used exactly once,
12
+ * so the private key embeds a monotonic index whose state must advance and
13
+ * persist atomically across every signature and every process restart. A single
14
+ * index reuse (a restored VM snapshot, a crashed writer, a concurrent signer)
15
+ * forfeits security and can leak enough one-time-key material to forge, which is
16
+ * why SP 800-208 sec. 8 constrains signing-state handling to hardware. So this
17
+ * module NEVER mints a signature -- it verifies signatures produced in an HSM
18
+ * elsewhere. Verification is pure public-input SHA-256 / SHAKE256 hashing
19
+ * (no secret, no side-channel surface), so a pure-JavaScript verifier is safe.
20
+ * @spec RFC 8554, RFC 9802, RFC 9708, NIST SP 800-208
21
+ * @card Verify HSS/LMS signatures (post-quantum, CNSA 2.0 firmware signing).
22
+ */
23
+
24
+ var nodeCrypto = require("crypto");
25
+ var frameworkError = require("./framework-error");
26
+ var guard = require("./guard-all");
27
+ var constants = require("./constants");
28
+
29
+ var ShbsError = frameworkError.ShbsError;
30
+ function _err(code, message, cause) { return new ShbsError(code, message, cause); }
31
+
32
+ // ---- big-endian serialization (RFC 8554 sec. 3.1: network byte order) --------
33
+
34
+ function u8(x) { return Buffer.from([x & 0xff]); }
35
+ function u16(x) { return Buffer.from([(x >> 8) & 0xff, x & 0xff]); }
36
+ function u32(x) { return Buffer.from([(x >>> 24) & 0xff, (x >>> 16) & 0xff, (x >>> 8) & 0xff, x & 0xff]); }
37
+
38
+ // Domain separators (RFC 8554 sec. 7.1). All exceed 264 (the max Winternitz
39
+ // digit index i) so a security-string D can never collide with a chain-hash i,
40
+ // which occupies the same 2-byte offset (sec. 9.1).
41
+ var D_PBLC = 0x8080; // LM-OTS public-key finalize
42
+ var D_MESG = 0x8181; // message hash
43
+ var D_LEAF = 0x8282; // LMS leaf hash
44
+ var D_INTR = 0x8383; // LMS interior node hash
45
+
46
+ // ---- typecode registry (IANA Leighton-Micali Signatures; NIST SP 800-208) ----
47
+ // LMS 0x05..0x18 : {m, h, hashFamily}. LM-OTS 0x01..0x10 : {n, w, hashFamily}.
48
+ // p and ls are DERIVED from (n, w) per RFC 8554 sec. 4.1 / Appendix B so the
49
+ // table stays data, not a switch, and a wrong hardcoded p cannot creep in.
50
+
51
+ function _ilog2(x) { var r = 0; while (x > 1) { x = Math.floor(x / 2); r += 1; } return r; }
52
+
53
+ // RFC 8554 Appendix B: u = ceil(8n/w) hash digits; v = the checksum digit count;
54
+ // p = u + v total Winternitz digits; ls = the left-shift aligning the checksum's
55
+ // significant bits into the digit positions coef reads.
56
+ function _deriveWinternitz(n, w) {
57
+ var u = Math.ceil((8 * n) / w);
58
+ var v = Math.ceil((_ilog2((Math.pow(2, w) - 1) * u) + 1) / w);
59
+ return { p: u + v, ls: 16 - v * w };
60
+ }
61
+
62
+ var LMS_SETS = {};
63
+ var LMOTS_SETS = {};
64
+ (function seed() {
65
+ var heights = [5, 10, 15, 20, 25]; // H5..H25 in typecode order
66
+ var ws = [1, 2, 4, 8]; // W1..W8 in typecode order
67
+ // LMS families in IANA order: SHA-256/M32 (0x05), SHA-256/M24 (0x0A),
68
+ // SHAKE/M32 (0x0F), SHAKE/M24 (0x14).
69
+ [{ base: 0x05, m: 32, f: "sha256" }, { base: 0x0A, m: 24, f: "sha256" },
70
+ { base: 0x0F, m: 32, f: "shake256" }, { base: 0x14, m: 24, f: "shake256" }].forEach(function (fam) {
71
+ heights.forEach(function (h, i) { LMS_SETS[fam.base + i] = { code: fam.base + i, m: fam.m, h: h, hashFamily: fam.f }; });
72
+ });
73
+ // LM-OTS families: SHA-256/N32 (0x01), SHA-256/N24 (0x05), SHAKE/N32 (0x09),
74
+ // SHAKE/N24 (0x0D).
75
+ [{ base: 0x01, n: 32, f: "sha256" }, { base: 0x05, n: 24, f: "sha256" },
76
+ { base: 0x09, n: 32, f: "shake256" }, { base: 0x0D, n: 24, f: "shake256" }].forEach(function (fam) {
77
+ ws.forEach(function (w, i) {
78
+ var d = _deriveWinternitz(fam.n, w);
79
+ LMOTS_SETS[fam.base + i] = { code: fam.base + i, n: fam.n, w: w, p: d.p, ls: d.ls, hashFamily: fam.f };
80
+ });
81
+ });
82
+ })();
83
+
84
+ function _lmsSet(code) {
85
+ var s = LMS_SETS[code];
86
+ if (!s) throw _err("shbs/unsupported-parameter-set", "unrecognized or unapproved LMS typecode 0x" + code.toString(16));
87
+ return s;
88
+ }
89
+ function _lmotsSet(code) {
90
+ var s = LMOTS_SETS[code];
91
+ if (!s) throw _err("shbs/unsupported-parameter-set", "unrecognized or unapproved LM-OTS typecode 0x" + code.toString(16));
92
+ return s;
93
+ }
94
+
95
+ // hashFamily + output length -> the n-byte hash of the concatenated inputs.
96
+ // SHA-256 truncated to n (n=24 is SHA-256/192); SHAKE256 with an n-byte output.
97
+ function _hash(family, n, parts) {
98
+ var h;
99
+ if (family === "shake256") h = nodeCrypto.createHash("shake256", { outputLength: n });
100
+ else h = nodeCrypto.createHash("sha256");
101
+ for (var i = 0; i < parts.length; i++) h.update(parts[i]);
102
+ var d = h.digest();
103
+ return (family === "shake256" || n === 32) ? d : d.subarray(0, n);
104
+ }
105
+
106
+ // ---- bounded big-endian reader (bounds-before-slice; the ct.js TlsReader model
107
+ // for a non-DER positional wire, kept shbs-local -- shbs needs u32 typecodes +
108
+ // fixed-n reads, not CT's length-prefixed vector helpers) --------------------
109
+
110
+ function Reader(buf, code, label) { this.buf = buf; this.pos = 0; this.code = code; this.label = label; }
111
+ Reader.prototype._need = function (k) {
112
+ if (k < 0 || this.pos + k > this.buf.length) {
113
+ throw _err(this.code, this.label + " is truncated (needed " + k + " byte(s) at offset " + this.pos + ", have " + (this.buf.length - this.pos) + ")");
114
+ }
115
+ };
116
+ Reader.prototype.u32 = function () { this._need(4); var v = this.buf.readUInt32BE(this.pos); this.pos += 4; return v; };
117
+ Reader.prototype.take = function (k) { this._need(k); var b = this.buf.subarray(this.pos, this.pos + k); this.pos += k; return b; };
118
+ Reader.prototype.remaining = function () { return this.buf.length - this.pos; };
119
+ Reader.prototype.atEnd = function () { return this.pos === this.buf.length; };
120
+
121
+ // ---- Winternitz digit + checksum (RFC 8554 sec. 3.1.3 / sec. 4.4) ------------
122
+
123
+ // coef(S, i, w): the i-th w-bit digit of S, big-endian WITHIN each byte (digit 0
124
+ // is the high-order w bits of byte 0). RFC 8554 sec. 3.1.3.
125
+ function _coef(S, i, w) {
126
+ var idx = Math.floor((i * w) / 8);
127
+ if (idx >= S.length) throw _err("shbs/bad-signature", "Winternitz coefficient index out of range");
128
+ var shift = 8 - (w * (i % (8 / w)) + w);
129
+ return ((1 << w) - 1) & (S[idx] >> shift);
130
+ }
131
+
132
+ // Cksm(Q) per RFC 8554 sec. 4.4: sum over the u message digits of (2^w-1 - digit),
133
+ // left-shifted by ls, as a 2-byte big-endian value. Computed over Q ALONE; the
134
+ // coefficient index space in the chain walk spans Q || Cksm(Q).
135
+ function _cksm(Q, set) {
136
+ var w = set.w, sum = 0, u = Math.ceil((8 * set.n) / w);
137
+ for (var i = 0; i < u; i++) sum += ((1 << w) - 1) - _coef(Q, i, w);
138
+ sum = (sum << set.ls) & 0xffff;
139
+ return u16(sum);
140
+ }
141
+
142
+ // ---- LM-OTS public-key candidate Kc (RFC 8554 Algorithm 4b) ------------------
143
+ // I, q identify the LMS leaf; otsSet is resolved from the PUBLIC KEY's otstype
144
+ // (the authority); C + y[] come from the signature. Returns the n-byte Kc.
145
+
146
+ function _lmotsKc(otsSet, I, q, C, y, message) {
147
+ var n = otsSet.n, w = otsSet.w, p = otsSet.p, f = otsSet.hashFamily;
148
+ var qb = u32(q);
149
+ var Q = _hash(f, n, [I, qb, u16(D_MESG), C, message]);
150
+ var Qc = Buffer.concat([Q, _cksm(Q, otsSet)]);
151
+ var z = [I, qb, u16(D_PBLC)];
152
+ for (var i = 0; i < p; i++) {
153
+ var a = _coef(Qc, i, w);
154
+ var tmp = y[i];
155
+ // Chain to 2^w - 1 total applications; the bound is EXCLUSIVE (last j = 2^w-2),
156
+ // matching key generation (RFC 8554 sec. 4.5 vs Algorithm 4b step 3).
157
+ for (var j = a; j < (1 << w) - 1; j++) tmp = _hash(f, n, [I, qb, u16(i), u8(j), tmp]);
158
+ z.push(tmp);
159
+ }
160
+ return _hash(f, n, z);
161
+ }
162
+
163
+ // ---- LMS verification core (RFC 8554 Algorithm 6 / 6a) -----------------------
164
+ // pubBytes = lmstype(4) || otstype(4) || I(16) || T[1](m); sigBytes an LMS
165
+ // signature. Returns true iff the recomputed Merkle root equals T[1]. Throws
166
+ // ShbsError on any structural fault; a well-formed-but-wrong signature -> false.
167
+
168
+ function _lmsVerify(pubBytes, message, sigBytes) {
169
+ // -- public key (the authority for both typecodes; RFC 8554 Algorithm 6) --
170
+ if (pubBytes.length < 8) throw _err("shbs/bad-public-key", "LMS public key is shorter than 8 bytes");
171
+ var pr = new Reader(pubBytes, "shbs/bad-public-key", "LMS public key");
172
+ var lmsSet = _lmsSet(pr.u32());
173
+ var otsSet = _lmotsSet(pr.u32());
174
+ var m = lmsSet.m, h = lmsSet.h;
175
+ if (pubBytes.length !== 24 + m) throw _err("shbs/bad-public-key", "LMS public key must be exactly " + (24 + m) + " bytes");
176
+ var I = pr.take(16);
177
+ var T1 = pr.take(m);
178
+
179
+ // -- signature (RFC 8554 Algorithm 6a). q first, then the LM-OTS signature,
180
+ // then the LMS typecode (AFTER the LM-OTS sig), then the auth path. --
181
+ if (sigBytes.length < 8) throw _err("shbs/bad-signature", "LMS signature is shorter than 8 bytes");
182
+ var sr = new Reader(sigBytes, "shbs/bad-signature", "LMS signature");
183
+ var q = sr.u32();
184
+ var otsSigType = sr.u32();
185
+ // The public key -- never the attacker-controlled signature -- is the authority
186
+ // (downgrade defense): a signature whose OTS typecode does not equal the one the
187
+ // public key commits to cannot verify against it. RFC 8554 Algorithm 6a checks
188
+ // the typecode (step 2c, and 2g for the LMS type below) BEFORE the length (steps
189
+ // 2d / 2i), and returns INVALID -- a verification FAILURE (false), not a
190
+ // structural error. So a mismatch is `false` at this point EVEN for a blob too
191
+ // short to be a complete signature: the mismatch is decidable once q + typecode
192
+ // are read (the bounded reader already threw if those 8 bytes are absent). A
193
+ // matching typecode with a truncated body still throws below (bounds-before-
194
+ // slice). Re-sizing the body by the signature's own mismatched typecode -- to
195
+ // "fully validate before returning false" -- would violate this order and reject
196
+ // a legitimate typecode-mutation test vector as malformed instead of INVALID.
197
+ if (otsSigType !== otsSet.code) return false;
198
+ var n = otsSet.n, p = otsSet.p;
199
+ var C = sr.take(n);
200
+ var y = [];
201
+ for (var yi = 0; yi < p; yi++) y.push(sr.take(n));
202
+ var sigLmsType = sr.u32();
203
+ if (sigLmsType !== lmsSet.code) return false; // RFC 8554 Algorithm 6a step 2g -> INVALID
204
+ var path = [];
205
+ for (var pi = 0; pi < h; pi++) path.push(sr.take(m));
206
+ if (!sr.atEnd()) throw _err("shbs/bad-signature", "LMS signature has " + sr.remaining() + " trailing byte(s)");
207
+ // RFC 8554 Algorithm 6a step 2i: a leaf index q >= 2^h is INVALID -- a
208
+ // verification FAILURE (false), NOT a structural error. Checked here, AFTER the
209
+ // exact-length validation above (a truncated / trailing blob already threw
210
+ // typed), against the REGISTRY height, never the blob. 2^h fits Number (h<=25).
211
+ if (q >= Math.pow(2, h)) return false;
212
+
213
+ // -- recompute the LM-OTS public-key candidate, then fold the Merkle path --
214
+ var Kc = _lmotsKc(otsSet, I, q, C, y, message);
215
+ var nodeNum = Math.pow(2, h) + q;
216
+ var tmp = _hash(lmsSet.hashFamily, m, [I, u32(nodeNum), u16(D_LEAF), Kc]);
217
+ for (var i2 = 0; nodeNum > 1; i2++) {
218
+ var parent = Math.floor(nodeNum / 2);
219
+ if (nodeNum % 2 === 1) {
220
+ // odd node_num: the current node is a RIGHT child, sibling on the LEFT.
221
+ tmp = _hash(lmsSet.hashFamily, m, [I, u32(parent), u16(D_INTR), path[i2], tmp]);
222
+ } else {
223
+ tmp = _hash(lmsSet.hashFamily, m, [I, u32(parent), u16(D_INTR), tmp, path[i2]]);
224
+ }
225
+ nodeNum = parent;
226
+ }
227
+ // The root comparison is over PUBLIC values (T1 is in the public key, tmp is
228
+ // derived from public inputs -- no secret, hence the module's "no side-channel
229
+ // surface" note), so constant time is not strictly required. It routes through
230
+ // the shared crypto guard anyway, for one length-checked comparison primitive
231
+ // across the hash-tree verifiers (pki.merkle folds its root the same way).
232
+ return guard.crypto.constantTimeEqual(tmp, T1);
233
+ }
234
+
235
+ // Consume exactly one LMS public key from a reader (an HSS inner key), returning
236
+ // its raw bytes; sized off the LMS typecode it leads with.
237
+ function _consumeLmsPublicKey(r) {
238
+ var start = r.pos;
239
+ var code = r.u32(); // lmstype
240
+ r.u32(); // otstype (validated when the key is verified)
241
+ var m = _lmsSet(code).m;
242
+ r.take(16 + m); // I || T[1]
243
+ return r.buf.subarray(start, r.pos);
244
+ }
245
+
246
+ // The exact byte length (12 + n*(p+1) + m*h) of an LMS signature verified under
247
+ // `keyBytes` -- the AUTHORITATIVE public key for its level. The concatenated HSS
248
+ // blob is split by the public key's typecodes, never the attacker-controlled
249
+ // signature's own: so a typecode-mutated signature is sliced to the length the
250
+ // key expects and then _lmsVerify returns false (a verification failure, RFC 8554
251
+ // Algorithm 6a 2c/2g), consistent with verifyLms, instead of throwing on a
252
+ // mis-sized slice. Bounds-guarded (no direct read that could escape as a raw
253
+ // RangeError on a truncated inner key).
254
+ function _lmsSigLen(keyBytes) {
255
+ if (keyBytes.length < 8) throw _err("shbs/bad-public-key", "LMS public key is shorter than 8 bytes");
256
+ var lmsSet = _lmsSet(keyBytes.readUInt32BE(0));
257
+ var otsSet = _lmotsSet(keyBytes.readUInt32BE(4));
258
+ return 12 + otsSet.n * (otsSet.p + 1) + lmsSet.m * lmsSet.h;
259
+ }
260
+
261
+ // ---- HSS verification (RFC 8554 sec. 6.3) ------------------------------------
262
+
263
+ function _hssVerify(pubBytes, message, sigBytes) {
264
+ if (pubBytes.length < 4) throw _err("shbs/bad-public-key", "HSS public key is shorter than 4 bytes");
265
+ var pr = new Reader(pubBytes, "shbs/bad-public-key", "HSS public key");
266
+ var L = pr.u32();
267
+ if (L < 1 || L > constants.LIMITS.HSS_MAX_LEVELS) throw _err("shbs/bad-public-key", "HSS level count L=" + L + " is outside 1.." + constants.LIMITS.HSS_MAX_LEVELS);
268
+ var topKey = pr.buf.subarray(pr.pos); // the top-level LMS public key -- its
269
+ // structure (including an under-length blob) is validated by _lmsVerify below
270
+ // (the < 8 and exact-24+m checks), so no direct read here that would bypass the
271
+ // bounded reader and escape as a raw RangeError on a truncated key.
272
+
273
+ var sr = new Reader(sigBytes, "shbs/bad-signature", "HSS signature");
274
+ var Nspk = sr.u32();
275
+ // The level-count gate, checked BEFORE parsing any component (RFC 8554 sec. 6.3).
276
+ if (Nspk + 1 !== L) throw _err("shbs/bad-signature", "HSS Nspk+1 (" + (Nspk + 1) + ") does not equal the public-key level count L (" + L + ")");
277
+
278
+ var key = topKey;
279
+ for (var i = 0; i < Nspk; i++) {
280
+ // Slice this level's LMS signature by the AUTHORITATIVE key's length (so a
281
+ // typecode mismatch is a false verdict via _lmsVerify, not a mis-sized throw).
282
+ var sig = sr.take(_lmsSigLen(key));
283
+ var nextKey = _consumeLmsPublicKey(sr);
284
+ // Each level signs the SERIALIZED next-level LMS public key; that recovered
285
+ // key becomes the verification key for the level below (chain of trust).
286
+ if (!_lmsVerify(key, nextKey, sig)) return false;
287
+ key = nextKey;
288
+ }
289
+ // The final signature is the remainder; _lmsVerify parses it, applies the
290
+ // public-key-authoritative typecode check (mismatch -> false), and rejects a
291
+ // trailing / truncated remainder (bounds-before-slice + its own atEnd check).
292
+ return _lmsVerify(key, message, sr.buf.subarray(sr.pos));
293
+ }
294
+
295
+ // ---- public surface ----------------------------------------------------------
296
+
297
+ function _asBytes(x, label) { return guard.bytes.source(x, ShbsError, "shbs/bad-input", label); }
298
+
299
+ /**
300
+ * @primitive pki.shbs.verify
301
+ * @signature pki.shbs.verify(publicKey, message, signature) -> boolean
302
+ * @since 0.2.1
303
+ * @status experimental
304
+ * @spec RFC 8554 sec. 6, RFC 9802, RFC 9708
305
+ * @related pki.shbs.verifyLms
306
+ *
307
+ * Verify an HSS (Hierarchical Signature System) signature over `message` under
308
+ * `publicKey` -- the wire form RFC 9802 (X.509) and RFC 9708 (CMS) carry for
309
+ * `id-alg-hss-lms-hashsig`. The public key and signature are the raw HSS octet
310
+ * blobs the certificate / CMS parsers already surface (no ASN.1 wrapping). Every
311
+ * level of the hierarchy must verify: a single failing level yields false.
312
+ * Returns true for a valid signature, false for a well-formed signature that does
313
+ * not verify; a malformed blob (bad length, unknown or unapproved typecode,
314
+ * truncation, a typecode disagreeing between the key and the signature) throws a
315
+ * typed `ShbsError`. VERIFY ONLY -- this module never signs.
316
+ *
317
+ * @example
318
+ * var cert = pki.schema.x509.parse(der);
319
+ * var ok = pki.shbs.verify(cert.subjectPublicKeyInfo.publicKey.bytes,
320
+ * cert.tbsBytes, cert.signatureValue.bytes);
321
+ */
322
+ function verify(publicKey, message, signature) {
323
+ return _hssVerify(_asBytes(publicKey, "public key"), _asBytes(message, "message"), _asBytes(signature, "signature"));
324
+ }
325
+
326
+ /**
327
+ * @primitive pki.shbs.verifyLms
328
+ * @signature pki.shbs.verifyLms(publicKey, message, signature) -> boolean
329
+ * @since 0.2.1
330
+ * @status experimental
331
+ * @spec RFC 8554 sec. 5
332
+ * @related pki.shbs.verify
333
+ *
334
+ * Verify a single-tree LMS (Leighton-Micali Signature) over `message` -- the
335
+ * component an HSS hierarchy composes at each level, and a standalone algorithm
336
+ * in its own right. Same verdict contract as `pki.shbs.verify`: true / false for
337
+ * a well-formed signature, a typed `ShbsError` for a malformed blob.
338
+ *
339
+ * @example
340
+ * // lmsPublicKey / lmsSignature are raw LMS blobs (an HSS level, or a bare
341
+ * // LMS-signed artifact). Shown here on arbitrary bytes, which fail closed.
342
+ * var ok = pki.shbs.verifyLms(bytes, bytes, bytes);
343
+ */
344
+ function verifyLms(publicKey, message, signature) {
345
+ return _lmsVerify(_asBytes(publicKey, "public key"), _asBytes(message, "message"), _asBytes(signature, "signature"));
346
+ }
347
+
348
+ module.exports = { verify: verify, verifyLms: verifyLms };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:ef020f83-b6b5-464b-aefb-b888959cf51c",
5
+ "serialNumber": "urn:uuid:78e0173e-dadb-4062-861f-ed7ded314dab",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-11T23:26:22.326Z",
8
+ "timestamp": "2026-07-12T03:33:57.948Z",
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.2.0",
22
+ "bom-ref": "@blamejs/pki@0.2.2",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.0",
25
+ "version": "0.2.2",
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.2.0",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.2",
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.2.0",
57
+ "ref": "@blamejs/pki@0.2.2",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]