@blamejs/pki 0.2.17 → 0.2.19

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/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); }
@@ -116,7 +117,7 @@ function parseJson(input) {
116
117
  // The strict bounded reader (byte + depth caps, duplicate-member reject at every
117
118
  // depth, __proto__-safe own-property assignment, fatal UTF-8, RFC 8259 grammar)
118
119
  // lives once in the shared JSON guard; the frozen jose/* codes are threaded through.
119
- return guard.json.parse(input, JoseError, {
120
+ return guard.json.parse(input, E, {
120
121
  maxBytes: LIMITS.JSON_MAX_BYTES, maxDepth: LIMITS.JSON_MAX_DEPTH,
121
122
  badJson: "jose/bad-json", tooDeep: "jose/too-deep", duplicateMember: "jose/duplicate-member",
122
123
  tooLarge: "jose/too-large", badInput: "jose/bad-input", label: "the JSON document",
@@ -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);
@@ -40,6 +40,9 @@ var crl = require("./schema-crl");
40
40
  var ocsp = require("./schema-ocsp");
41
41
  var guard = require("./guard-all");
42
42
  var constants = require("./constants");
43
+ var validator = require("./validator-all");
44
+ var compositeSig = require("./composite-sig");
45
+ var edwardsPoint = require("./edwards-point");
43
46
 
44
47
  var PathError = errors.PathError;
45
48
  function E(code, message, cause) { return new PathError(code, message, cause); }
@@ -114,6 +117,10 @@ function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
114
117
  var entry = { verify: verify, imp: imp, params: params };
115
118
  if (ecdsa) entry.ecdsa = true;
116
119
  if (sameKeyOid) entry.sameKeyOid = true;
120
+ // EdDSA descriptors carry the Edwards curve id (6 = Ed25519, 7 = Ed448) so the verify path
121
+ // validates the issuer point through the shared gate without re-branching on the algorithm name.
122
+ if (verify.name === "Ed25519") entry.eddsa = 6;
123
+ else if (verify.name === "Ed448") entry.eddsa = 7;
117
124
  SIG_ALGS[oid.byName(name)] = entry;
118
125
  }
119
126
  // RSASSA-PKCS1-v1_5 -- parameters MUST be NULL.
@@ -151,13 +158,8 @@ HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
151
158
  HASH_BY_OID[oid.byName("sha384")] = "SHA-384";
152
159
  HASH_BY_OID[oid.byName("sha512")] = "SHA-512";
153
160
 
154
- var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
155
- // Curve group orders n -- a valid ECDSA signature has r,s in [1, n-1].
156
- var CURVE_ORDER = {
157
- "P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
158
- "P-384": BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),
159
- "P-521": BigInt("0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"),
160
- };
161
+ // The order-aware ECDSA DER->P1363 converter + its CURVE_FIELD_BYTES / CURVE_ORDER tables now
162
+ // live in validator-sig.js (validator.sig.ecdsaDerToP1363), shared with the composite engine.
161
163
 
162
164
  // The algorithm OID of an AlgorithmIdentifier SEQUENCE { algorithm OID,
163
165
  // parameters OPTIONAL }. STRICT: a universal SEQUENCE with the OID and AT MOST
@@ -261,20 +263,11 @@ function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] ===
261
263
 
262
264
  function resolveDescriptor(sigAlg) {
263
265
  if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
264
- var comp = COMPOSITE_ALGS[sigAlg.oid];
265
- if (comp) {
266
- // draft-ietf-lamps-pq-composite-sigs sec. 5.3: parameters MUST be absent.
267
- // Coverage residual -- unreachable: every sigAlg reaching resolveDescriptor is a
268
- // field of a parsed x509/CRL/OCSP structure, and the shared AlgorithmIdentifier
269
- // decoder (schema-pkix) rejects a composite OID carrying parameters at parse time
270
- // (oid.paramsMustBeAbsent), so `parameters` is always absent here. Defense in depth.
271
- if (sigAlg.parameters !== null && sigAlg.parameters !== undefined) {
272
- throw E("path/unsupported-algorithm", "composite signature algorithm parameters must be absent (draft-ietf-lamps-pq-composite-sigs sec. 5.3)");
273
- }
274
- // The composite public-key algorithm OID equals the signature OID; enforce
275
- // the RFC 9814-style key<->signature consistency structurally (sameKeyOid).
276
- return { composite: comp, sameKeyOid: true };
277
- }
266
+ // Composite ML-DSA: the OID-keyed registry + the parameters-absent check
267
+ // (draft-ietf-lamps-pq-composite-sigs sec. 5.3) live in composite-sig.js, shared with
268
+ // CMS. sameKeyOid enforces the RFC 9814 sec. 4 key<->signature OID consistency.
269
+ var comp = compositeSig.resolveCompositeDescriptor(sigAlg, PathError, "path/unsupported-algorithm");
270
+ if (comp) return comp;
278
271
  var d = SIG_ALGS[sigAlg.oid];
279
272
  if (!d) throw E("path/unsupported-algorithm", "no verify descriptor for signature algorithm " + (sigAlg.name || sigAlg.oid));
280
273
  // The signatureAlgorithm's parameters MUST match the algorithm's fixed shape:
@@ -304,177 +297,15 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
304
297
  }
305
298
  }
