@blamejs/pki 0.2.16 → 0.2.18

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.
@@ -0,0 +1,240 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The composite ML-DSA signature engine
6
+ // (draft-ietf-lamps-pq-composite-sigs for the construction; draft-ietf-lamps-cms-composite-sigs
7
+ // binds it to CMS), shared by certification-path validation (X.509 composite certificates,
8
+ // lib/path-validate.js) and CMS (composite SignerInfo, lib/cms-verify.js / lib/cms-sign.js).
9
+ //
10
+ // A composite signature pairs a post-quantum ML-DSA with a traditional RSA / ECDSA / EdDSA so the
11
+ // signature stays trustworthy if EITHER primitive is later broken. The public key is the raw
12
+ // concatenation mldsaPK || tradPK (sec. 4.1) in the SPKI BIT STRING; the signature is
13
+ // mldsaSig || tradSig (sec. 4.3). Verification (sec. 2) reconstructs
14
+ // M' = Prefix || Label || len(ctx) || ctx || PH(M),
15
+ // verifies the ML-DSA component over M' with ctx = the composite Label, verifies the traditional
16
+ // component over M' under its own hash, and accepts IFF BOTH pass (THREAT-MODEL: all components
17
+ // must verify -- never an AND-to-OR downgrade). The ML-DSA component is the fixed-length FIRST
18
+ // half; the split point is its length.
19
+ //
20
+ // Error-parameterized like the guard / validator families: each consumer passes its own typed
21
+ // error CONSTRUCTOR `E` and its domain codes (path validation passes PathError + path/*; CMS
22
+ // passes CmsError + cms/*), so the shared engine keeps no domain of its own. compositeVerify keeps
23
+ // the { ok, code, error } verdict shape; the codes are caller-supplied.
24
+
25
+ var asn1 = require("./asn1-der");
26
+ var oid = require("./oid");
27
+ var webcrypto = require("./webcrypto");
28
+ var validator = require("./validator-all");
29
+ var edwardsPoint = require("./edwards-point");
30
+ var subtle = webcrypto.webcrypto.subtle;
31
+ var _b = asn1.build;
32
+
33
+ // A caught error's OWN code if it shares the caller's domain prefix (the fallback's namespace,
34
+ // e.g. path/ or cms/), else the fallback -- a foreign code (asn1/*) maps to the fallback so the
35
+ // verdict stays in the caller's error namespace. Mirrors path-validate's original pathCode when
36
+ // the fallback is a path/* code.
37
+ function _codeOf(e, fallback) {
38
+ var prefix = fallback.slice(0, fallback.indexOf("/") + 1);
39
+ return (e && typeof e.code === "string" && e.code.indexOf(prefix) === 0) ? e.code : fallback;
40
+ }
41
+
42
+ var COMPOSITE_PREFIX = Buffer.from("CompositeAlgorithmSignatures2025", "ascii");
43
+ var MLDSA_COMPONENT = {
44
+ "ML-DSA-44": { pk: 1312, sig: 2420, oid: "id-ml-dsa-44" },
45
+ "ML-DSA-65": { pk: 1952, sig: 3309, oid: "id-ml-dsa-65" },
46
+ "ML-DSA-87": { pk: 2592, sig: 4627, oid: "id-ml-dsa-87" },
47
+ };
48
+ var EC_CURVE_OID = { "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1" };
49
+ // The WebCrypto pre-hash name (ph) -> the CMS digest-algorithm name (the SignerInfo digestAlgorithm,
50
+ // draft-ietf-lamps-cms-composite-sigs Table 1). The SINGLE source of truth for the CMS binding's
51
+ // digest -- both the verify coherence gate and the sign digestAlgorithm read phCms, never a second
52
+ // name map inlined at the CMS boundary.
53
+ var PH_CMS = { "SHA-256": "sha256", "SHA-512": "sha512", "SHAKE256": "shake256" };
54
+ var COMPOSITE_ALGS = {};
55
+ // _comp(name, mldsa, ph, label, trad). `trad` is exactly one component shape:
56
+ // { ec, hash } | { eddsa } | { rsaPss, hash, salt } | { rsaPkcs1, hash } |
57
+ // { unsupported } for the arms Node's WebCrypto surface cannot verify (brainpool
58
+ // curves; the SHAKE256/64 pre-hash) -- registered + params-guarded, deferred at
59
+ // verify to the caller's unsupported-algorithm code rather than silently accepted.
60
+ function _comp(name, mldsa, ph, label, trad) {
61
+ var sz = MLDSA_COMPONENT[mldsa];
62
+ COMPOSITE_ALGS[oid.byName(name)] = {
63
+ name: name, mldsa: mldsa, mldsaPk: sz.pk, mldsaSig: sz.sig, mldsaOid: sz.oid,
64
+ ph: ph, phCms: PH_CMS[ph], label: Buffer.from(label, "ascii"), trad: trad,
65
+ };
66
+ }
67
+ _comp("id-MLDSA44-RSA2048-PSS-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PSS-SHA256", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 2048 });
68
+ _comp("id-MLDSA44-RSA2048-PKCS15-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PKCS15-SHA256", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 2048 });
69
+ _comp("id-MLDSA44-Ed25519-SHA512", "ML-DSA-44", "SHA-512", "COMPSIG-MLDSA44-Ed25519-SHA512", { eddsa: "Ed25519" });
70
+ _comp("id-MLDSA44-ECDSA-P256-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-ECDSA-P256-SHA256", { ec: "P-256", hash: "SHA-256" });
71
+ _comp("id-MLDSA65-RSA3072-PSS-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PSS-SHA512", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 3072 });
72
+ _comp("id-MLDSA65-RSA3072-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 3072 });
73
+ _comp("id-MLDSA65-RSA4096-PSS-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PSS-SHA512", { rsaPss: true, hash: "SHA-384", salt: 48, rsaBits: 4096 });
74
+ _comp("id-MLDSA65-RSA4096-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-384", rsaBits: 4096 });
75
+ _comp("id-MLDSA65-ECDSA-P256-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P256-SHA512", { ec: "P-256", hash: "SHA-256" });
76
+ _comp("id-MLDSA65-ECDSA-P384-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
77
+ _comp("id-MLDSA65-ECDSA-brainpoolP256r1-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-BP256-SHA512", { unsupported: "brainpoolP256r1 is not in the WebCrypto ECDSA curve set" });
78
+ _comp("id-MLDSA65-Ed25519-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-Ed25519-SHA512", { eddsa: "Ed25519" });
79
+ _comp("id-MLDSA87-ECDSA-P384-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
80
+ _comp("id-MLDSA87-ECDSA-brainpoolP384r1-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-BP384-SHA512", { unsupported: "brainpoolP384r1 is not in the WebCrypto ECDSA curve set" });
81
+ _comp("id-MLDSA87-Ed448-SHAKE256", "ML-DSA-87", "SHAKE256", "COMPSIG-MLDSA87-Ed448-SHAKE256", { unsupported: "the SHAKE256/64 pre-hash is not in the WebCrypto digest set" });
82
+ _comp("id-MLDSA87-RSA3072-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-RSA3072-PSS-SHA512", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 3072 });
83
+ _comp("id-MLDSA87-RSA4096-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-RSA4096-PSS-SHA512", { rsaPss: true, hash: "SHA-384", salt: 48, rsaBits: 4096 });
84
+ _comp("id-MLDSA87-ECDSA-P521-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P521-SHA512", { ec: "P-521", hash: "SHA-512" });
85
+
86
+ // Wrap a raw component public key in the SPKI its WebCrypto import expects, so each half is
87
+ // verified through the SAME import + verify seam the classical path uses (no second parallel
88
+ // verify path). tradPK for RSA is the RSAPublicKey DER; for EC the uncompressed point; for EdDSA
89
+ // the raw public key.
90
+ function _spkiFor(algNode, keyBytes) { return _b.sequence([algNode, _b.bitString(keyBytes, 0)]); }
91
+ function _mldsaSpki(mldsaOid, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(mldsaOid))]), raw); }
92
+ function _ecSpki(curve, point) { return _spkiFor(_b.sequence([_b.oid(oid.byName("ecPublicKey")), _b.oid(oid.byName(EC_CURVE_OID[curve]))]), point); }
93
+ function _edSpki(name, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(name))]), raw); }
94
+ function _rsaSpki(rsaPub) { return _spkiFor(_b.sequence([_b.oid(oid.byName("rsaEncryption")), _b.nullValue()]), rsaPub); }
95
+
96
+ // The exact modulus bit length of an RSAPublicKey (SEQUENCE { modulus, exponent }).
97
+ function _rsaModulusBits(rsaPubDer) {
98
+ return asn1.read.integer(asn1.decode(rsaPubDer).children[0]).toString(2).length;
99
+ }
100
+
101
+ // M' = Prefix || Label || len(ctx) || ctx || PH(M) (draft sec. 2 / RFC 8410-style domain
102
+ // separation). ctx is a single-octet-length-prefixed application context; X.509 path validation
103
+ // and CMS both use the empty context. Shared by verify AND sign so the two cannot diverge.
104
+ function compositeMprime(d, phBuf, ctx) {
105
+ ctx = ctx || Buffer.alloc(0);
106
+ return Buffer.concat([COMPOSITE_PREFIX, d.label, Buffer.from([ctx.length]), ctx, Buffer.from(phBuf)]);
107
+ }
108
+
109
+ function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
110
+ if (trad.ec) {
111
+ // draft sec. 5.1: the EC point MUST be the uncompressed X9.62 form (leading 0x04).
112
+ // A compressed / hybrid point is a non-conforming composite component encoding.
113
+ if (tradPK.length < 1 || tradPK[0] !== 0x04) return Promise.resolve(false);
114
+ return subtle.importKey("spki", _ecSpki(trad.ec, tradPK), { name: "ECDSA" }, false, ["verify"]).then(function (k) {
115
+ // The traditional ECDSA signature is DER Ecdsa-Sig-Value; convert to P1363 through the shared
116
+ // order-aware reader that also rejects r/s outside [1,n-1] (CVE-2022-21449).
117
+ var p1363 = validator.sig.ecdsaDerToP1363(tradSig, trad.ec, E, badSig);
118
+ return subtle.verify({ name: "ECDSA", hash: trad.hash }, k, p1363, mprime);
119
+ });
120
+ }
121
+ if (trad.eddsa) {
122
+ // node/OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a low-order
123
+ // (e.g. all-zeroes) key verifies a forged signature -- which would collapse the composite AND
124
+ // to ML-DSA-only. Reject a non-full-order point before verify, through the same shared
125
+ // edwards-point gate every other EdDSA verify path in the toolkit routes through, surfacing a
126
+ // coded fault (like the ECDSA order-bound belt) rather than a silent false.
127
+ if (!edwardsPoint.validate(tradPK, trad.eddsa === "Ed25519" ? 6 : 7)) {
128
+ throw new E(badSig, "the composite EdDSA component public key is not a valid, full-order Edwards point");
129
+ }
130
+ return subtle.importKey("spki", _edSpki(trad.eddsa, tradPK), { name: trad.eddsa }, false, ["verify"])
131
+ .then(function (k) { return subtle.verify({ name: trad.eddsa }, k, tradSig, mprime); });
132
+ }
133
+ if (trad.rsaPss || trad.rsaPkcs1) {
134
+ // The composite OID fixes the RSA modulus size: a downgraded or mismatched modulus under the
135
+ // declared OID (an id-MLDSA44-RSA2048-* whose component is really 1024-bit) is rejected BEFORE
136
+ // verify, so a weak RSA component cannot satisfy an arm that promises 2048/3072/4096 bits. A
137
+ // malformed RSAPublicKey rejects the same way.
138
+ var bits;
139
+ try { bits = _rsaModulusBits(tradPK); }
140
+ catch (_e) { return Promise.resolve(false); }
141
+ if (bits !== trad.rsaBits) return Promise.resolve(false);
142
+ if (trad.rsaPss) {
143
+ return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSA-PSS", hash: trad.hash }, false, ["verify"])
144
+ .then(function (k) { return subtle.verify({ name: "RSA-PSS", saltLength: trad.salt }, k, tradSig, mprime); });
145
+ }
146
+ return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
147
+ .then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
148
+ }
149
+ // Coverage residual -- unreachable: compositeVerify returns early for a trad.unsupported arm, so
150
+ // _verifyTradComponent runs only for arms that set exactly one of ec/eddsa/rsaPss/rsaPkcs1.
151
+ return Promise.resolve(false);
152
+ }
153
+
154
+ // Verify a composite signature: `spkiBytes` the issuer SPKI DER, `sigBytes` the raw signatureValue
155
+ // (mldsaSig || tradSig), `message` the signed region. `E` the caller's error constructor;
156
+ // `unsupported`/`badSig` its domain codes. Returns { ok, code?, error? } (a false is a verdict).
157
+ function compositeVerify(spkiBytes, sigBytes, message, d, E, unsupported, badSig) {
158
+ if (d.trad.unsupported) {
159
+ return Promise.resolve({ ok: false, code: unsupported,
160
+ error: new E(unsupported, "composite " + d.name + ": " + d.trad.unsupported) });
161
+ }
162
+ var rawKey;
163
+ try {
164
+ var bs = asn1.read.bitString(asn1.decode(spkiBytes).children[1]);
165
+ // The composite subjectPublicKey is an octet-aligned concatenation (no unused bits); a non-zero
166
+ // unused-bit count is malformed.
167
+ if (bs.unusedBits !== 0) throw new E(badSig, "composite subjectPublicKey has unused bits");
168
+ rawKey = bs.bytes;
169
+ } catch (e) { return Promise.resolve({ ok: false, code: _codeOf(e, badSig), error: e }); }
170
+ // The ML-DSA half is fixed-length and FIRST; the traditional half is the remainder. Both must be
171
+ // non-empty for a well-formed composite.
172
+ if (rawKey.length <= d.mldsaPk || sigBytes.length <= d.mldsaSig) {
173
+ return Promise.resolve({ ok: false, code: badSig,
174
+ error: new E(badSig, "composite key/signature shorter than the fixed ML-DSA component") });
175
+ }
176
+ var mldsaPK = rawKey.subarray(0, d.mldsaPk), tradPK = rawKey.subarray(d.mldsaPk);
177
+ var mldsaSig = sigBytes.subarray(0, d.mldsaSig), tradSig = sigBytes.subarray(d.mldsaSig);
178
+ return subtle.digest({ name: d.ph }, message).then(function (phBuf) {
179
+ var mprime = compositeMprime(d, phBuf);
180
+ var mldsaP = subtle.importKey("spki", _mldsaSpki(d.mldsaOid, mldsaPK), { name: d.mldsa }, false, ["verify"])
181
+ .then(function (mk) { return subtle.verify({ name: d.mldsa, context: d.label }, mk, mldsaSig, mprime); });
182
+ var tradP = _verifyTradComponent(d.trad, tradPK, tradSig, mprime, E, badSig);
183
+ return Promise.all([mldsaP, tradP]).then(function (r) { return { ok: r[0] === true && r[1] === true }; });
184
+ }).catch(function (e) { return { ok: false, code: _codeOf(e, badSig), error: e }; });
185
+ }
186
+
187
+ // Resolve the composite descriptor for a signatureAlgorithm, or null if the OID is not composite.
188
+ // A composite OID MUST carry absent parameters (draft-ietf-lamps-pq-composite-sigs sec. 5.3); the
189
+ // public-key algorithm OID equals the signature OID, so the caller enforces sameKeyOid.
190
+ function resolveCompositeDescriptor(sigAlg, E, unsupported) {
191
+ var comp = COMPOSITE_ALGS[sigAlg.oid];
192
+ if (!comp) return null;
193
+ // Coverage residual -- unreachable through the shipped consumer: the only caller (path-validate)
194
+ // reaches here with a sigAlg from a parsed structure whose shared AlgorithmIdentifier decoder
195
+ // already rejects a composite OID carrying parameters (the _PARAMS_ABSENT gate), so `parameters`
196
+ // is always absent here. Kept as defense-in-depth. (CMS does its own inline params check.)
197
+ if (sigAlg.parameters !== null && sigAlg.parameters !== undefined) {
198
+ throw new E(unsupported, "composite signature algorithm parameters must be absent (draft-ietf-lamps-pq-composite-sigs sec. 5.3)");
199
+ }
200
+ return { composite: comp, sameKeyOid: true };
201
+ }
202
+
203
+ // Sign a composite signature over `message`: `keys` = { mldsa, trad } component private keys as
204
+ // PKCS#8 DER (Node has no composite private-key type). Signs the ML-DSA half over M' with ctx =
205
+ // the composite Label (the same domain-separated preimage compositeVerify reconstructs), signs the
206
+ // traditional half over M', and returns mldsaSig || tradSig. The caller rejects an unsupported arm
207
+ // before calling (d.trad.unsupported).
208
+ function compositeSign(d, keys, message, ctx) {
209
+ return subtle.digest({ name: d.ph }, message).then(function (phBuf) {
210
+ var mprime = compositeMprime(d, phBuf, ctx);
211
+ var mldsaP = subtle.importKey("pkcs8", keys.mldsa, { name: d.mldsa }, false, ["sign"])
212
+ .then(function (k) { return subtle.sign({ name: d.mldsa, context: d.label }, k, mprime); });
213
+ var tradP = _signTradComponent(d.trad, keys.trad, mprime);
214
+ return Promise.all([mldsaP, tradP]).then(function (s) { return Buffer.concat([Buffer.from(s[0]), Buffer.from(s[1])]); });
215
+ });
216
+ }
217
+ function _signTradComponent(trad, pkcs8, mprime) {
218
+ if (trad.ec) {
219
+ return subtle.importKey("pkcs8", pkcs8, { name: "ECDSA", namedCurve: trad.ec }, false, ["sign"])
220
+ .then(function (k) { return subtle.sign({ name: "ECDSA", hash: trad.hash }, k, mprime); })
221
+ // WebCrypto emits P1363 r||s; the composite trad-ECDSA signature is DER Ecdsa-Sig-Value, so
222
+ // re-encode through the shared canonical-DER gate (the inverse of the verify path's read).
223
+ .then(function (raw) { var r = Buffer.from(raw); return validator.sig.rawToEcdsaDer(r, r.length / 2); });
224
+ }
225
+ if (trad.eddsa) {
226
+ return subtle.importKey("pkcs8", pkcs8, { name: trad.eddsa }, false, ["sign"])
227
+ .then(function (k) { return subtle.sign({ name: trad.eddsa }, k, mprime); }).then(function (s) { return Buffer.from(s); });
228
+ }
229
+ var imp = trad.rsaPss ? { name: "RSA-PSS", hash: trad.hash } : { name: "RSASSA-PKCS1-v1_5", hash: trad.hash };
230
+ var alg = trad.rsaPss ? { name: "RSA-PSS", saltLength: trad.salt } : { name: "RSASSA-PKCS1-v1_5" };
231
+ return subtle.importKey("pkcs8", pkcs8, imp, false, ["sign"]).then(function (k) { return subtle.sign(alg, k, mprime); }).then(function (s) { return Buffer.from(s); });
232
+ }
233
+
234
+ module.exports = {
235
+ COMPOSITE_ALGS: COMPOSITE_ALGS,
236
+ compositeVerify: compositeVerify,
237
+ resolveCompositeDescriptor: resolveCompositeDescriptor,
238
+ compositeMprime: compositeMprime,
239
+ compositeSign: compositeSign,
240
+ };
@@ -18,6 +18,8 @@
18
18
  // Pure BigInt modular arithmetic (constant-time is not a goal here: the input is a public
