@blamejs/pki 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +1 -1
- package/lib/asn1-der.js +4 -0
- package/lib/cms-sign.js +97 -39
- package/lib/cms-verify.js +134 -35
- package/lib/composite-sig.js +240 -0
- package/lib/edwards-point.js +20 -1
- package/lib/inspect.js +71 -0
- package/lib/jose.js +7 -0
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +64 -186
- package/lib/schema-cms.js +19 -0
- package/lib/sigstore.js +6 -0
- package/lib/validator-cose.js +8 -0
- package/lib/validator-sig.js +48 -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/inspect.js
CHANGED
|
@@ -63,6 +63,12 @@ function _hexColon(buf, opts) {
|
|
|
63
63
|
return lines.join("\n");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Coverage residual -- two _hexColon default arms are unreachable through the public API:
|
|
67
|
+
// * `opts = opts || {}` -- all call sites pass an explicit opts object literal, so the
|
|
68
|
+
// `|| {}` default never fires.
|
|
69
|
+
// * `" ".repeat(opts.indent || 0)` -- every wrap-mode caller passes a positive indent
|
|
70
|
+
// (pad.length + 8 >= 8, or inner.length == 16), so `opts.indent` is never falsy.
|
|
71
|
+
|
|
66
72
|
// Control-byte neutralization for a GeneralName string value routes through the
|
|
67
73
|
// guard family (the guard-shape-reinlined detector protects the shape).
|
|
68
74
|
// @guard-via guard\.name\.escape
|
|
@@ -217,6 +223,28 @@ function _gnRaw(buf) {
|
|
|
217
223
|
} catch (_e) { return _hexColon(buf, {}); }
|
|
218
224
|
}
|
|
219
225
|
|
|
226
|
+
// Coverage residual -- the GeneralName render fallbacks are unreachable because the strict
|
|
227
|
+
// generalName decoder and each caller already narrow the shape (the _gnRaw catch above is
|
|
228
|
+
// separately documented):
|
|
229
|
+
// * _ipString `if (!Buffer.isBuffer(buf)) return "";` -- every caller passes a Buffer (the
|
|
230
|
+
// recursion slices a Buffer, _gn guards Buffer.isBuffer(g.value), _gnRaw passes
|
|
231
|
+
// asn1.decode(...).content).
|
|
232
|
+
// * _ipString trailing `return _hexColon(buf, {});` -- generalName enforces iPAddress to
|
|
233
|
+
// 4/16 octets (8/32 for a name-constraints subtree base), so length is only ever 4/8/16/32.
|
|
234
|
+
// * _gn `if (!g || typeof g !== "object") return "";` -- every _gn call maps a decoder-produced
|
|
235
|
+
// GeneralName object; the decoders never emit a null element.
|
|
236
|
+
// * _gn directoryName `: ((g.value && g.value.dn) || "")` -- a decoded directoryName [4] always
|
|
237
|
+
// carries a Name with an rdns array, so the rdns arm is always taken.
|
|
238
|
+
// * _gn otherName `: "<unsupported>"` -- a decoded GeneralName always carries its raw bytes TLV.
|
|
239
|
+
// * _gn `|| ("tag" + t)` -- GeneralName CHOICE tags are 0..8, all mapped in NAMES.GENERAL_NAME;
|
|
240
|
+
// generalName rejects a tag outside 0..8.
|
|
241
|
+
// * _gn `? _hexColon(g.value, {})` -- iPAddress [7] is the only choice whose decoded value is a
|
|
242
|
+
// Buffer, handled at the t === 7 branch before this ternary; other tags' value is string or null.
|
|
243
|
+
// * _gn trailing `: ""` -- a decoded GeneralName always carries a bytes Buffer, so the
|
|
244
|
+
// Buffer.isBuffer(g.bytes) arm is always taken.
|
|
245
|
+
// * _gnRaw `if (!Buffer.isBuffer(buf)) return "";` -- the sole caller (the CRL-DP fullName loop)
|
|
246
|
+
// guards Buffer.isBuffer(nm) before calling _gnRaw.
|
|
247
|
+
|
|
220
248
|
// Shared value renderers reused by more than one extension key.
|
|
221
249
|
function _renderAltName(decoded, inner) {
|
|
222
250
|
return inner + (decoded.names || []).map(_gn).join(", ");
|
|
@@ -254,6 +282,18 @@ function _renderCrlDp(decoded, inner) {
|
|
|
254
282
|
return dpLines.join("\n");
|
|
255
283
|
}
|
|
256
284
|
|
|
285
|
+
// Coverage residual -- the shared alt-name / CRL-DP render fallbacks are unreachable because the
|
|
286
|
+
// strict decoders already narrow the shape:
|
|
287
|
+
// * _renderAltName `(decoded.names || [])` -- the subjectAltName/issuerAltName decoder always
|
|
288
|
+
// yields a names array.
|
|
289
|
+
// * _renderCrlDp `(decoded || [])` -- the cRLDistributionPoints/freshestCRL decoder always
|
|
290
|
+
// yields an array.
|
|
291
|
+
// * _renderCrlDp `: _gn(nm)` -- distributionPointName surfaces fullName entries as raw
|
|
292
|
+
// GeneralName Buffers, so Buffer.isBuffer(nm) is always true.
|
|
293
|
+
// * _renderCrlDp `if (!wrote) ... "(distribution point)"` -- crlDistributionPoints throws unless
|
|
294
|
+
// a distributionPoint (always fullName/rdn) or cRLIssuer (sets wrote) is present, so !wrote
|
|
295
|
+
// never holds.
|
|
296
|
+
|
|
257
297
|
// Declarative extension-value renderer registry: extension name -> (decoded, inner) ->
|
|
258
298
|
// text. Data-driven dispatch (the schema family's "registry, not switch" shape) so a
|
|
259
299
|
// new extension is a row rather than another hand-coded branch, and the set an
|
|
@@ -365,6 +405,35 @@ var EXT_RENDERERS = {
|
|
|
365
405
|
},
|
|
366
406
|
};
|
|
367
407
|
|
|
408
|
+
// Coverage residual -- the EXT_RENDERERS entry fallbacks are unreachable because each shared
|
|
409
|
+
// decoder (and asn1.read.oid) already narrows the shape before the renderer runs:
|
|
410
|
+
// * extKeyUsage `catch (_e) { /* unregistered EKU OID */ }` and certificatePolicies
|
|
411
|
+
// `catch (_e) { /* unregistered qualifier */ }` -- oid.name returns undefined (never throws)
|
|
412
|
+
// for a well-formed unregistered OID; it throws only on a non-dotted argument, and both OIDs
|
|
413
|
+
// come from asn1.read.oid (always a valid dotted OID).
|
|
414
|
+
// * certificatePolicies `(asn1.decode(p.qualifiersBytes).children || [])` -- asn1.decode of the
|
|
415
|
+
// assertPolicyQualifiers-validated qualifiers SEQUENCE always yields a children array.
|
|
416
|
+
// * certificatePolicies `: Buffer.alloc(0)` -- assertPolicyQualifiers requires every
|
|
417
|
+
// PolicyQualifierInfo to be a 2-child SEQUENCE, so pqi.children[1] is always a present node
|
|
418
|
+
// carrying a bytes Buffer.
|
|
419
|
+
// * certificatePolicies outer `catch (_e) { ... _hexColon(p.qualifiersBytes, {}) ... }` --
|
|
420
|
+
// certificatePolicies already validated qualifiersBytes as a SEQUENCE of 2-child PQIs each
|
|
421
|
+
// leading with a valid OID, so the re-decode + asn1.read.oid cannot throw.
|
|
422
|
+
// * policyConstraints `: inner + "(empty)"` -- policyConstraints rejects an empty SEQUENCE
|
|
423
|
+
// (>= 1 context field), so requireExplicitPolicy or inhibitPolicyMapping is non-null;
|
|
424
|
+
// pc.length is never 0.
|
|
425
|
+
// * SCT `(decoded.scts || [])` / `(decoded.unknownScts || [])` -- ct.parseSctList always returns
|
|
426
|
+
// both arrays.
|
|
427
|
+
// * SCT `: 0` -- every scts entry is a decoded v1 SCT with numeric version 0 (unknown-version
|
|
428
|
+
// SCTs go to unknownScts and are not iterated here).
|
|
429
|
+
// * SCT `: inner + "(empty SCT list)"` -- ct.parseSctList rejects an empty list and routes every
|
|
430
|
+
// SerializedSCT into scts/unknownScts, so at least one line is always emitted.
|
|
431
|
+
// * subjectKeyIdentifier `: (decoded.bytes || Buffer.alloc(0))` -- the subjectKeyIdentifier
|
|
432
|
+
// decoder returns the KeyIdentifier as a Buffer, so Buffer.isBuffer(decoded) is always true.
|
|
433
|
+
// * authorityKeyIdentifier `: BigInt(decoded.authorityCertSerialNumber)` -- the AKI decoder reads
|
|
434
|
+
// authorityCertSerialNumber via asn1.read.integerImplicit (a bigint), so typeof === "bigint"
|
|
435
|
+
// is always true.
|
|
436
|
+
|
|
368
437
|
function _renderExtValue(ext, decoded, inner) {
|
|
369
438
|
var fn = EXT_RENDERERS[ext.name];
|
|
370
439
|
return fn ? fn(decoded, inner) : null; // no registered renderer -> caller hex-dumps
|
|
@@ -476,6 +545,8 @@ function certificate(input) {
|
|
|
476
545
|
L.push(" Subject: " + _dnString(c.subject));
|
|
477
546
|
L.push(" Subject Public Key Info:");
|
|
478
547
|
L.push(_keyBlock(c.subjectPublicKeyInfo, " "));
|
|
548
|
+
// Coverage residual -- `c.extensions || []` is unreachable: both input paths guarantee an array
|
|
549
|
+
// (x509.parse yields one; the pre-parsed fast path requires Array.isArray(extensions) in _looksParsed).
|
|
479
550
|
if ((c.extensions || []).length) {
|
|
480
551
|
L.push(" X509v3 extensions:");
|
|
481
552
|
c.extensions.forEach(function (ext) { L.push(_extension(ext, " ")); });
|
package/lib/jose.js
CHANGED
|
@@ -39,6 +39,7 @@ var constants = require("./constants");
|
|
|
39
39
|
var webcrypto = require("./webcrypto").webcrypto;
|
|
40
40
|
var frameworkError = require("./framework-error");
|
|
41
41
|
var guard = require("./guard-all");
|
|
42
|
+
var edwardsPoint = require("./edwards-point");
|
|
42
43
|
|
|
43
44
|
var JoseError = frameworkError.JoseError;
|
|
44
45
|
function E(code, message, cause) { return new JoseError(code, message, cause); }
|
|
@@ -267,6 +268,12 @@ function _assertKeyType(algRow, jwk) {
|
|
|
267
268
|
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + JSON.stringify(jwk.kty) + " does not match the algorithm");
|
|
268
269
|
if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
|
|
269
270
|
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + JSON.stringify(jwk.crv));
|
|
271
|
+
// node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
|
|
272
|
+
// forged signature; reject a non-full-order Edwards point through the shared gate, the same check
|
|
273
|
+
// every EdDSA verify path in the toolkit applies.
|
|
274
|
+
if (algRow.kty === "OKP" && typeof jwk.x === "string" && !edwardsPoint.validate(b64uDecode(jwk.x), jwk.crv === "Ed25519" ? 6 : 7)) {
|
|
275
|
+
throw E("jose/bad-key", "the OKP public key is not a valid, full-order Edwards point");
|
|
276
|
+
}
|
|
270
277
|
// An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
|
|
271
278
|
// the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
|
|
272
279
|
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + JSON.stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
|
package/lib/oid.js
CHANGED
|
@@ -284,7 +284,7 @@ var FAMILIES = {
|
|
|
284
284
|
|
|
285
285
|
// NIST hash functions (SHA-2, SHA-3, SHAKE).
|
|
286
286
|
nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
|
|
287
|
-
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
|
|
287
|
+
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake128: 11, shake256: 12 } },
|
|
288
288
|
|
|
289
289
|
// NIST signature algorithms -- FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
|
|
290
290
|
// signature arc 2.16.840.1.101.3.4.3. RFC 9909 sec. 3 assigns the 12 Pure SLH-DSA
|