306
299
 
307
- // DER Ecdsa-Sig-Value SEQUENCE { r, s } -> fixed-width r||s (P1363), rejecting
308
- // r or s outside [1, n-1] (CVE-2022-21449 "Psychic Signatures").
309
- function ecdsaDerToP1363(der, curve) {
310
- var width = CURVE_FIELD_BYTES[curve];
311
- var order = CURVE_ORDER[curve];
312
- // Coverage residual -- unreachable: `curve` is a WebCrypto-imported ECDSA
313
- // namedCurve (P-256/384/521) or a composite trad.ec from the same set; all three
314
- // are present in the width/order tables, so no import yields an untabulated curve.
315
- if (!width || !order) throw E("path/bad-signature", "unsupported ECDSA curve " + curve);
316
- var n;
317
- try { n = asn1.decode(der); }
318
- catch (e) { throw E("path/bad-signature", "ECDSA signature is not a DER SEQUENCE(r,s)", e); }
319
- if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
320
- throw E("path/bad-signature", "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
321
- }
322
- var r = asn1.read.integer(n.children[0]);
323
- var s = asn1.read.integer(n.children[1]);
324
- if (r < 1n || s < 1n || r >= order || s >= order) {
325
- throw E("path/bad-signature", "ECDSA signature component out of range [1, n-1] (CVE-2022-21449)");
326
- }
327
- function pad(v) {
328
- var hex = v.toString(16);
329
- if (hex.length % 2) hex = "0" + hex;
330
- var buf = Buffer.from(hex, "hex");
331
- // Coverage residual -- unreachable: the preceding [1, order-1] range gate bounds
332
- // r and s below the curve order, which fits within `width` bytes.
333
- if (buf.length > width) throw E("path/bad-signature", "ECDSA signature component wider than the curve field");
334
- var out = Buffer.alloc(width);
335
- buf.copy(out, width - buf.length);
336
- return out;
337
- }
338
- return Buffer.concat([pad(r), pad(s)]);
339
- }
300
+ // ecdsaDerToP1363 relocated to validator-sig.js; the composite trad-ECDSA and classical ECDSA
301
+ // paths call validator.sig.ecdsaDerToP1363(sig, curve, PathError, "path/bad-signature").
340
302
 
341
303
  // ---- composite ML-DSA signatures (draft-ietf-lamps-pq-composite-sigs) -------