19
19
  // key, and the verdict is a public validity bit, not a secret).
20
20
 
21
+ var asn1 = require("./asn1-der");
22
+
21
23
  // ---- field parameters ------------------------------------------------------
22
24
  var P25519 = (1n << 255n) - 19n;
23
25
  // d = -121665 / 121666 (mod p); sqrt(-1) = 2^((p-1)/4) (mod p).
@@ -88,4 +90,21 @@ function validate(raw, crv) {
88
90
  return true;
89
91
  }
90
92
 
91
- module.exports = { validate: validate };
93
+ // validateSpki(spkiBytes, crv, E, code) -> throws new E(code, ...) unless the
94
+ // SubjectPublicKeyInfo's subjectPublicKey (its BIT STRING body, past the unused-bits octet) is a
95
+ // canonical, on-curve, full-order Edwards point for the OKP curve (6 = Ed25519, 7 = Ed448). This
96
+ // is the one home every EdDSA verify path routes an SPKI through before importKey/verify -- node
97
+ // imports a low-order (e.g. all-zeroes) OKP SPKI without complaint and such a key verifies a
98
+ // forged signature. Error-parameterized like the guard family: the caller passes its own typed
99
+ // error CONSTRUCTOR `E` and domain `code`, so the shared gate keeps no error domain of its own.
100
+ function validateSpki(spkiBytes, crv, E, code) {
101
+ var point;
102
+ try {
103
+ point = asn1.decode(spkiBytes).children[1].content.subarray(1);
104
+ } catch (e) { throw new E(code, "the EdDSA public key is not a well-formed SubjectPublicKeyInfo", e); }
105
+ if (!validate(point, crv)) {
106
+ throw new E(code, "the EdDSA public key is not a valid, full-order Edwards point");
107
+ }
108
+ }
109
+
110
+ module.exports = { validate: validate, validateSpki: validateSpki };
package/lib/inspect.js CHANGED
@@ -63,6 +63,12 @@ function _hexColon(buf, opts) {
63
63
  return lines.join("\n");
64
64
  }
