@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.
- package/CHANGELOG.md +28 -1
- package/README.md +4 -4
- package/lib/cms-sign.js +77 -36
- package/lib/cms-verify.js +103 -24
- package/lib/composite-sig.js +240 -0
- package/lib/edwards-point.js +20 -1
- package/lib/jose.js +8 -1
- package/lib/path-validate.js +42 -198
- package/lib/schema-all.js +1 -1
- package/lib/schema-tsp.js +68 -0
- package/lib/sigstore.js +1 -1
- package/lib/tsp-sign.js +523 -1
- package/lib/validator-sig.js +48 -1
- package/lib/webcrypto.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -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
|
+
};
|
package/lib/edwards-point.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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);
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
-
|
|
155
|
-
//
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
//
|
|
308
|
-
//
|
|
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
|
-
//
|
|
343
|
-
//
|
|
344
|
-
// (
|
|
345
|
-
//
|
|
346
|
-
//
|
|
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
|
|
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
|
|
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 },
|