342
- // A composite signature pairs a post-quantum ML-DSA with a traditional RSA /
343
- // ECDSA / EdDSA. The public key is the raw concatenation mldsaPK || tradPK
344
- // (sec. 4.1) in the SPKI BIT STRING; the signature is mldsaSig || tradSig
345
- // (sec. 4.3) in the signatureValue BIT STRING. Both are surfaced RAW by the
346
- // x509 parser (no sub-walk). Verification (sec. 2): reconstruct
347
- // M' = Prefix || Label || len(ctx) || ctx || PH(M),
348
- // verify the ML-DSA component over M' with ctx = the composite Label, verify the
349
- // traditional component over M' under its own hash, and accept IFF BOTH pass
350
- // (THREAT-MODEL: all components must verify -- never an AND-to-OR downgrade). The
351
- // ML-DSA component is the fixed-length FIRST half; the split point is its length.
352
- var COMPOSITE_PREFIX = Buffer.from("CompositeAlgorithmSignatures2025", "ascii");
353
- var MLDSA_COMPONENT = {
354
- "ML-DSA-44": { pk: 1312, sig: 2420, oid: "id-ml-dsa-44" },
355
- "ML-DSA-65": { pk: 1952, sig: 3309, oid: "id-ml-dsa-65" },
356
- "ML-DSA-87": { pk: 2592, sig: 4627, oid: "id-ml-dsa-87" },
357
- };
358
- var EC_CURVE_OID = { "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1" };
359
- var COMPOSITE_ALGS = {};
360
- // _comp(name, mldsa, ph, label, trad). `trad` is exactly one component shape:
361
- // { ec, hash } | { eddsa } | { rsaPss, hash, salt } | { rsaPkcs1, hash } |
362
- // { unsupported } for the arms Node's WebCrypto surface cannot verify (brainpool
363
- // curves; the SHAKE256/64 pre-hash) -- registered + params-guarded, deferred at
364
- // verify to path/unsupported-algorithm rather than silently accepted.
365
- function _comp(name, mldsa, ph, label, trad) {
366
- var sz = MLDSA_COMPONENT[mldsa];
367
- COMPOSITE_ALGS[oid.byName(name)] = {
368
- name: name, mldsa: mldsa, mldsaPk: sz.pk, mldsaSig: sz.sig, mldsaOid: sz.oid,
369
- ph: ph, label: Buffer.from(label, "ascii"), trad: trad,
370
- };
371
- }
372
- _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 });
373
- _comp("id-MLDSA44-RSA2048-PKCS15-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PKCS15-SHA256", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 2048 });
374
- _comp("id-MLDSA44-Ed25519-SHA512", "ML-DSA-44", "SHA-512", "COMPSIG-MLDSA44-Ed25519-SHA512", { eddsa: "Ed25519" });
375
- _comp("id-MLDSA44-ECDSA-P256-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-ECDSA-P256-SHA256", { ec: "P-256", hash: "SHA-256" });
376
- _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 });
377
- _comp("id-MLDSA65-RSA3072-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 3072 });
378
- _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 });
379
- _comp("id-MLDSA65-RSA4096-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-384", rsaBits: 4096 });
380
- _comp("id-MLDSA65-ECDSA-P256-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P256-SHA512", { ec: "P-256", hash: "SHA-256" });
381
- _comp("id-MLDSA65-ECDSA-P384-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
382
- _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" });
383
- _comp("id-MLDSA65-Ed25519-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-Ed25519-SHA512", { eddsa: "Ed25519" });
384
- _comp("id-MLDSA87-ECDSA-P384-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
385
- _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" });
386
- _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" });
387
- _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 });
388
- _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 });
389
- _comp("id-MLDSA87-ECDSA-P521-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P521-SHA512", { ec: "P-521", hash: "SHA-512" });
390
-
391
- var _b = asn1.build;
392
- // Wrap a raw component public key in the SPKI its WebCrypto import expects, so
393
- // each half is verified through the SAME import + verify seam the classical path
394
- // uses (no second parallel verify path). tradPK for RSA is the RSAPublicKey DER;
395
- // for EC the uncompressed point; for EdDSA the raw public key.
396
- function _spkiFor(algNode, keyBytes) { return _b.sequence([algNode, _b.bitString(keyBytes, 0)]); }
397
- function _mldsaSpki(mldsaOid, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(mldsaOid))]), raw); }
398
- function _ecSpki(curve, point) { return _spkiFor(_b.sequence([_b.oid(oid.byName("ecPublicKey")), _b.oid(oid.byName(EC_CURVE_OID[curve]))]), point); }
399
- function _edSpki(name, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(name))]), raw); }
400
- function _rsaSpki(rsaPub) { return _spkiFor(_b.sequence([_b.oid(oid.byName("rsaEncryption")), _b.nullValue()]), rsaPub); }
401
-
402
- // The exact modulus bit length of an RSAPublicKey (SEQUENCE { modulus, exponent }).
403
- function _rsaModulusBits(rsaPubDer) {
404
- return asn1.read.integer(asn1.decode(rsaPubDer).children[0]).toString(2).length;
405
- }
406
-
407
- function _verifyTradComponent(trad, tradPK, tradSig, mprime) {
408
- if (trad.ec) {
409
- // draft sec. 5.1: the EC point MUST be the uncompressed X9.62 form (leading 0x04).
410
- // A compressed / hybrid point is a non-conforming composite component encoding.
411
- if (tradPK.length < 1 || tradPK[0] !== 0x04) return Promise.resolve(false);
412
- return subtle.importKey("spki", _ecSpki(trad.ec, tradPK), { name: "ECDSA" }, false, ["verify"]).then(function (k) {
413
- // The traditional ECDSA signature is DER Ecdsa-Sig-Value; convert to P1363
414
- // through the shared reader that also rejects r/s outside [1,n-1] (CVE-2022-21449).
415
- var p1363 = ecdsaDerToP1363(tradSig, trad.ec);
416
- return subtle.verify({ name: "ECDSA", hash: trad.hash }, k, p1363, mprime);
417
- });
418
- }
419
- if (trad.eddsa) {
420
- return subtle.importKey("spki", _edSpki(trad.eddsa, tradPK), { name: trad.eddsa }, false, ["verify"])
421
- .then(function (k) { return subtle.verify({ name: trad.eddsa }, k, tradSig, mprime); });
422
- }
423
- if (trad.rsaPss || trad.rsaPkcs1) {
424
- // The composite OID fixes the RSA modulus size: a downgraded or mismatched modulus
425
- // under the declared OID (an id-MLDSA44-RSA2048-* whose component is really 1024-bit)
426
- // is rejected BEFORE verify, so a weak RSA component cannot satisfy an arm that
427
- // promises 2048/3072/4096 bits. A malformed RSAPublicKey rejects the same way.
428
- var bits;
429
- try { bits = _rsaModulusBits(tradPK); }
430
- catch (_e) { return Promise.resolve(false); }
431
- if (bits !== trad.rsaBits) return Promise.resolve(false);
432
- if (trad.rsaPss) {
433
- return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSA-PSS", hash: trad.hash }, false, ["verify"])
434
- .then(function (k) { return subtle.verify({ name: "RSA-PSS", saltLength: trad.salt }, k, tradSig, mprime); });
435
- }
436
- return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
437
- .then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
438
- }
439
- // Coverage residual -- unreachable: compositeVerify returns early for a
440
- // trad.unsupported arm, so _verifyTradComponent runs only for arms that set
441
- // exactly one of ec/eddsa/rsaPss/rsaPkcs1; this final fallthrough is a backstop.
442
- return Promise.resolve(false);
443
- }
444
-
445
- // Verify a composite signature: `spkiBytes` the issuer SPKI DER, `sigBytes` the
446
- // raw signatureValue (mldsaSig || tradSig), `message` the signed region (tbsBytes).
447
- function compositeVerify(spkiBytes, sigBytes, message, d) {
448
- if (d.trad.unsupported) {
449
- return Promise.resolve({ ok: false, code: "path/unsupported-algorithm",
450
- error: E("path/unsupported-algorithm", "composite " + d.name + ": " + d.trad.unsupported) });
451
- }
452
- var rawKey;
453
- try {
454
- var bs = asn1.read.bitString(asn1.decode(spkiBytes).children[1]);
455
- // The composite subjectPublicKey is an octet-aligned concatenation (no unused
456
- // bits); a non-zero unused-bit count is malformed.
457
- if (bs.unusedBits !== 0) throw E("path/bad-signature", "composite subjectPublicKey has unused bits");
458
- rawKey = bs.bytes;
459
- } catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/bad-signature"), error: e }); }
460
- // The ML-DSA half is fixed-length and FIRST; the traditional half is the
461
- // remainder. Both must be non-empty for a well-formed composite.
462
- if (rawKey.length <= d.mldsaPk || sigBytes.length <= d.mldsaSig) {
463
- return Promise.resolve({ ok: false, code: "path/bad-signature",
464
- error: E("path/bad-signature", "composite key/signature shorter than the fixed ML-DSA component") });
465
- }
466
- var mldsaPK = rawKey.subarray(0, d.mldsaPk), tradPK = rawKey.subarray(d.mldsaPk);
467
- var mldsaSig = sigBytes.subarray(0, d.mldsaSig), tradSig = sigBytes.subarray(d.mldsaSig);
468
- return subtle.digest({ name: d.ph }, message).then(function (phBuf) {
469
- // M' = Prefix || Label || len(ctx)=0 || ctx="" || PH(M). In X.509 path
470
- // validation the application context is empty (a caller ctx is out of scope).
471
- var mprime = Buffer.concat([COMPOSITE_PREFIX, d.label, Buffer.from([0]), Buffer.from(phBuf)]);
472
- var mldsaP = subtle.importKey("spki", _mldsaSpki(d.mldsaOid, mldsaPK), { name: d.mldsa }, false, ["verify"])
473
- .then(function (mk) { return subtle.verify({ name: d.mldsa, context: d.label }, mk, mldsaSig, mprime); });
474
- var tradP = _verifyTradComponent(d.trad, tradPK, tradSig, mprime);
475
- return Promise.all([mldsaP, tradP]).then(function (r) { return { ok: r[0] === true && r[1] === true }; });
476
- }).catch(function (e) { return { ok: false, code: pathCode(e, "path/bad-signature"), error: e }; });
477
- }
304
+ // The composite verify/sign engine + the COMPOSITE_ALGS OID-keyed registry live in
305
+ // composite-sig.js (shared with CMS composite SignerInfo). Path validation composes it:
306
+ // resolveDescriptor (above) delegates to compositeSig.resolveCompositeDescriptor, the
307
+ // certificate + OCSP verify paths to compositeSig.compositeVerify, and
308
+ // compositeKeyUsageCheck (below) enforces the sec. 5.2 signature-only keyUsage restriction.
478
309
 
