@blamejs/pki 0.1.32 → 0.2.1

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.
@@ -47,11 +47,8 @@ function pemDecode(text, label, PemError) {
47
47
  if (!m) throw new PemError("pem/no-block", "no PEM block found");
48
48
  if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
49
49
  var b64 = m[2].replace(/[\r\n\t ]+/g, "");
50
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64)) throw new PemError("pem/bad-base64", "PEM body is not valid base64");
51
- if (b64.length % 4 !== 0) throw new PemError("pem/bad-base64", "PEM base64 body must be whole 4-character groups (RFC 4648 sec. 3.5)");
52
- var der = Buffer.from(b64, "base64");
53
- if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
54
- return der;
50
+ // Strict canonical base64 (RFC 4648 sec. 3.5) via the shared encoding guard.
51
+ return guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body");
55
52
  }
56
53
 
57
54
  // pemDecodeAll(text, label, PemError): decode EVERY PEM block under the STRICT
@@ -72,10 +69,7 @@ function pemDecodeAll(text, label, PemError) {
72
69
  if (/\S/.test(text.slice(lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
73
70
  if (m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
74
71
  var b64 = m[2].replace(/[\r\n\t ]+/g, "");
75
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64) || b64.length % 4 !== 0) throw new PemError("pem/bad-base64", "PEM body is not canonical base64 (RFC 4648 sec. 3.5)");
76
- var der = Buffer.from(b64, "base64");
77
- if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
78
- blocks.push(der);
72
+ blocks.push(guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body"));
79
73
  lastEnd = re.lastIndex;
80
74
  }
81
75
  if (blocks.length === 0) throw new PemError("pem/no-block", "no PEM block found");
@@ -92,7 +86,10 @@ function pemEncode(der, label, PemError) {
92
86
  if (typeof label !== "string" || !/^[A-Z0-9]+( [A-Z0-9]+)*$/.test(label)) {
93
87
  throw new PemError("pem/bad-label", "pemEncode requires an uppercase A-Z0-9 label with single spaces");
94
88
  }
95
- var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
89
+ // Re-view through the byte guard: a string input would silently utf8-armor into
90
+ // a bogus PEM, and a detached-backed Buffer would armor an empty body -- both
91
+ // fail closed (a Uint8Array / Buffer of real DER re-views cleanly).
92
+ var buf = guard.bytes.view(der, PemError, "pem/bad-input", "pemEncode DER input");
96
93
  var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
97
94
  return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
98
95
  }
@@ -270,10 +267,12 @@ function attrValueToString(ns) {
270
267
  if (typeof value === "string" && value.charAt(0) === "\\") return asn1.build.utf8(value.slice(1));
271
268
  if (typeof value === "string" && value.charAt(0) === "#") {
272
269
  var hex = value.slice(1);
273
- if (!/^(?:[0-9A-Fa-f]{2})+$/.test(hex)) {
270
+ if (hex.length === 0) {
274
271
  throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
275
272
  }
276
- var raw = Buffer.from(hex, "hex");
273
+ // Strict canonical hex via the shared encoding guard (even-length, canonical,
274
+ // no silent truncation at the first non-hex character).
275
+ var raw = guard.encoding.hex(hex, null, ns.E, ns.prefix + "/bad-atv", "a #hex attribute value");
277
276
  try { asn1.decode(raw); }
278
277
  catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
279
278
  return raw;
@@ -550,6 +549,36 @@ function generalNames(ns, opts) {
550
549
  return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
551
550
  }
552
551
 
552
+ // DistributionPointName ::= CHOICE { fullName [0] IMPLICIT GeneralNames,
553
+ // nameRelativeToCRLIssuer [1] IMPLICIT RelativeDistinguishedName } (RFC 5280
554
+ // sec. 4.2.1.13). The caller hands the CHOICE node itself -- the alternative
555
+ // INSIDE the [0]-tagged distributionPoint field wrapper (a context tag on a
556
+ // CHOICE-typed field is always EXPLICIT, so the wire nests [0]{ [0]|[1] ... }).
557
+ // fullName surfaces each element's RAW GeneralName DER as { kind: "fullName",
558
+ // names: [Buffer, ...] } -- the byte-exact comparison key the sec. 5.2.5
559
+ // "identical encoding MUST be used" correspondence rule requires;
560
+ // nameRelativeToCRLIssuer surfaces its full [1]-tagged TLV as { kind: "rdn",
561
+ // bytes: Buffer } (byte-identical [1] TLVs are byte-identical RDN fragments).
562
+ // An empty fullName (GeneralNames is SIZE 1..MAX), a malformed element, or any
563
+ // other alternative throws the caller's `code`. Shared by the
564
+ // cRLDistributionPoints / freshestCRL extension decoders and the CRL checker's
565
+ // IssuingDistributionPoint decode, so the two sides of the sec. 6.3.3(b)(2)(i)
566
+ // correspondence comparison cannot drift.
567
+ function distributionPointName(ns, node, code) {
568
+ if (node && node.tagClass === "context" && node.tagNumber === 0) {
569
+ var gns = schema.walk(generalNames(ns, { implicitTag: 0, code: code, what: "DistributionPointName fullName" }), node, ns).result;
570
+ return { kind: "fullName", names: gns.names.map(function (n) { return n.bytes; }) };
571
+ }
572
+ if (node && node.tagClass === "context" && node.tagNumber === 1) {
573
+ // [1] IMPLICIT RelativeDistinguishedName -- the context tag replaces the
574
+ // SET tag, so the node's direct children are the AttributeTypeAndValues.
575
+ schema.walk(schema.implicitSetOf(1, attributeTypeAndValue(ns), {
576
+ min: 1, code: code, what: "DistributionPointName nameRelativeToCRLIssuer" }), node, ns);
577
+ return { kind: "rdn", bytes: node.bytes };
578
+ }
579
+ throw ns.E(code, "DistributionPointName must be fullName [0] or nameRelativeToCRLIssuer [1] (RFC 5280 sec. 4.2.1.13)");
580
+ }
581
+
553
582
  // certExtensionDecoders(ns) -- the ns-parameterized RFC 5280 sec. 4.2.1 extension
554
583
  // VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
555
584
  // value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
@@ -837,6 +866,54 @@ function certExtensionDecoders(ns) {
837
866
  return { poison: true };
838
867
  }
839
868
 
869
+ // cRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
870
+ // (RFC 5280 sec. 4.2.1.13); DistributionPoint ::= SEQUENCE {
871
+ // distributionPoint [0] DistributionPointName OPTIONAL, reasons [1]
872
+ // ReasonFlags OPTIONAL, cRLIssuer [2] GeneralNames OPTIONAL }. "a
873
+ // DistributionPoint MUST NOT consist of only the reasons field; either
874
+ // distributionPoint or cRLIssuer MUST be present" -- a reasons-only (or
875
+ // empty) DistributionPoint is malformed. The DistributionPointName is
876
+ // surfaced through the shared helper (raw name encodings -- the sec. 5.2.5
877
+ // correspondence key); `reasons` stays a raw BIT STRING ({ unusedBits,
878
+ // bytes }); cRLIssuer is a validated GeneralNames with decoded values.
879
+ // FreshestCRL ::= CRLDistributionPoints (sec. 4.2.1.15), so the SAME decoder
880
+ // serves both OIDs -- one codec, both registry rows.
881
+ function crlDistributionPoints(buf) {
882
+ var C = ns.prefix + "/bad-crl-distribution-points";
883
+ var kids = seqChildren(buf, C, "CRLDistributionPoints");
884
+ if (kids.length < 1) throw ns.E(C, "CRLDistributionPoints must contain at least one DistributionPoint (RFC 5280 sec. 4.2.1.13)");
885
+ return kids.map(function (dp) {
886
+ if (dp.tagClass !== "universal" || dp.tagNumber !== _T.SEQUENCE || !dp.children) {
887
+ throw ns.E(C, "DistributionPoint must be a SEQUENCE { distributionPoint?, reasons?, cRLIssuer? }");
888
+ }
889
+ var out = { distributionPoint: null, reasons: null, cRLIssuer: null };
890
+ var lastTag = -1;
891
+ dp.children.forEach(function (f) {
892
+ if (f.tagClass !== "context") throw ns.E(C, "DistributionPoint fields are context-tagged [0]/[1]/[2]");
893
+ if (f.tagNumber <= lastTag) throw ns.E(C, "DistributionPoint fields must be unique and in ascending order (DER)");
894
+ lastTag = f.tagNumber;
895
+ if (f.tagNumber === 0) {
896
+ // distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE.
897
+ if (!f.children || f.children.length !== 1) throw ns.E(C, "DistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
898
+ out.distributionPoint = distributionPointName(ns, f.children[0], C);
899
+ } else if (f.tagNumber === 1) {
900
+ var bs;
901
+ try { bs = asn1.read.bitStringImplicit(f, 1); }
902
+ catch (e) { throw ns.E(C, "DistributionPoint reasons [1] must be an IMPLICIT ReasonFlags BIT STRING", e); }
903
+ out.reasons = { unusedBits: bs.unusedBits, bytes: bs.bytes };
904
+ } else if (f.tagNumber === 2) {
905
+ out.cRLIssuer = schema.walk(generalNames(ns, { implicitTag: 2, decodeValue: true, code: C, what: "DistributionPoint cRLIssuer" }), f, ns).result;
906
+ } else {
907
+ throw ns.E(C, "DistributionPoint has an unexpected field [" + f.tagNumber + "]");
908
+ }
909
+ });
910
+ if (out.distributionPoint === null && out.cRLIssuer === null) {
911
+ throw ns.E(C, "a DistributionPoint must include distributionPoint or cRLIssuer -- it MUST NOT consist of only the reasons field (RFC 5280 sec. 4.2.1.13)");
912
+ }
913
+ return out;
914
+ });
915
+ }
916
+
840
917
  // byName returns undefined for an unregistered name; keying a decoder row
841
918
  // under "undefined" would silently drop that extension from dispatch (the
842
919
  // consumer would treat the extension as structurally unvalidated / absent),
@@ -861,6 +938,8 @@ function certExtensionDecoders(ns) {
861
938
  byOid[O("subjectKeyIdentifier")] = subjectKeyIdentifier;
862
939
  byOid[O("signedCertificateTimestampList")] = sctList;
863
940
  byOid[O("precertificatePoison")] = precertPoison;
941
+ byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
942
+ byOid[O("freshestCRL")] = crlDistributionPoints;
864
943
  return { byOid: byOid };
865
944
  }
866
945
 
@@ -1069,6 +1148,7 @@ module.exports = {
1069
1148
  name: name,
1070
1149
  generalName: generalName,
1071
1150
  generalNames: generalNames,
1151
+ distributionPointName: distributionPointName,
1072
1152
  generalizedTime: generalizedTime,
1073
1153
  utf8Text: utf8Text,
1074
1154
  rawNonEmptySequence: rawNonEmptySequence,
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 };