65
65
 
66
+ // Coverage residual -- two _hexColon default arms are unreachable through the public API:
67
+ // * `opts = opts || {}` -- all call sites pass an explicit opts object literal, so the
68
+ // `|| {}` default never fires.
69
+ // * `" ".repeat(opts.indent || 0)` -- every wrap-mode caller passes a positive indent
70
+ // (pad.length + 8 >= 8, or inner.length == 16), so `opts.indent` is never falsy.
71
+
66
72
  // Control-byte neutralization for a GeneralName string value routes through the
67
73
  // guard family (the guard-shape-reinlined detector protects the shape).
68
74
  // @guard-via guard\.name\.escape
@@ -217,6 +223,28 @@ function _gnRaw(buf) {
217
223
  } catch (_e) { return _hexColon(buf, {}); }
218
224
  }
219
225
 
226
+ // Coverage residual -- the GeneralName render fallbacks are unreachable because the strict
227
+ // generalName decoder and each caller already narrow the shape (the _gnRaw catch above is
228
+ // separately documented):
229
+ // * _ipString `if (!Buffer.isBuffer(buf)) return "";` -- every caller passes a Buffer (the
230
+ // recursion slices a Buffer, _gn guards Buffer.isBuffer(g.value), _gnRaw passes
231
+ // asn1.decode(...).content).
232
+ // * _ipString trailing `return _hexColon(buf, {});` -- generalName enforces iPAddress to
233
+ // 4/16 octets (8/32 for a name-constraints subtree base), so length is only ever 4/8/16/32.
234
+ // * _gn `if (!g || typeof g !== "object") return "";` -- every _gn call maps a decoder-produced
235
+ // GeneralName object; the decoders never emit a null element.
236
+ // * _gn directoryName `: ((g.value && g.value.dn) || "")` -- a decoded directoryName [4] always
237
+ // carries a Name with an rdns array, so the rdns arm is always taken.
238
+ // * _gn otherName `: "<unsupported>"` -- a decoded GeneralName always carries its raw bytes TLV.
239
+ // * _gn `|| ("tag" + t)` -- GeneralName CHOICE tags are 0..8, all mapped in NAMES.GENERAL_NAME;
240
+ // generalName rejects a tag outside 0..8.
241
+ // * _gn `? _hexColon(g.value, {})` -- iPAddress [7] is the only choice whose decoded value is a
242
+ // Buffer, handled at the t === 7 branch before this ternary; other tags' value is string or null.
243
+ // * _gn trailing `: ""` -- a decoded GeneralName always carries a bytes Buffer, so the
244
+ // Buffer.isBuffer(g.bytes) arm is always taken.
245
+ // * _gnRaw `if (!Buffer.isBuffer(buf)) return "";` -- the sole caller (the CRL-DP fullName loop)
246
+ // guards Buffer.isBuffer(nm) before calling _gnRaw.
247
+
220
248
  // Shared value renderers reused by more than one extension key.
