@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +1 -1
  3. package/index.js +1 -1
  4. package/lib/acme.js +59 -815
  5. package/lib/asn1-der.js +7 -245
  6. package/lib/attrcert-sign.js +12 -260
  7. package/lib/attrcert-verify.js +1 -7
  8. package/lib/byte-reader.js +0 -8
  9. package/lib/byte-writer.js +0 -11
  10. package/lib/cbor-det.js +0 -54
  11. package/lib/cmc-build.js +15 -357
  12. package/lib/cmc-verify.js +6 -239
  13. package/lib/cmp-build.js +41 -317
  14. package/lib/cmp-session.js +73 -760
  15. package/lib/cmp-verify.js +32 -488
  16. package/lib/cms-compress.js +3 -32
  17. package/lib/cms-decrypt.js +29 -294
  18. package/lib/cms-encrypt.js +14 -234
  19. package/lib/cms-sign.js +29 -324
  20. package/lib/cms-verify.js +9 -500
  21. package/lib/composite-kem.js +2 -102
  22. package/lib/composite-sig.js +1 -77
  23. package/lib/constants.js +0 -249
  24. package/lib/crl-sign.js +9 -237
  25. package/lib/crl-verify.js +1 -10
  26. package/lib/crmf-sign.js +19 -198
  27. package/lib/crmf-verify.js +1 -12
  28. package/lib/csr-sign.js +3 -34
  29. package/lib/csr-verify.js +1 -12
  30. package/lib/ct.js +28 -253
  31. package/lib/edwards-point.js +10 -44
  32. package/lib/est.js +28 -764
  33. package/lib/framework-error.js +4 -182
  34. package/lib/guard-all.js +1 -58
  35. package/lib/guard-async.js +1 -36
  36. package/lib/guard-bytes.js +6 -650
  37. package/lib/guard-compress.js +5 -102
  38. package/lib/guard-crypto.js +1 -40
  39. package/lib/guard-der.js +2 -93
  40. package/lib/guard-encoding.js +2 -60
  41. package/lib/guard-header.js +1 -36
  42. package/lib/guard-identifier.js +7 -531
  43. package/lib/guard-intrinsic.js +1 -150
  44. package/lib/guard-json.js +10 -73
  45. package/lib/guard-limits.js +1 -77
  46. package/lib/guard-list.js +1 -49
  47. package/lib/guard-name.js +3 -209
  48. package/lib/guard-parsed.js +7 -335
  49. package/lib/guard-range.js +1 -71
  50. package/lib/guard-secret.js +1 -102
  51. package/lib/guard-text.js +1 -64
  52. package/lib/guard-time.js +1 -97
  53. package/lib/hpke.js +3 -105
  54. package/lib/http-digest.js +14 -142
  55. package/lib/http-retry-after.js +1 -47
  56. package/lib/http-transport.js +21 -131
  57. package/lib/inspect.js +30 -284
  58. package/lib/ip-utils.js +6 -47
  59. package/lib/jose.js +7 -142
  60. package/lib/key.js +2 -67
  61. package/lib/lint.js +9 -194
  62. package/lib/merkle.js +1 -27
  63. package/lib/mime.js +9 -82
  64. package/lib/ocsp-request-verify.js +1 -14
  65. package/lib/ocsp-verify.js +1 -82
  66. package/lib/ocsp.js +11 -169
  67. package/lib/oid.js +5 -337
  68. package/lib/path-validate.js +103 -1492
  69. package/lib/pbes2.js +2 -89
  70. package/lib/pkcs12-build.js +31 -379
  71. package/lib/pki-build.js +64 -290
  72. package/lib/rc2.js +2 -18
  73. package/lib/rfc3339.js +4 -27
  74. package/lib/scep.js +59 -233
  75. package/lib/schema-all.js +0 -122
  76. package/lib/schema-attrcert.js +4 -146
  77. package/lib/schema-c509.js +153 -802
  78. package/lib/schema-cmc.js +5 -220
  79. package/lib/schema-cmp.js +2 -223
  80. package/lib/schema-cms.js +13 -427
  81. package/lib/schema-crl.js +5 -68
  82. package/lib/schema-crmf.js +6 -167
  83. package/lib/schema-csr.js +1 -54
  84. package/lib/schema-csrattrs.js +2 -87
  85. package/lib/schema-engine.js +0 -152
  86. package/lib/schema-ocsp.js +0 -117
  87. package/lib/schema-pkcs12.js +2 -93
  88. package/lib/schema-pkcs8.js +3 -35
  89. package/lib/schema-pkix.js +24 -655
  90. package/lib/schema-smime.js +0 -54
  91. package/lib/schema-tsp.js +3 -99
  92. package/lib/schema-x509.js +4 -63
  93. package/lib/shbs.js +11 -95
  94. package/lib/sign-scheme.js +2 -97
  95. package/lib/sigstore.js +9 -169
  96. package/lib/sleep.js +2 -8
  97. package/lib/smime.js +15 -408
  98. package/lib/tls-cert-compress.js +0 -62
  99. package/lib/trust.js +19 -185
  100. package/lib/tsp-sign.js +16 -270
  101. package/lib/validator-all.js +1 -30
  102. package/lib/validator-attcert.js +2 -46
  103. package/lib/validator-cose.js +2 -153
  104. package/lib/validator-keydesc.js +1 -37
  105. package/lib/validator-sig.js +1 -46
  106. package/lib/validator-tls.js +1 -88
  107. package/lib/validator-tpm.js +19 -122
  108. package/lib/verify-seam.js +1 -24
  109. package/lib/webauthn-mds.js +3 -400
  110. package/lib/webauthn.js +12 -833
  111. package/lib/webcrypto.js +5 -485
  112. package/lib/x509-sign.js +4 -108
  113. package/package.json +1 -1
  114. package/sbom.cdx.json +6 -6
@@ -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; // d || z, both 32 bytes (draft sec. 4.2)
97
- var RSA_SS = 32; // ss_len for RSA-OAEP-KEM (draft sec. 6.1)
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,
@@ -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) {