@blamejs/pki 0.6.13 → 0.6.15
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 +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/composite-kem.js
CHANGED
|
@@ -19,15 +19,6 @@
|
|
|
19
19
|
* @spec draft-ietf-lamps-pq-composite-kem
|
|
20
20
|
* @card Establish a shared secret with a hybrid post-quantum plus traditional KEM.
|
|
21
21
|
*/
|
|
22
|
-
//
|
|
23
|
-
// ML-KEM runs on the toolkit's hardened WebCrypto surface (the FIPS 203 ciphertext-length check +
|
|
24
|
-
// shared-secret zeroization ride along); the traditional component uses node:crypto directly, as the
|
|
25
|
-
// sibling KEM module lib/hpke.js does, promoting RSA-OAEP into a KEM (a fresh random secret encrypted,
|
|
26
|
-
// draft sec. 2.1) and ECDH / X25519 / X448 into a KEM (an ephemeral public key as the ciphertext,
|
|
27
|
-
// draft sec. 2.2). The public key is mlkemEK || tradPK (sec. 4.1) in the SPKI BIT STRING; the private
|
|
28
|
-
// key is mlkemSeed || tradSK (sec. 4.2, a 64-byte ML-KEM seed) in the PKCS#8 privateKey OCTET STRING;
|
|
29
|
-
// the ciphertext is mlkemCT || tradCT (sec. 4.3). The ML-KEM component is the fixed-length first half,
|
|
30
|
-
// and the split point is its parameter-set length.
|
|
31
22
|
|
|
32
23
|
var asn1 = require("./asn1-der");
|
|
33
24
|
var oid = require("./oid");
|
|
@@ -43,12 +34,6 @@ var KemError = frameworkError.KemError;
|
|
|
43
34
|
var PemError = frameworkError.PemError;
|
|
44
35
|
function _err(code, message, cause) { return new KemError(code, message, cause); }
|
|
45
36
|
|
|
46
|
-
// Accept a composite key as DER (any BufferSource, snapshotted at the door) or as a PEM block with the
|
|
47
|
-
// given label. A PEM decode fault is re-typed into the module's own KemError so the verb's documented
|
|
48
|
-
// "throws a typed KemError" contract holds for every input shape. PEM is detected with the shared armor
|
|
49
|
-
// check (so leading text before "-----BEGIN" is tolerated as elsewhere in the toolkit), and the byte
|
|
50
|
-
// source is snapshot-COPIED first: the async verbs read the key across await points, so it must not alias
|
|
51
|
-
// a caller buffer that can change underneath them (guard.bytes.source, which coerceToDer uses, aliases).
|
|
52
37
|
function _toDer(input, pemLabel, what) {
|
|
53
38
|
if (typeof input === "string") return _pemToDer(input, pemLabel);
|
|
54
39
|
var buf = guard.bytes.snapshotSource(input, KemError, "kem/bad-input", what);
|
|
@@ -60,17 +45,6 @@ function _pemToDer(text, label) {
|
|
|
60
45
|
catch (e) { throw _err("kem/bad-key", "the PEM input is not a valid " + label + " block", e); }
|
|
61
46
|
}
|
|
62
47
|
|
|
63
|
-
// ---- registry (draft sec. 6) -------------------------------------------------
|
|
64
|
-
//
|
|
65
|
-
// One row per composite algorithm, keyed by the composite OID. `trad` is exactly one component shape,
|
|
66
|
-
// each carrying `ctLen`, the exact byte length of its traditional ciphertext component (RSA-OAEP block,
|
|
67
|
-
// EC uncompressed point, or raw OKP ephemeral key), so the composite ciphertext length is fixed and an
|
|
68
|
-
// oversized attacker-controlled component is refused before any key is built:
|
|
69
|
-
// { kind: "rsa", bits, ctLen } RSA-OAEP-KEM, OAEP SHA-256 / MGF1-SHA256 / empty label (sec. 6.1)
|
|
70
|
-
// { kind: "ec", node, curve, ctLen } ECDH DHKEM, `node` the node namedCurve, `curve` the curve OID name
|
|
71
|
-
// { kind: "okp", curve, ctLen } X25519 / X448 DHKEM, `curve` the registered OID name
|
|
72
|
-
// `label` is stored as the AUTHORITATIVE per-row bytes (draft sec. 6): most are ASCII, but the
|
|
73
|
-
// id-MLKEM768-X25519 label is the raw six bytes 5c2e2f2f5e5c and MUST NOT be derived from the name.
|
|
74
48
|
var COMPOSITE_KEM = {};
|
|
75
49
|
function _comp(name, mlkemBits, label, trad) {
|
|
76
50
|
var mlOidName = "id-ml-kem-" + mlkemBits;
|
|
@@ -93,14 +67,9 @@ _comp("id-MLKEM1024-ECDH-brainpoolP384r1-SHA3-256", 1024, Buffer.from("MLKEM1024
|
|
|
93
67
|
_comp("id-MLKEM1024-X448-SHA3-256", 1024, Buffer.from("MLKEM1024-X448", "ascii"), { kind: "okp", curve: "X448", ctLen: 56 });
|
|
94
68
|
_comp("id-MLKEM1024-ECDH-P521-SHA3-256", 1024, Buffer.from("MLKEM1024-P521", "ascii"), { kind: "ec", node: "secp521r1", curve: "secp521r1", ctLen: 133 });
|
|
95
69
|
|
|
96
|
-
var MLKEM_SEED = 64;
|
|
97
|
-
var RSA_SS = 32;
|
|
70
|
+
var MLKEM_SEED = 64;
|
|
71
|
+
var RSA_SS = 32;
|
|
98
72
|
|
|
99
|
-
// ---- ML-KEM component (toolkit WebCrypto) ------------------------------------
|
|
100
|
-
//
|
|
101
|
-
// The private key is the 64-byte seed; node reconstructs the decapsulation key from a PKCS#8 whose
|
|
102
|
-
// privateKey OCTET STRING wraps the seed as a context-[0] primitive (the OpenSSL / draft seed form),
|
|
103
|
-
// not from node's own { seed } keygen option, whose seed convention differs.
|
|
104
73
|
function _mlkemSeedPkcs8(d, seed) {
|
|
105
74
|
return _b.sequence([
|
|
106
75
|
_b.integer(0n),
|
|
@@ -115,33 +84,17 @@ function _mlkemEncaps(d, mlkemEK) {
|
|
|
115
84
|
return subtle.importKey("spki", _mlkemSpki(d, mlkemEK), { name: d.mlkemWc }, false, ["encapsulateBits"])
|
|
116
85
|
.then(function (k) { return subtle.encapsulateBits({ name: d.mlkemWc }, k); })
|
|
117
86
|
.then(function (r) { return { ss: Buffer.from(r.sharedKey), ct: Buffer.from(r.ciphertext) }; })
|
|
118
|
-
// a malformed ML-KEM public-key half surfaces from the engine as a WebCryptoError; re-type it into
|
|
119
|
-
// the module's own KemError so the verb's typed-error contract holds. The chain runs only the
|
|
120
|
-
// WebCrypto component operations, which never raise a KemError, so the re-type is unconditional.
|
|
121
87
|
.catch(function (e) { throw _err("kem/bad-key", "the ML-KEM component public key is invalid", e); });
|
|
122
88
|
}
|
|
123
89
|
function _mlkemDecaps(d, seed, mlkemCT) {
|
|
124
90
|
return subtle.importKey("pkcs8", _mlkemSeedPkcs8(d, seed), { name: d.mlkemWc }, false, ["decapsulateBits"])
|
|
125
91
|
.then(function (k) { return subtle.decapsulateBits({ name: d.mlkemWc }, k, mlkemCT); })
|
|
126
92
|
.then(function (ss) { return Buffer.from(ss); })
|
|
127
|
-
// Defensive re-type mirroring _mlkemEncaps. Verified-unreachable from the shipped decapsulate path:
|
|
128
|
-
// the caller-checked split always supplies a valid 64-byte ML-KEM seed (any 64 bytes are a valid
|
|
129
|
-
// key) and an exact-parameter-set-length ML-KEM ciphertext, and ML-KEM decapsulation is implicitly
|
|
130
|
-
// rejecting, so neither the import nor the decapsulation of the ML-KEM half can fail here. Kept for
|
|
131
|
-
// node's experimental ML-KEM surface and for symmetry with the reachable encapsulate catch.
|
|
132
93
|
.catch(function (e) { throw _err("kem/bad-key", "the ML-KEM component private key is invalid", e); });
|
|
133
94
|
}
|
|
134
95
|
|
|
135
|
-
// ---- traditional component (node:crypto, like lib/hpke.js) -------------------
|
|
136
|
-
//
|
|
137
|
-
// The traditional public key encodings mixed into the combiner (draft sec. 4.1 / Appendix B): RSA is
|
|
138
|
-
// the RSAPublicKey DER, EC is the uncompressed X9.62 point, X25519 / X448 is the raw curve point.
|
|
139
96
|
function _ecPubFromPoint(d, point) {
|
|
140
|
-
// draft Appendix B: the EC component is the uncompressed X9.62 point (0x04 || X || Y). node would
|
|
141
|
-
// silently decompress a 0x02/0x03 point, so a compressed or hybrid form is refused here as non-canonical;
|
|
142
|
-
// createPublicKey then validates the length and that the point is on the curve.
|
|
143
97
|
if (point.length < 1 || point[0] !== 0x04) throw _err("kem/bad-key", "the EC component point is not in uncompressed form");
|
|
144
|
-
// SubjectPublicKeyInfo { AlgorithmIdentifier { ecPublicKey, namedCurve }, BIT STRING point }.
|
|
145
98
|
var spki = _b.sequence([_b.sequence([_b.oid(oid.byName("ecPublicKey")), _b.oid(oid.byName(d.trad.curve))]), _b.bitString(point, 0)]);
|
|
146
99
|
return nodeCrypto.createPublicKey({ key: spki, format: "der", type: "spki" });
|
|
147
100
|
}
|
|
@@ -153,8 +106,6 @@ function _okpPrivFromRaw(d, raw) {
|
|
|
153
106
|
var p8 = _b.sequence([_b.integer(0n), _b.sequence([_b.oid(oid.byName(d.trad.curve))]), _b.octetString(_b.octetString(raw))]);
|
|
154
107
|
return nodeCrypto.createPrivateKey({ key: p8, format: "der", type: "pkcs8" });
|
|
155
108
|
}
|
|
156
|
-
// The subjectPublicKey octets of a public KeyObject (the uncompressed EC point / raw OKP key), read
|
|
157
|
-
// off the SPKI export rather than a JWK export, which node cannot produce for the brainpool curves.
|
|
158
109
|
function _rawPubOf(keyObject) {
|
|
159
110
|
var spki = keyObject.export({ format: "der", type: "spki" });
|
|
160
111
|
return Buffer.from(asn1.read.bitString(asn1.decode(spki).children[1]).bytes);
|
|
@@ -165,8 +116,6 @@ function _tradEncaps(d, tradPK) {
|
|
|
165
116
|
if (trad.kind === "rsa") {
|
|
166
117
|
var pub;
|
|
167
118
|
try {
|
|
168
|
-
// node's createPublicKey ignores trailing bytes after the RSAPublicKey; decode strictly first so a
|
|
169
|
-
// composite key carrying extra bytes after the RSA component is refused (draft sec. 4.1: exact encoding).
|
|
170
119
|
asn1.decode(tradPK);
|
|
171
120
|
pub = nodeCrypto.createPublicKey({ key: tradPK, format: "der", type: "pkcs1" });
|
|
172
121
|
} catch (e) { return Promise.reject(_err("kem/bad-key", "the RSA component public key is not a valid RSAPublicKey", e)); }
|
|
@@ -175,18 +124,11 @@ function _tradEncaps(d, tradPK) {
|
|
|
175
124
|
var ct;
|
|
176
125
|
try { ct = Buffer.from(nodeCrypto.publicEncrypt({ key: pub, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, ss)); }
|
|
177
126
|
catch (e) {
|
|
178
|
-
// the random secret is already allocated; wipe it before rejecting if the RSA public operation fails
|
|
179
|
-
// (draft sec. 3.5). On success it is returned and wiped by the combiner.
|
|
180
127
|
guard.secret.zeroize(ss, KemError, "kem/bad-input", "the RSA-OAEP component secret");
|
|
181
128
|
return Promise.reject(_err("kem/bad-key", "the RSA-OAEP component encryption failed", e));
|
|
182
129
|
}
|
|
183
130
|
return Promise.resolve({ ss: ss, ct: ct });
|
|
184
131
|
}
|
|
185
|
-
// DHKEM: an ephemeral keypair, the shared secret is DH(ephemeral, recipient), the ciphertext is the
|
|
186
|
-
// ephemeral public key in the component's own encoding (draft sec. 2.2). The Diffie-Hellman is inside
|
|
187
|
-
// the try: a structurally valid but low-order recipient point (e.g. an all-zero X25519 key) imports
|
|
188
|
-
// cleanly but makes node's diffieHellman throw, which must surface as a typed fault rather than a raw
|
|
189
|
-
// OpenSSL error escaping the encapsulation.
|
|
190
132
|
var dh, dhCt;
|
|
191
133
|
try {
|
|
192
134
|
var recip = trad.kind === "ec" ? _ecPubFromPoint(d, tradPK) : _okpPubFromRaw(d, tradPK);
|
|
@@ -202,36 +144,25 @@ function _tradDecaps(d, tradSK, tradCT) {
|
|
|
202
144
|
if (trad.kind === "rsa") {
|
|
203
145
|
var priv;
|
|
204
146
|
try {
|
|
205
|
-
// node's createPrivateKey ignores trailing bytes after the RSAPrivateKey; decode strictly first so a
|
|
206
|
-
// composite key carrying extra bytes after the RSA component is refused (draft sec. 4.2: exact encoding).
|
|
207
147
|
asn1.decode(tradSK);
|
|
208
148
|
priv = nodeCrypto.createPrivateKey({ key: tradSK, format: "der", type: "pkcs1" });
|
|
209
149
|
} catch (e) { return Promise.reject(_err("kem/bad-key", "the RSA component private key is not a valid RSAPrivateKey", e)); }
|
|
210
150
|
if (priv.asymmetricKeyDetails.modulusLength !== trad.bits) return Promise.reject(_err("kem/bad-key", "the RSA component modulus is not " + trad.bits + " bits (draft sec. 6)"));
|
|
211
151
|
var ss;
|
|
212
|
-
// OAEP-invalid ciphertext and valid-padding-wrong-length plaintext MUST fail identically: a distinct
|
|
213
|
-
// verdict for either is an RSA-OAEP padding-validity oracle (Manger). Both reject with the same code
|
|
214
|
-
// and message AND no cause (the OpenSSL error is dropped, since it too would tell the paths apart), so
|
|
215
|
-
// the caller cannot tell the failure reason apart. draft sec. 2.1 fixes the transported secret at 32
|
|
216
|
-
// bytes; a plaintext of any other length is wiped and rejected here with that same verdict (sec. 3.5).
|
|
217
152
|
try { ss = Buffer.from(nodeCrypto.privateDecrypt({ key: priv, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, tradCT)); }
|
|
218
153
|
catch (_e) { return Promise.reject(_err("kem/decapsulation-failed", "the RSA-OAEP component decryption failed")); }
|
|
219
154
|
if (ss.length !== RSA_SS) {
|
|
220
155
|
guard.secret.zeroize(ss, KemError, "kem/bad-input", "the RSA-OAEP component secret");
|
|
221
156
|
return Promise.reject(_err("kem/decapsulation-failed", "the RSA-OAEP component decryption failed"));
|
|
222
157
|
}
|
|
223
|
-
// The combiner input tradPK is the RSAPublicKey DER (n, e), draft sec. 4.1, taken from the key object.
|
|
224
158
|
return Promise.resolve({ ss: ss, tradPK: Buffer.from(nodeCrypto.createPublicKey(priv).export({ format: "der", type: "pkcs1" })) });
|
|
225
159
|
}
|
|
226
160
|
var priv2, eph;
|
|
227
161
|
try {
|
|
228
162
|
if (trad.kind === "ec") {
|
|
229
|
-
// node's SEC1 reader ignores trailing bytes after the ECPrivateKey; decode strictly first so a
|
|
230
|
-
// composite key carrying extra bytes after the EC component is refused (draft sec. 4.2, exact encoding).
|
|
231
163
|
asn1.decode(tradSK);
|
|
232
164
|
priv2 = nodeCrypto.createPrivateKey({ key: tradSK, format: "der", type: "sec1" });
|
|
233
165
|
} else {
|
|
234
|
-
// the OKP private key is a fixed-length raw scalar; node rejects any wrong length, trailing included.
|
|
235
166
|
priv2 = _okpPrivFromRaw(d, tradSK);
|
|
236
167
|
}
|
|
237
168
|
eph = trad.kind === "ec" ? _ecPubFromPoint(d, tradCT) : _okpPubFromRaw(d, tradCT);
|
|
@@ -242,23 +173,15 @@ function _tradDecaps(d, tradSK, tradCT) {
|
|
|
242
173
|
return Promise.resolve({ ss: dh, tradPK: _rawPubOf(nodeCrypto.createPublicKey(priv2)) });
|
|
243
174
|
}
|
|
244
175
|
|
|
245
|
-
// ---- combiner (draft sec. 3.4) -----------------------------------------------
|
|
246
176
|
function _combine(d, mlkemSS, tradSS, tradCT, tradPK) {
|
|
247
177
|
var preimage = Buffer.concat([mlkemSS, tradSS, tradCT, tradPK, d.label]);
|
|
248
178
|
return subtle.digest("SHA3-256", preimage).then(function (h) {
|
|
249
179
|
return Buffer.from(h);
|
|
250
180
|
}).finally(function () {
|
|
251
|
-
// draft sec. 3.5: clear the intermediate key material on every path, whether the digest resolves or
|
|
252
|
-
// fails. Only the returned composite secret survives. tradCT / tradPK are public, but the preimage
|
|
253
|
-
// holds copies of both component shared secrets, and mlkemSS / tradSS are the component secrets.
|
|
254
181
|
guard.secret.zeroizeAll([preimage, mlkemSS, tradSS], KemError, "kem/bad-input", "a KEM combiner secret");
|
|
255
182
|
});
|
|
256
183
|
}
|
|
257
184
|
|
|
258
|
-
// Run the two component operations; if EITHER rejects, wipe the shared secret the OTHER already produced
|
|
259
|
-
// before re-throwing, so a component failure leaves no key material readable (draft sec. 3.5 -- clear all
|
|
260
|
-
// buffers and forward the error). On success the combiner wipes both component secrets. `secA` / `secB`
|
|
261
|
-
// extract the secret Buffer from each component's result.
|
|
262
185
|
async function _settleOrWipe(pA, pB, secA, secB) {
|
|
263
186
|
var s = await Promise.allSettled([pA, pB]);
|
|
264
187
|
if (s[0].status === "rejected" || s[1].status === "rejected") {
|
|
@@ -269,25 +192,18 @@ async function _settleOrWipe(pA, pB, secA, secB) {
|
|
|
269
192
|
return [s[0].value, s[1].value];
|
|
270
193
|
}
|
|
271
194
|
|
|
272
|
-
// ---- descriptor resolution ---------------------------------------------------
|
|
273
195
|
function _descriptor(algOid) {
|
|
274
196
|
var d = COMPOSITE_KEM[algOid];
|
|
275
197
|
if (!d) throw _err("kem/unsupported-algorithm", "not a composite ML-KEM algorithm: " + (oid.name(algOid) || algOid));
|
|
276
198
|
return d;
|
|
277
199
|
}
|
|
278
|
-
// Resolve the composite descriptor from an algorithm OID, enforcing the draft sec. 5.3 parameters-absent
|
|
279
|
-
// rule. Both verbs feed the same (oid, hasParams) here so the dispatch and the parameters rule live once,
|
|
280
|
-
// whether the AlgorithmIdentifier was read from a hand-decoded SPKI or a schema-parsed OneAsymmetricKey.
|
|
281
200
|
function _descriptorForAlg(algOid, hasParams) {
|
|
282
201
|
if (hasParams) throw _err("kem/bad-algorithm", "a composite ML-KEM AlgorithmIdentifier MUST have absent parameters (draft sec. 5.3)");
|
|
283
202
|
return _descriptor(algOid);
|
|
284
203
|
}
|
|
285
|
-
// A DER node is a universal SEQUENCE (SubjectPublicKeyInfo and AlgorithmIdentifier are both SEQUENCE, not
|
|
286
|
-
// SET or any other constructed tag): a SET carrying the same children is not a canonical encoding.
|
|
287
204
|
function _isSequence(node) {
|
|
288
205
|
return !!node && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.SEQUENCE && !!node.children;
|
|
289
206
|
}
|
|
290
|
-
// Resolve the composite descriptor from a raw AlgorithmIdentifier node (the SPKI encapsulation path).
|
|
291
207
|
function _fromAlgId(algNode) {
|
|
292
208
|
if (!_isSequence(algNode) || algNode.children.length < 1) throw _err("kem/bad-algorithm", "the AlgorithmIdentifier is malformed");
|
|
293
209
|
var algOid;
|
|
@@ -296,7 +212,6 @@ function _fromAlgId(algNode) {
|
|
|
296
212
|
return _descriptorForAlg(algOid, algNode.children.length > 1);
|
|
297
213
|
}
|
|
298
214
|
|
|
299
|
-
// ---- public verbs ------------------------------------------------------------
|
|
300
215
|
|
|
301
216
|
/**
|
|
302
217
|
* @primitive pki.kem.decapsulate
|
|
@@ -320,33 +235,23 @@ function _fromAlgId(algNode) {
|
|
|
320
235
|
* // requires: ciphertext -- the composite KEM ciphertext received from the sender
|
|
321
236
|
* var secret = await pki.kem.decapsulate(privatePkcs8Der, ciphertext); // 32-byte Buffer
|
|
322
237
|
*/
|
|
323
|
-
// async so any synchronous fault (an argument that is not a BufferSource, a malformed DER structure)
|
|
324
|
-
// leaves as a rejected promise rather than a synchronous throw, keeping the Promise-returning contract.
|
|
325
238
|
async function _decapsulate(privateKey, ciphertext) {
|
|
326
239
|
var pkcs8 = _toDer(privateKey, "PRIVATE KEY", "the composite ML-KEM private key");
|
|
327
240
|
var ct = guard.bytes.snapshotSource(ciphertext, KemError, "kem/bad-input", "the composite ML-KEM ciphertext");
|
|
328
241
|
var d, sk;
|
|
329
242
|
try {
|
|
330
243
|
try {
|
|
331
|
-
// Compose the shared strict OneAsymmetricKey parser so the version, the algorithm, and the optional
|
|
332
|
-
// [0]/[1] trailing fields are all validated once, rather than reading fixed positions and trusting
|
|
333
|
-
// the rest. The privateKey octets are the raw composite mlkemSeed || tradSK (draft sec. 4.2).
|
|
334
244
|
var info = schemaPkcs8.parse(pkcs8);
|
|
335
245
|
var alg = info.privateKeyAlgorithm;
|
|
336
246
|
d = _descriptorForAlg(alg.oid, alg.parameters !== null);
|
|
337
247
|
sk = Buffer.from(info.privateKey);
|
|
338
248
|
} catch (e) {
|
|
339
249
|
if (e instanceof KemError) throw e;
|
|
340
|
-
// the shared parser rejects a present `parameters` at the envelope; re-map it to the module's own
|
|
341
|
-
// parameters-absent verdict so both verbs report draft sec. 5.3 the same way.
|
|
342
250
|
if (e && e.code === "pkcs8/bad-algorithm-parameters") throw _err("kem/bad-algorithm", "a composite ML-KEM AlgorithmIdentifier MUST have absent parameters (draft sec. 5.3)", e);
|
|
343
251
|
throw _err("kem/bad-key", "the composite ML-KEM private key is not a well-formed OneAsymmetricKey", e);
|
|
344
252
|
}
|
|
345
253
|
try {
|
|
346
254
|
if (sk.length <= MLKEM_SEED) throw _err("kem/bad-key", "the composite private key is shorter than the ML-KEM seed");
|
|
347
|
-
// the composite ciphertext is a fixed length (the ML-KEM ciphertext plus the exact traditional
|
|
348
|
-
// component); reject any other length before building a key, so an attacker-controlled oversized
|
|
349
|
-
// traditional component cannot force an unbounded allocation (CWE-770).
|
|
350
255
|
if (ct.length !== d.mlkemCt + d.trad.ctLen) throw _err("kem/bad-ciphertext", "the composite ciphertext is not the expected " + (d.mlkemCt + d.trad.ctLen) + " bytes for " + d.name);
|
|
351
256
|
var seed = sk.subarray(0, MLKEM_SEED), tradSK = sk.subarray(MLKEM_SEED);
|
|
352
257
|
var mlkemCT = ct.subarray(0, d.mlkemCt), tradCT = ct.subarray(d.mlkemCt);
|
|
@@ -354,12 +259,9 @@ async function _decapsulate(privateKey, ciphertext) {
|
|
|
354
259
|
function (v) { return v; }, function (v) { return v.ss; });
|
|
355
260
|
return await _combine(d, r[0], r[1].ss, tradCT, r[1].tradPK);
|
|
356
261
|
} finally {
|
|
357
|
-
// the reconstructed private-key copy (ML-KEM seed || traditional private key) is wiped on every path,
|
|
358
|
-
// including the length-validation exits above.
|
|
359
262
|
guard.secret.zeroize(sk, KemError, "kem/bad-input", "the composite private key material");
|
|
360
263
|
}
|
|
361
264
|
} finally {
|
|
362
|
-
// the snapshotted PKCS#8 copy still holds the private-key octets; wipe it on every path (draft sec. 3.5).
|
|
363
265
|
guard.secret.zeroize(pkcs8, KemError, "kem/bad-input", "the composite private key DER");
|
|
364
266
|
}
|
|
365
267
|
}
|
|
@@ -405,8 +307,6 @@ async function _encapsulate(publicKey) {
|
|
|
405
307
|
return { sharedSecret: ss, ciphertext: Buffer.concat([r[0].ct, r[1].ct]) };
|
|
406
308
|
}
|
|
407
309
|
|
|
408
|
-
// Only the two verbs are public: this module IS the pki.kem namespace, so the internal registry stays
|
|
409
|
-
// unexported to keep it off the operator surface and immutable from outside.
|
|
410
310
|
module.exports = {
|
|
411
311
|
encapsulate: _encapsulate,
|
|
412
312
|
decapsulate: _decapsulate,
|
package/lib/composite-sig.js
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
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 if and only if both pass. Every component must
|
|
17
|
-
// verify; there is no AND-to-OR downgrade. The ML-DSA component is the fixed-length first half, and
|
|
18
|
-
// 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.
|
|
4
|
+
// @internal
|
|
24
5
|
|
|
25
6
|
var asn1 = require("./asn1-der");
|
|
26
7
|
var oid = require("./oid");
|
|
@@ -30,10 +11,6 @@ var edwardsPoint = require("./edwards-point");
|
|
|
30
11
|
var subtle = webcrypto.webcrypto.subtle;
|
|
31
12
|
var _b = asn1.build;
|
|
32
13
|
|
|
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
14
|
function _codeOf(e, fallback) {
|
|
38
15
|
var prefix = fallback.slice(0, fallback.indexOf("/") + 1);
|
|
39
16
|
return (e && typeof e.code === "string" && e.code.indexOf(prefix) === 0) ? e.code : fallback;
|
|
@@ -46,17 +23,8 @@ var MLDSA_COMPONENT = {
|
|
|
46
23
|
"ML-DSA-87": { pk: 2592, sig: 4627, oid: "id-ml-dsa-87" },
|
|
47
24
|
};
|
|
48
25
|
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
26
|
var PH_CMS = { "SHA-256": "sha256", "SHA-512": "sha512", "SHAKE256": "shake256" };
|
|
54
27
|
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 (the two
|
|
58
|
-
// brainpool curves): registered and params-guarded, deferred at verify to the
|
|
59
|
-
// caller's unsupported-algorithm code instead of silently accepted.
|
|
60
28
|
function _comp(name, mldsa, ph, label, trad) {
|
|
61
29
|
var sz = MLDSA_COMPONENT[mldsa];
|
|
62
30
|
COMPOSITE_ALGS[oid.byName(name)] = {
|
|
@@ -83,24 +51,16 @@ _comp("id-MLDSA87-RSA3072-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-
|
|
|
83
51
|
_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
52
|
_comp("id-MLDSA87-ECDSA-P521-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P521-SHA512", { ec: "P-521", hash: "SHA-512" });
|
|
85
53
|
|
|
86
|
-
// Wrap a raw component public key in the SPKI its WebCrypto import expects, so each half is
|
|
87
|
-
// verified through the same import and 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
54
|
function _spkiFor(algNode, keyBytes) { return _b.sequence([algNode, _b.bitString(keyBytes, 0)]); }
|
|
91
55
|
function _mldsaSpki(mldsaOid, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(mldsaOid))]), raw); }
|
|
92
56
|
function _ecSpki(curve, point) { return _spkiFor(_b.sequence([_b.oid(oid.byName("ecPublicKey")), _b.oid(oid.byName(EC_CURVE_OID[curve]))]), point); }
|
|
93
57
|
function _edSpki(name, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(name))]), raw); }
|
|
94
58
|
function _rsaSpki(rsaPub) { return _spkiFor(_b.sequence([_b.oid(oid.byName("rsaEncryption")), _b.nullValue()]), rsaPub); }
|
|
95
59
|
|
|
96
|
-
// The exact modulus bit length of an RSAPublicKey (SEQUENCE { modulus, exponent }).
|
|
97
60
|
function _rsaModulusBits(rsaPubDer) {
|
|
98
61
|
return asn1.read.integer(asn1.decode(rsaPubDer).children[0]).toString(2).length;
|
|
99
62
|
}
|
|
100
63
|
|
|
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
64
|
function compositeMprime(d, phBuf, ctx) {
|
|
105
65
|
ctx = ctx || Buffer.alloc(0);
|
|
106
66
|
return Buffer.concat([COMPOSITE_PREFIX, d.label, Buffer.from([ctx.length]), ctx, Buffer.from(phBuf)]);
|
|
@@ -108,22 +68,13 @@ function compositeMprime(d, phBuf, ctx) {
|
|
|
108
68
|
|
|
109
69
|
function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
|
|
110
70
|
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
71
|
if (tradPK.length < 1 || tradPK[0] !== 0x04) return Promise.resolve(false);
|
|
114
72
|
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
73
|
var p1363 = validator.sig.ecdsaDerToP1363(tradSig, trad.ec, E, badSig);
|
|
118
74
|
return subtle.verify({ name: "ECDSA", hash: trad.hash }, k, p1363, mprime);
|
|
119
75
|
});
|
|
120
76
|
}
|
|
121
77
|
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, collapsing the composite conjunction to
|
|
124
|
-
// ML-DSA alone. 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) instead of a silent false.
|
|
127
78
|
if (!edwardsPoint.validate(tradPK, trad.eddsa === "Ed25519" ? 6 : 7)) {
|
|
128
79
|
throw new E(badSig, "the composite EdDSA component public key is not a valid, full-order Edwards point");
|
|
129
80
|
}
|
|
@@ -131,10 +82,6 @@ function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
|
|
|
131
82
|
.then(function (k) { return subtle.verify({ name: trad.eddsa }, k, tradSig, mprime); });
|
|
132
83
|
}
|
|
133
84
|
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
85
|
var bits;
|
|
139
86
|
try { bits = _rsaModulusBits(tradPK); }
|
|
140
87
|
catch (_e) { return Promise.resolve(false); }
|
|
@@ -146,14 +93,9 @@ function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
|
|
|
146
93
|
return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
|
|
147
94
|
.then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
|
|
148
95
|
}
|
|
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
96
|
return Promise.resolve(false);
|
|
152
97
|
}
|
|
153
98
|
|
|
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
99
|
function compositeVerify(spkiBytes, sigBytes, message, d, E, unsupported, badSig) {
|
|
158
100
|
if (d.trad.unsupported) {
|
|
159
101
|
return Promise.resolve({ ok: false, code: unsupported,
|
|
@@ -162,13 +104,9 @@ function compositeVerify(spkiBytes, sigBytes, message, d, E, unsupported, badSig
|
|
|
162
104
|
var rawKey;
|
|
163
105
|
try {
|
|
164
106
|
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
107
|
if (bs.unusedBits !== 0) throw new E(badSig, "composite subjectPublicKey has unused bits");
|
|
168
108
|
rawKey = bs.bytes;
|
|
169
109
|
} catch (e) { return Promise.resolve({ ok: false, code: _codeOf(e, badSig), error: e }); }
|
|
170
|
-
// The ML-DSA half is fixed-length and comes first; the traditional half is the remainder. Both must be
|
|
171
|
-
// non-empty for a well-formed composite.
|
|
172
110
|
if (rawKey.length <= d.mldsaPk || sigBytes.length <= d.mldsaSig) {
|
|
173
111
|
return Promise.resolve({ ok: false, code: badSig,
|
|
174
112
|
error: new E(badSig, "composite key/signature shorter than the fixed ML-DSA component") });
|
|
@@ -184,27 +122,15 @@ function compositeVerify(spkiBytes, sigBytes, message, d, E, unsupported, badSig
|
|
|
184
122
|
}).catch(function (e) { return { ok: false, code: _codeOf(e, badSig), error: e }; });
|
|
185
123
|
}
|
|
186
124
|
|
|
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
125
|
function resolveCompositeDescriptor(sigAlg, E, unsupported) {
|
|
191
126
|
var comp = COMPOSITE_ALGS[sigAlg.oid];
|
|
192
127
|
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
128
|
if (sigAlg.parameters !== null && sigAlg.parameters !== undefined) {
|
|
198
129
|
throw new E(unsupported, "composite signature algorithm parameters must be absent (draft-ietf-lamps-pq-composite-sigs sec. 5.3)");
|
|
199
130
|
}
|
|
200
131
|
return { composite: comp, sameKeyOid: true };
|
|
201
132
|
}
|
|
202
133
|
|
|
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
134
|
function compositeSign(d, keys, message, ctx) {
|
|
209
135
|
return subtle.digest({ name: d.ph }, message).then(function (phBuf) {
|
|
210
136
|
var mprime = compositeMprime(d, phBuf, ctx);
|
|
@@ -218,8 +144,6 @@ function _signTradComponent(trad, pkcs8, mprime) {
|
|
|
218
144
|
if (trad.ec) {
|
|
219
145
|
return subtle.importKey("pkcs8", pkcs8, { name: "ECDSA", namedCurve: trad.ec }, false, ["sign"])
|
|
220
146
|
.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
147
|
.then(function (raw) { var r = Buffer.from(raw); return validator.sig.rawToEcdsaDer(r, r.length / 2); });
|
|
224
148
|
}
|
|
225
149
|
if (trad.eddsa) {
|