221
249
  function _renderAltName(decoded, inner) {
222
250
  return inner + (decoded.names || []).map(_gn).join(", ");
@@ -254,6 +282,18 @@ function _renderCrlDp(decoded, inner) {
254
282
  return dpLines.join("\n");
255
283
  }
256
284
 
285
+ // Coverage residual -- the shared alt-name / CRL-DP render fallbacks are unreachable because the
286
+ // strict decoders already narrow the shape:
287
+ // * _renderAltName `(decoded.names || [])` -- the subjectAltName/issuerAltName decoder always
288
+ // yields a names array.
289
+ // * _renderCrlDp `(decoded || [])` -- the cRLDistributionPoints/freshestCRL decoder always
290
+ // yields an array.
291
+ // * _renderCrlDp `: _gn(nm)` -- distributionPointName surfaces fullName entries as raw
292
+ // GeneralName Buffers, so Buffer.isBuffer(nm) is always true.
293
+ // * _renderCrlDp `if (!wrote) ... "(distribution point)"` -- crlDistributionPoints throws unless
294
+ // a distributionPoint (always fullName/rdn) or cRLIssuer (sets wrote) is present, so !wrote
295
+ // never holds.
296
+
257
297
  // Declarative extension-value renderer registry: extension name -> (decoded, inner) ->
258
298
  // text. Data-driven dispatch (the schema family's "registry, not switch" shape) so a
259
299
  // new extension is a row rather than another hand-coded branch, and the set an
@@ -365,6 +405,35 @@ var EXT_RENDERERS = {
365
405
  },
366
406
  };