479
310
  // draft-ietf-lamps-pq-composite-sigs sec. 5.2: a certificate whose SubjectPublicKeyInfo
480
311
  // carries a composite ML-DSA OID, IF it has a keyUsage extension, MUST assert at least
@@ -500,6 +331,19 @@ function compositeKeyUsageCheck(cert) {
500
331
  return { ok: true };
501
332
  }
502
333
 
334
+ // Import a descriptor's verification key, validating an EdDSA point FIRST: node/OpenSSL import a
335
+ // low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without complaint and such a key
336
+ // verifies a forged signature. This is the ONE seam both the certificate path and the revocation
337
+ // (CRL / OCSP-response) path import through, so neither can skip the point gate -- a low-order
338
+ // issuer / responder key fails the caller closed (a rejected promise the caller maps to a bad
339
+ // verdict), never verifying a forged chain or a forged revocation.
340
+ function _importVerifyKey(spkiBytes, d) {
341
+ try {
342
+ if (d.eddsa) edwardsPoint.validateSpki(spkiBytes, d.eddsa, PathError, "path/bad-signature");
343
+ } catch (e) { return Promise.reject(e); }
344
+ return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]);
345
+ }
346
+
503
347
  // Verify cert.signatureValue over cert.tbsBytes with the working public key.