367
407
 
408
+ // Coverage residual -- the EXT_RENDERERS entry fallbacks are unreachable because each shared
409
+ // decoder (and asn1.read.oid) already narrows the shape before the renderer runs:
410
+ // * extKeyUsage `catch (_e) { /* unregistered EKU OID */ }` and certificatePolicies
411
+ // `catch (_e) { /* unregistered qualifier */ }` -- oid.name returns undefined (never throws)
412
+ // for a well-formed unregistered OID; it throws only on a non-dotted argument, and both OIDs
413
+ // come from asn1.read.oid (always a valid dotted OID).
414
+ // * certificatePolicies `(asn1.decode(p.qualifiersBytes).children || [])` -- asn1.decode of the
415
+ // assertPolicyQualifiers-validated qualifiers SEQUENCE always yields a children array.
416
+ // * certificatePolicies `: Buffer.alloc(0)` -- assertPolicyQualifiers requires every
417
+ // PolicyQualifierInfo to be a 2-child SEQUENCE, so pqi.children[1] is always a present node
418
+ // carrying a bytes Buffer.
419
+ // * certificatePolicies outer `catch (_e) { ... _hexColon(p.qualifiersBytes, {}) ... }` --
420
+ // certificatePolicies already validated qualifiersBytes as a SEQUENCE of 2-child PQIs each
421
+ // leading with a valid OID, so the re-decode + asn1.read.oid cannot throw.
422
+ // * policyConstraints `: inner + "(empty)"` -- policyConstraints rejects an empty SEQUENCE
423
+ // (>= 1 context field), so requireExplicitPolicy or inhibitPolicyMapping is non-null;
424
+ // pc.length is never 0.
425
+ // * SCT `(decoded.scts || [])` / `(decoded.unknownScts || [])` -- ct.parseSctList always returns
426
+ // both arrays.
427
+ // * SCT `: 0` -- every scts entry is a decoded v1 SCT with numeric version 0 (unknown-version
428
+ // SCTs go to unknownScts and are not iterated here).
429
+ // * SCT `: inner + "(empty SCT list)"` -- ct.parseSctList rejects an empty list and routes every
430
+ // SerializedSCT into scts/unknownScts, so at least one line is always emitted.
431
+ // * subjectKeyIdentifier `: (decoded.bytes || Buffer.alloc(0))` -- the subjectKeyIdentifier
432
+ // decoder returns the KeyIdentifier as a Buffer, so Buffer.isBuffer(decoded) is always true.
433
+ // * authorityKeyIdentifier `: BigInt(decoded.authorityCertSerialNumber)` -- the AKI decoder reads
434
+ // authorityCertSerialNumber via asn1.read.integerImplicit (a bigint), so typeof === "bigint"
435
+ // is always true.
436
+
368
437
  function _renderExtValue(ext, decoded, inner) {
369
438
  var fn = EXT_RENDERERS[ext.name];
370
439
  return fn ? fn(decoded, inner) : null; // no registered renderer -> caller hex-dumps
@@ -476,6 +545,8 @@ function certificate(input) {
476
545
  L.push(" Subject: " + _dnString(c.subject));
477
546
  L.push(" Subject Public Key Info:");
478
547
  L.push(_keyBlock(c.subjectPublicKeyInfo, " "));
548
+ // Coverage residual -- `c.extensions || []` is unreachable: both input paths guarantee an array
549
+ // (x509.parse yields one; the pre-parsed fast path requires Array.isArray(extensions) in _looksParsed).
479
550
  if ((c.extensions || []).length) {
480
551
  L.push(" X509v3 extensions:");
481
552
  c.extensions.forEach(function (ext) { L.push(_extension(ext, " ")); });
package/lib/jose.js CHANGED
@@ -39,6 +39,7 @@ var constants = require("./constants");
39
39
  var webcrypto = require("./webcrypto").webcrypto;
40
40
  var frameworkError = require("./framework-error");
41
41
  var guard = require("./guard-all");
42
+ var edwardsPoint = require("./edwards-point");
42
43
 
43
44
  var JoseError = frameworkError.JoseError;
44
45
  function E(code, message, cause) { return new JoseError(code, message, cause); }
@@ -267,6 +268,12 @@ function _assertKeyType(algRow, jwk) {
267
268
  if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + JSON.stringify(jwk.kty) + " does not match the algorithm");
268
269
  if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
269
270
  if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + JSON.stringify(jwk.crv));
271
+ // node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
272
+ // forged signature; reject a non-full-order Edwards point through the shared gate, the same check
273
+ // every EdDSA verify path in the toolkit applies.
274
+ if (algRow.kty === "OKP" && typeof jwk.x === "string" && !edwardsPoint.validate(b64uDecode(jwk.x), jwk.crv === "Ed25519" ? 6 : 7)) {
275
+ throw E("jose/bad-key", "the OKP public key is not a valid, full-order Edwards point");
276
+ }
270
277
  // An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
271
278
  // the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
272
279
  if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + JSON.stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
package/lib/oid.js CHANGED
@@ -284,7 +284,7 @@ var FAMILIES = {
284
284
 
285
285
  // NIST hash functions (SHA-2, SHA-3, SHAKE).
286
286
  nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
287
- sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
287
+ sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake128: 11, shake256: 12 } },
288
288
 
289
289
  // NIST signature algorithms -- FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
290
290
  // signature arc 2.16.840.1.101.3.4.3. RFC 9909 sec. 3 assigns the 12 Pure SLH-DSA