504
348
  function builtinVerify(state, cert) {
505
349
  var d;
@@ -513,12 +357,12 @@ function builtinVerify(state, cert) {
513
357
  // A composite signature verifies its ML-DSA and traditional halves and accepts
514
358
  // IFF both pass -- delegated to the composite combinator (which reuses this
515
359
  // file's ECDSA range-check + the same import/verify seam).
516
- if (d.composite) return compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite);
360
+ if (d.composite) return compositeSig.compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature");
517
361
  var key;
518
- return subtle.importKey("spki", state.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
362
+ return _importVerifyKey(state.workingPublicKey, d).then(function (k) {
519
363
  key = k;
520
364
  var sig = cert.signatureValue.bytes;
521
- if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
365
+ if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
522
366
  return subtle.verify(d.verify, key, sig, cert.tbsBytes);
523
367
  }).then(function (ok) {
524
368
  return { ok: ok === true };
@@ -1365,7 +1209,7 @@ async function validate(path, opts) {
1365
1209
  // draft-ietf-lamps-pq-composite-sigs sec. 5.2: a composite-keyed certificate's
1366
1210
  // keyUsage must be signature-only (no dual-usage). Runs for the target AND every
1367
1211
  // intermediate whose own subject key is composite.
1368
- if (COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1212
+ if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1369
1213
  var cku = compositeKeyUsageCheck(cert);
1370
1214
  checks.push({ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
1371
1215
  if (!cku.ok) failed = true;
@@ -1946,10 +1790,10 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
1946
1790
  // A composite-signed CRL / OCSP response verifies through the same combinator
1947
1791
  // (both halves must pass) that the certificate path uses -- one composite verify,
1948
1792
  // never a second parallel one.
1949
- if (d.composite) return compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite).then(function (r) { return r.ok === true; });
1950
- return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]).then(function (key) {
1793
+ if (d.composite) return compositeSig.compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature").then(function (r) { return r.ok === true; });
1794
+ return _importVerifyKey(spkiBytes, d).then(function (key) {
1951
1795
  var sig = rawSig;
1952
- if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
1796
+ if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
1953
1797
  return subtle.verify(d.verify, key, sig, tbsBytes);
1954
1798
  }).then(function (ok) { return ok === true; }, function () { return false; });
1955
1799
  }
@@ -2113,7 +1957,7 @@ async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits
2113
1957
  // A composite-keyed delegate is an out-of-path signer cert and gets the same
2114
1958
  // composite keyUsage gate the path certificates do (draft sec. 5.2): a dual-usage
2115
1959
  // composite responder key (a forbidden encryption bit set) is not authorized.
2116
- if (COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
1960
+ if (compositeSig.COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
2117
1961
  return ocspResponderSpki(rc, issuer);
2118
1962
  }
2119
1963
  return null;
package/lib/schema-all.js CHANGED
@@ -301,7 +301,7 @@ module.exports = {
301
301
  pkcs12: { parse: pkcs12.parse, pemDecode: pkcs12.pemDecode, pemEncode: pkcs12.pemEncode },
302
302
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
303
303
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode, pemEncode: ocsp.pemEncode },
304
- tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
304
+ tsp: { parse: tsp.parse, parseResponse: tsp.parseResponse, parseRequest: tsp.parseRequest, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
305
305
  crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
306
306
  cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
307
307
  csrattrs: { parse: csrattrs.parse },