@blamejs/pki 0.6.47 → 0.6.49

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/lib/cmc-verify.js CHANGED
@@ -261,7 +261,9 @@ function _governingStatus(statuses, outcome) {
261
261
  * - `dataReturn` (Buffer) -- the Data Return payload the request sent, echoed verbatim.
262
262
  * - `certs` (Buffer[]) -- extra certificates for signer lookup, for a message that does not carry
263
263
  * its own signer; the certificates the message carries are searched either way.
264
- * - `recipient` (object) -- key material for an AuthenticatedData carrier, in the shape
264
+ * - `recipient` (object) -- `{ identifier, secret }` for a response MACed under the shared secret
265
+ * the client enrolled with, which derives the key the way RFC 5272 sec. 3.2(c) states, or key
266
+ * material for an AuthenticatedData carrier, in the shape
265
267
  * `pki.cms.decrypt` takes. Its MAC is then checked and the verdict reports
266
268
  * `signatureVerified: true`; the content it authenticates is bound to the content the verdict
267
269
  * was read from, so a MAC over other bytes cannot stand in for it.
@@ -288,14 +290,29 @@ function _governingStatus(statuses, outcome) {
288
290
  * v.boundToRequest; // false: nothing ties this response to a request either
289
291
  */
290
292
  function verify(response, sent) {
291
- var frozenResponse, frozenSent;
293
+ var frozenResponse, frozenSent, ownedSecret = null;
294
+ /** @internal The options snapshot owns a copy of a byte-valued shared secret from the moment it is
295
+ * taken, so it is wiped when the verification settles however it settles: a malformed response, a
296
+ * carrier that never needed the secret, a refusal on the way to the MAC, and a failure in the rest
297
+ * of this prologue all reach here. A failure DURING the snapshot is cleared by the snapshot itself,
298
+ * which is the only place that can still reach a half-built copy. The caller's own buffer is theirs
299
+ * and is never touched. */
300
+ function _wipeOwnedSecret() {
301
+ if (ownedSecret) guard.secret.zeroize(ownedSecret, CmcError, "cmc/bad-input", "the shared-secret copy");
302
+ ownedSecret = null;
303
+ }
292
304
  try {
293
305
  frozenSent = _snapshotSent(_assertOpts(sent));
306
+ ownedSecret = frozenSent.recipient && guard.bytes.isByteSource(frozenSent.recipient.secret)
307
+ ? frozenSent.recipient.secret : null;
294
308
  frozenResponse = _snapshotIfBytes(response);
295
309
  } catch (e) {
310
+ _wipeOwnedSecret();
296
311
  return Promise.reject(e);
297
312
  }
298
- return guard.async.deferred(function () { return _verify(frozenResponse, frozenSent); });
313
+ return guard.async.deferred(function () { return _verify(frozenResponse, frozenSent); })
314
+ .then(function (v) { _wipeOwnedSecret(); return v; },
315
+ function (e) { _wipeOwnedSecret(); throw e; });
299
316
  }
300
317
 
301
318
  function _assertOpts(sent) {
@@ -330,18 +347,30 @@ function _snapshotSent(sent) {
330
347
  if (Array.isArray(out.bodyPartPaths)) {
331
348
  out.bodyPartPaths = out.bodyPartPaths.map(function (p) { return Array.isArray(p) ? p.slice() : p; });
332
349
  }
333
- if (out.recipient && typeof out.recipient === "object") {
334
- var r = intrinsic.create(null), rk;
335
- for (rk in out.recipient) {
336
- if (!_hasOwn(out.recipient, rk)) continue;
337
- r[rk] = _copyAnyBytes(out.recipient[rk]);
350
+ /** @internal Once a copy of a shared secret exists, EVERYTHING that follows is protected, to the
351
+ * return: the remaining copies, the last option reads, all of it. A caller field is read through
352
+ * whatever accessor the caller wrote, so any of these steps can throw, and the copy would then be
353
+ * left behind with nothing holding it. Guarding one step at a time is what let three earlier
354
+ * versions of this each miss the next one. */
355
+ var takenSecret = null;
356
+ try {
357
+ if (out.recipient && typeof out.recipient === "object") {
358
+ var r = intrinsic.create(null), rk;
359
+ for (rk in out.recipient) {
360
+ if (!_hasOwn(out.recipient, rk)) continue;
361
+ r[rk] = _copyAnyBytes(out.recipient[rk]);
362
+ if (rk === "secret" && guard.bytes.isByteSource(r[rk])) takenSecret = r[rk];
363
+ }
364
+ out.recipient = r;
338
365
  }
339
- out.recipient = r;
366
+ if (Array.isArray(out.certs)) out.certs = out.certs.map(_copyAnyBytes);
367
+ out.allowUnverified = sent.allowUnverified === true;
368
+ out.allowUnbound = sent.allowUnbound === true;
369
+ return out;
370
+ } catch (e) {
371
+ if (takenSecret) guard.secret.zeroize(takenSecret, CmcError, "cmc/bad-input", "the shared-secret copy");
372
+ throw e;
340
373
  }
341
- if (Array.isArray(out.certs)) out.certs = out.certs.map(_copyAnyBytes);
342
- out.allowUnverified = sent.allowUnverified === true;
343
- out.allowUnbound = sent.allowUnbound === true;
344
- return out;
345
374
  }
346
375
 
347
376
  function _verify(response, sent) {
@@ -368,10 +397,28 @@ function _assertAuthentic(body, sent, responseBytes) {
368
397
  "authenticating an AuthenticatedData response needs the response as DER bytes or PEM, " +
369
398
  "because its MAC is computed over them -- pass the encoded response rather than a parsed one");
370
399
  }
371
- return cmsDecrypt.decrypt(responseBytes, sent.recipient).then(null, function (e) {
372
- throw E("cmc/unverified-response",
373
- "the AuthenticatedData response did not authenticate under the supplied recipient key", e);
374
- }).then(function (res) {
400
+ /** @internal A caller naming the shared secret it enrolled with gets the sec. 3.2(c) derivation
401
+ * from the same function the producer uses, rather than deriving it again by hand. Raw key
402
+ * material is still taken as it was, which is what an implementation that derived the key
403
+ * itself supplies. */
404
+ var recipient = sent.recipient, derivedKey = null;
405
+ /** @internal The concatenation this derivation owns. The secret copy it was built from belongs
406
+ * to the options snapshot, which `verify` wipes when the verification settles. */
407
+ function _wipeDerived() {
408
+ if (derivedKey) guard.secret.zeroize(derivedKey, CmcError, "cmc/bad-input", "the derived shared-secret key");
409
+ derivedKey = null;
410
+ }
411
+ if (recipient && typeof recipient === "object" && recipient.mac == null &&
412
+ (recipient.identifier != null || recipient.secret != null) && recipient.password == null) {
413
+ derivedKey = cmcBuild.macPassword(recipient);
414
+ recipient = { password: derivedKey };
415
+ }
416
+ return cmsDecrypt.decrypt(responseBytes, recipient).then(function (r) { _wipeDerived(); return r; },
417
+ function (e) {
418
+ _wipeDerived();
419
+ throw E("cmc/unverified-response",
420
+ "the AuthenticatedData response did not authenticate under the supplied recipient key", e);
421
+ }).then(function (res) {
375
422
  if (!res || res.authenticated !== true) {
376
423
  throw E("cmc/unverified-response", "the AuthenticatedData response did not authenticate under the supplied recipient key");
377
424
  }
@@ -470,7 +517,20 @@ function _shape(body, sent, signatureVerified, bound) {
470
517
  * requests, each naming exactly one arm: `tcr` (a PKCS#10 CSR), `crm` (a CRMF CertReqMsg, or the
471
518
  * CertReqMessages `pki.crmf.build` returns when it carries exactly one) or `orm`
472
519
  * (`{ type, value }`). `spec.controls` are additional controls as `{ type, value }`, and `signer`
473
- * is the `{ cert, key }` that signs the enclosing CMS SignedData. A `tcr` request is verified against
520
+ * is the `{ cert, key }` that signs the enclosing CMS SignedData. RFC 5272 sec. 3.2 gives a Full PKI
521
+ * Request a second carrier for a client that has no key to sign with: pass
522
+ * `{ mac: { identifier, secret } }` in place of `{ cert, key }` and the request is an
523
+ * AuthenticatedData whose MAC the shared secret keys, through the PasswordRecipientInfo sec. 3.2(a)
524
+ * requires. The key is derived from the identifier encoded as UTF-8 followed by the secret
525
+ * (sec. 3.2(c)), which is the derivation `pki.cmc.verify` reads a `recipient` under, so a caller names
526
+ * what it enrolled with at both ends rather than concatenating by hand. The request also carries the
527
+ * identifier in an Identification control, because a PasswordRecipientInfo names no recipient and that
528
+ * control is what "assists the server in locating the shared-secret" (RFC 5272 sec. 6.2.3); supplying
529
+ * a different name through `spec.identityProof.identity` is refused rather than chosen between. Since
530
+ * carrying that control also changes how an Identity Proof witness is derived (sec. 6.2.3), a request
531
+ * carrying both derives the witness with the name it carries, and its MAC and its Identity Proof are
532
+ * keyed from one secret rather than two. Naming a signature and a shared secret together is refused,
533
+ * since a request has one carrier. A `tcr` request is verified against
474
534
  * its own proof-of-possession first: a PKCS#10 whose self-signature does not verify under its subject
475
535
  * public key is refused (`cmc/bad-popo`) rather than signed into a message a CA would reject.
476
536
  *
@@ -490,6 +550,26 @@ function _shape(body, sent, signatureVerified, bound) {
490
550
  * carry neither Identification nor Identity Proof (sec. 3.2 (a)), so asking for both is refused
491
551
  * and never silently dropped.
492
552
  *
553
+ * `spec.popChallenge: { challenge, recipient }` answers an authority's challenge-response proof of
554
+ * possession, which is how a key that cannot sign for itself proves the client holds it (RFC 5272
555
+ * sec. 6.7). `challenge` is the Encrypted POP control's value and `recipient` is the key material
556
+ * `pki.cms.decrypt` takes. The proof value is decrypted, hashed under the witness algorithm the
557
+ * challenge names and compared with the witness it carries; a value that does not decrypt or does not
558
+ * match is not the one the authority issued, so the request is refused with `cmc/pop-failed` and no
559
+ * proof is sent. The answer is a MAC over the request the challenge carried, keyed by that value
560
+ * (truncated to 64 bytes, sec. 6.7) under the algorithm the challenge named, and it names the
561
+ * certification request in THIS request, so a request carrying more than one is refused. The
562
+ * algorithms are the ones RFC 5274 sec. 4.2 requires of a client: SHA-1 or SHA-256 for the witness,
563
+ * HMAC-SHA1 or HMAC-SHA256 for the proof.
564
+ *
565
+ * A `tcr` request signed with `id-alg-noSignature` (RFC 5272 App. C.1) is the request a decrypt-only
566
+ * key produces, and it carries no proof of its own. It is accepted only when this message answers a
567
+ * challenge for that same request and the key that opened the challenge is the request's own subject
568
+ * public key, compared as keys rather than as encodings. A challenge opened by a password or a
569
+ * key-encryption key identifies no key and does not qualify, and its parameters must be present and
570
+ * encoded as NULL. Without such an answer the request is refused with `cmc/bad-popo`, and a request
571
+ * that does carry a signature is still held to it.
572
+ *
493
573
  * `spec.transactionId` (number|bigint), `spec.senderNonce` and `spec.dataReturn` (bytes) attach the
494
574
  * exchange-binding controls (RFC 5272 sec. 6.6 / 6.4), the same three `pki.cmc.verify` checks the
495
575
  * response against. They are named fields, not something to hand-encode into
@@ -511,6 +591,7 @@ function _shape(body, sent, signatureVerified, bound) {
511
591
  * var req = await pki.cmc.build({ requests: [{ tcr: csr }] }, { cert: cert, key: key });
512
592
  * pki.schema.cmc.parse(req).requests[0].arm; // "tcr"
513
593
  */
514
- var build = require("./cmc-build").build;
594
+ var cmcBuild = require("./cmc-build");
595
+ var build = cmcBuild.build;
515
596
 
516
597
  module.exports = { verify: verify, build: build };
@@ -160,7 +160,7 @@ function _kekIdMatches(ri, kekId) {
160
160
  return Buffer.compare(id, guard.bytes.view(kekId, CmsError, "cms/bad-input", "kekId")) === 0;
161
161
  }
162
162
 
163
- var _passThrough = intrinsic.assign(intrinsic.create(null), { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1 });
163
+ var _passThrough = intrinsic.assign(intrinsic.create(null), { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1, "cms/too-many-recipient-keys": 1 });
164
164
 
165
165
  async function _acquireCek(ri, km, opts) {
166
166
  try {
@@ -198,6 +198,29 @@ async function _ktriCek(ri, km) {
198
198
  }
199
199
  }
200
200
 
201
+ /** @internal One KeyAgreeRecipientInfo can carry an encrypted key for several recipients under one
202
+ * originator and one key-derivation, and a caller decrypting with a private key alone has no identifier
203
+ * to select its own entry by. The key-encryption key is derived once and every entry is offered to it;
204
+ * AES key wrap's integrity check settles which entry belongs to this key, so an entry that opens is one
205
+ * this key was addressed by. The attempt count is bounded because the list is attacker-supplied. */
206
+ async function _kariUnwrapAny(kek, reks) {
207
+ var list = reks || [];
208
+ /** @internal The list arrives with the message, so the number of unwraps it can demand is bounded.
209
+ * The bound caps the attempts rather than refusing the list, so an entry within it still opens the
210
+ * message it always opened. */
211
+ var tried = list.length < C.LIMITS.CMS_MAX_KARI_ENCRYPTED_KEYS
212
+ ? list.length : C.LIMITS.CMS_MAX_KARI_ENCRYPTED_KEYS;
213
+ for (var i = 0; i < tried; i++) {
214
+ try { return await _aesKwUnwrap(kek, list[i].encryptedKey); }
215
+ catch (_e) { continue; } // allow:swallow-unverified this entry is not addressed to this key; a later one may be
216
+ }
217
+ if (list.length > tried) {
218
+ throw _err("cms/too-many-recipient-keys", "a KeyAgreeRecipientInfo names " + list.length +
219
+ " encrypted keys and none of the first " + tried + " opened; at most " + tried + " are tried");
220
+ }
221
+ throw _fail();
222
+ }
223
+
201
224
  async function _kariCek(ri, km) {
202
225
  var k = _normKeyDer(km.key);
203
226
  var kek, z, mz;
@@ -207,7 +230,7 @@ async function _kariCek(ri, km) {
207
230
  var wrapAlg = _kariWrap(kea);
208
231
  var scheme = kea.oid;
209
232
  var origSpki = _originatorSpki(ri.originator);
210
- var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
233
+ var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || null;
211
234
  var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
212
235
  if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
213
236
  var ukm = ri.ukm || null;
@@ -222,7 +245,8 @@ async function _kariCek(ri, km) {
222
245
  } else {
223
246
  var origAlg = asn1.decode(origSpki).children[0];
224
247
  var origHasParams = origAlg.children.length > 1;
225
- var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) || (origHasParams ? asn1.read.oid(origAlg.children[1]) : null);
248
+ var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) ||
249
+ (origHasParams ? asn1.read.oid(origAlg.children[1]) : _ecCurveFromPrivateKey(keyDer));
226
250
  var curve = curveOid ? CURVE[curveOid] : null;
227
251
  if (!curve) throw _err("cms/unsupported-algorithm", "unsupported or missing originator EC curve");
228
252
  var origSpkiFull = origHasParams ? origSpki : _withEcCurveParams(origSpki, curveOid);
@@ -232,7 +256,15 @@ async function _kariCek(ri, km) {
232
256
  var zKey = await subtle.importKey("raw", z, { name: "X963KDF" }, false, ["deriveBits"]);
233
257
  kek = Buffer.from(await subtle.deriveBits({ name: "X963KDF", hash: _x963Hash(scheme), info: _eccSharedInfo(wrapAlg.name, ukm, kekBytes) }, zKey, kekBytes * 8));
234
258
  }
235
- return await _aesKwUnwrap(kek, rek.encryptedKey);
259
+ /** @internal A caller that supplied a certificate selects its entry by recipient identifier, which
260
+ * is what that certificate is for. Offering every entry is for the caller that has only a key, and
261
+ * so has no identifier to select by. */
262
+ if (km.cert != null) {
263
+ var chosen = rek || ri.recipientEncryptedKeys[0];
264
+ if (!chosen) throw _fail();
265
+ return await _aesKwUnwrap(kek, chosen.encryptedKey);
266
+ }
267
+ return await _kariUnwrapAny(kek, ri.recipientEncryptedKeys);
236
268
  } finally {
237
269
  guard.secret.zeroizeAll([z, mz, kek], CmsError, "cms/bad-input", "the key-agreement shared secret");
238
270
  _releaseKeyDer(k);
@@ -507,6 +539,19 @@ function _ecCurveFromCert(cert) {
507
539
  if (spki.algorithm.oid !== O("ecPublicKey") || spki.algorithm.parameters == null) return null;
508
540
  return asn1.read.oid(asn1.decode(spki.algorithm.parameters));
509
541
  }
542
+ /** @internal The curve of the recipient's own private key. RFC 5753 sec. 7.1 lets an originator omit its
543
+ * curve parameters because the recipient already knows the curve; a recipient decrypting with a key and
544
+ * no certificate has that knowledge in the key itself, and nowhere else. */
545
+ function _ecCurveFromPrivateKey(keyDer) {
546
+ var spki;
547
+ try {
548
+ spki = nodeCrypto.createPublicKey({ key: keyDer, format: "der", type: "pkcs8" })
549
+ .export({ format: "der", type: "spki" });
550
+ } catch (_e) { return null; } // allow:swallow-unverified a key this runtime cannot read names no curve
551
+ var alg = asn1.decode(spki).children[0];
552
+ if (asn1.read.oid(alg.children[0]) !== O("ecPublicKey") || alg.children.length < 2) return null;
553
+ return asn1.read.oid(alg.children[1]);
554
+ }
510
555
  function _withEcCurveParams(spki, curveOid) {
511
556
  var node = asn1.decode(spki);
512
557
  var alg = node.children[0];
package/lib/constants.js CHANGED
@@ -124,6 +124,7 @@ var LIMITS = Object.assign(Object.create(null), {
124
124
  CBOR_MAX_ITEMS: 1000000,
125
125
  SCT_MAX_BYTES: BYTES.kib(64) + 1,
126
126
  SCT_MAX_COUNT: 256,
127
+ CMS_MAX_KARI_ENCRYPTED_KEYS: 64,
127
128
  MERKLE_MAX_PROOF_NODES: 65,
128
129
  PROXY_MAX_TLS_ANCHORS: 256,
129
130
  PATH_MAX_CERTS: 100,
@@ -4,6 +4,7 @@
4
4
  // @internal
5
5
 
6
6
  var _utilTypes = require("util").types;
7
+ var _nodeCrypto = require("node:crypto");
7
8
 
8
9
  var _call = Function.prototype.call;
9
10
  var _bind = Function.prototype.bind;
@@ -19,6 +20,10 @@ var _bufferByteLength = Buffer.byteLength;
19
20
  var _bufferCompare = Buffer.compare;
20
21
  var _bufferEquals = uncurry(Buffer.prototype.equals);
21
22
  var _bufferToString = uncurry(Buffer.prototype.toString);
23
+ /** @internal Key identity, taken off the KeyObject prototype at load. A caller that replaced this
24
+ * method could make two different keys answer as one, which is the question a proof of possession
25
+ * turns on. */
26
+ var _keyEquals = uncurry(_nodeCrypto.KeyObject.prototype.equals);
22
27
 
23
28
  var _isView = ArrayBuffer.isView;
24
29
  var _isInteger = Number.isInteger;
@@ -146,6 +151,7 @@ module.exports = Object.freeze({
146
151
  byteLength: _bufferByteLength,
147
152
  compare: _bufferCompare,
148
153
  bufferEquals: _bufferEquals,
154
+ keyEquals: _keyEquals,
149
155
  isView: _isView,
150
156
  isInteger: _isInteger,
151
157
  isSafeInteger: _isSafeInteger,
package/lib/schema-cmc.js CHANGED
@@ -42,6 +42,8 @@ var schema = require("./schema-engine");
42
42
  var pkix = require("./schema-pkix");
43
43
  var oid = require("./oid");
44
44
  var cms = require("./schema-cms");
45
+ var csr = require("./schema-csr");
46
+ var crmf = require("./schema-crmf");
45
47
  var guard = require("./guard-all");
46
48
  var frameworkError = require("./framework-error");
47
49
 
@@ -55,6 +57,8 @@ function O(name) { return oid.byName(name); }
55
57
 
56
58
  var OID_PKI_DATA = O("id-cct-PKIData");
57
59
  var OID_PKI_RESPONSE = O("id-cct-PKIResponse");
60
+ var OID_ENCRYPTED_POP = O("id-cmc-encryptedPOP");
61
+ var OID_DECRYPTED_POP = O("id-cmc-decryptedPOP");
58
62
  var OID_STATUS_INFO = O("id-cmc-statusInfo");
59
63
  var OID_STATUS_INFO_V2 = O("id-cmc-statusInfoV2");
60
64
  var OID_RESPONSE_BODY = O("id-cmc-responseBody");
@@ -79,6 +83,11 @@ var CMC_CARRIERS = Object.assign(Object.create(null), { signedData: 1, authData:
79
83
  var CONTROL_PLACEMENT = Object.create(null);
80
84
  CONTROL_PLACEMENT[OID_RESPONSE_BODY] = "pkiResponse";
81
85
  CONTROL_PLACEMENT[OID_RA_IDENTITY_WITNESS] = "pkiData";
86
+ /** @internal The two halves of the challenge-response exchange travel in opposite directions: the
87
+ * authority sends the challenge in its response, and the client answers in its next request (RFC 5272
88
+ * sec. 6.7). A message carrying the other one is not the message that section describes. */
89
+ CONTROL_PLACEMENT[OID_ENCRYPTED_POP] = "pkiResponse";
90
+ CONTROL_PLACEMENT[OID_DECRYPTED_POP] = "pkiData";
82
91
 
83
92
 
84
93
  function readBodyPartId(node, label) {
@@ -429,6 +438,181 @@ var PKI_RESPONSE = schema.seq([
429
438
  },
430
439
  });
431
440
 
441
+ /** @internal The challenge-response proof of possession a server sends when the key being certified
442
+ * cannot sign for itself (RFC 5272 sec. 6.7). The `cms` field is surfaced as the bytes it arrived as,
443
+ * because it is an EnvelopedData a caller decrypts with its own key material rather than something
444
+ * this parser opens. */
445
+ /** @internal The request a challenge quotes is one the client answers for, so it is held to the
446
+ * definition of the arm it uses rather than to its outer tag alone: a proof computed over a request
447
+ * that is not one could never be checked against anything. This is asked of a CHALLENGED request only.
448
+ * A request body carries what its sender sent, and judging it is the enrollment's business, not the
449
+ * decoder's. */
450
+ function _assertAnswerableRequest(request) {
451
+ if (request.arm !== "tcr" && request.arm !== "crm") return;
452
+ /** @internal A CertReqMessages of one, which is the shape the CRMF parser takes; built outside the
453
+ * catch below so only the parse it wraps can be what fails. */
454
+ var payload = request.arm === "tcr" ? request.certificationRequestBytes
455
+ : asn1.build.sequence([asn1.build.raw(request.certReqMsgBytes)]);
456
+ try {
457
+ if (request.arm === "tcr") { csr.parse(payload); } else { crmf.parse(payload); }
458
+ } catch (e) {
459
+ throw E("cmc/bad-pop-challenge",
460
+ "the request an EncryptedPOP quotes is not a " +
461
+ (request.arm === "tcr" ? "PKCS#10 CertificationRequest" : "CRMF CertReqMsg") +
462
+ ", so a proof over it could not be checked: " + ((e && e.message) || "malformed") +
463
+ " (RFC 5272 sec. 6.7)", e);
464
+ }
465
+ }
466
+
467
+ function parseEncryptedPop(node) {
468
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.constructed ||
469
+ node.children.length !== 5) {
470
+ throw E("cmc/bad-pop-challenge",
471
+ "an EncryptedPOP is SEQUENCE { request, cms, thePOPAlgID, witnessAlgID, witness } (RFC 5272 sec. 6.7)");
472
+ }
473
+ var envelope = _assertEnvelopedChallenge(node.children[1]);
474
+ var request;
475
+ try { request = parseTaggedRequest(node.children[0]); }
476
+ catch (e) {
477
+ throw E("cmc/bad-pop-challenge",
478
+ "an EncryptedPOP quotes the request it challenges as a TaggedRequest: " +
479
+ ((e && e.message) || "malformed") + " (RFC 5272 sec. 6.7)", e);
480
+ }
481
+ _assertAnswerableRequest(request);
482
+ return {
483
+ request: request,
484
+ /** @internal The TaggedRequest exactly as it arrived. A proof of possession is a MAC over these
485
+ * bytes, and the challenge carries them so neither side has to re-encode the request to agree on
486
+ * what was covered (RFC 5272 sec. 6.7). */
487
+ requestBytes: node.children[0].bytes,
488
+ cms: envelope.bytes,
489
+ cmsRecipientCount: envelope.recipientCount,
490
+ thePOPAlgID: _popAlgorithm(node.children[2], "thePOPAlgID"),
491
+ witnessAlgID: _popAlgorithm(node.children[3], "witnessAlgID"),
492
+ witness: _popOctetString(node.children[4], "witness"),
493
+ };
494
+ }
495
+
496
+ /** @internal A leaf of one of the POP controls. A malformed one is a malformed challenge, so it carries
497
+ * this control's code rather than the codec's. */
498
+ function _popOctetString(node, what) {
499
+ try { return asn1.read.octetString(node); }
500
+ catch (e) {
501
+ throw E("cmc/bad-pop-challenge", "a POP " + what + " is an OCTET STRING (RFC 5272 sec. 6.7)", e);
502
+ }
503
+ }
504
+
505
+ function parseDecryptedPop(node) {
506
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.constructed ||
507
+ node.children.length !== 3) {
508
+ throw E("cmc/bad-pop-challenge",
509
+ "a DecryptedPOP is SEQUENCE { bodyPartID, thePOPAlgID, thePOP } (RFC 5272 sec. 6.7)");
510
+ }
511
+ var bodyPartID;
512
+ try { bodyPartID = readBodyPartIdentity(node.children[0]); }
513
+ catch (e) {
514
+ throw E("cmc/bad-pop-challenge",
515
+ "a DecryptedPOP names the certification request it proves with a BodyPartID: " +
516
+ ((e && e.message) || "malformed") + " (RFC 5272 sec. 6.7)", e);
517
+ }
518
+ return {
519
+ bodyPartID: bodyPartID,
520
+ thePOPAlgID: _popAlgorithm(node.children[1], "thePOPAlgID"),
521
+ thePOP: _popOctetString(node.children[2], "thePOP"),
522
+ };
523
+ }
524
+
525
+ /** @internal The proof value travels in an EnvelopedData (RFC 5272 sec. 6.7), and the bytes are handed
526
+ * on unopened because opening them needs key material this parser does not hold. Unopened is not
527
+ * unchecked: a field that is not a ContentInfo carrying an EnvelopedData cannot be a challenge, and
528
+ * saying so here is what keeps a control that could never be answered from reading as one. */
529
+ function _assertEnvelopedChallenge(node) {
530
+ var kids = pkix.rootSequenceChildren(node, 2, 2);
531
+ if (!kids) {
532
+ throw E("cmc/bad-pop-challenge",
533
+ "an EncryptedPOP carries the proof value in a CMS ContentInfo, which is a SEQUENCE of a content " +
534
+ "type and its content (RFC 5272 sec. 6.7)");
535
+ }
536
+ var contentType;
537
+ try { contentType = asn1.read.oid(kids[0]); }
538
+ catch (e) {
539
+ throw E("cmc/bad-pop-challenge",
540
+ "an EncryptedPOP's ContentInfo names its content type with an OBJECT IDENTIFIER (RFC 5272 sec. 6.7)", e);
541
+ }
542
+ if (contentType !== O("envelopedData")) {
543
+ throw E("cmc/bad-pop-challenge",
544
+ "an EncryptedPOP carries the proof value in an EnvelopedData, got " +
545
+ (oid.name(contentType) || contentType) + " (RFC 5272 sec. 6.7)");
546
+ }
547
+ var content = kids[1];
548
+ if (content.tagClass !== "context" || content.tagNumber !== 0 || !content.constructed ||
549
+ content.children.length !== 1) {
550
+ throw E("cmc/bad-pop-challenge",
551
+ "an EncryptedPOP's ContentInfo carries its content in [0] EXPLICIT (RFC 5652 sec. 3)");
552
+ }
553
+ /** @internal Validated by the CMS EnvelopedData schema itself rather than by a shape check written
554
+ * here, so every field is held to one definition and the count reported below is the count of
555
+ * recipients that same definition accepted. Sec. 6.7 also allows a recipient identified by an
556
+ * issuerAndSerialNumber whose issuer is encoded as NULL, which is not the Name that schema requires;
557
+ * a challenge addressed only that way is refused here, and `pki.cms.decrypt` could not open it
558
+ * either, so it is refused where the reason can be named rather than after a failed answer. */
559
+ var enveloped;
560
+ try { enveloped = cms.walkEnvelopedData(content.children[0]); }
561
+ catch (e) {
562
+ throw E("cmc/bad-pop-challenge",
563
+ "an EncryptedPOP's ContentInfo does not carry an EnvelopedData a recipient could open: " +
564
+ ((e && e.message) || "malformed") + " (RFC 5652 sec. 6.1)", e);
565
+ }
566
+ var recipients = (enveloped.recipientInfos || []).length;
567
+ if (recipients < 1) {
568
+ throw E("cmc/bad-pop-challenge",
569
+ "an EnvelopedData names at least one recipient in its recipientInfos SET (RFC 5652 sec. 6.1)");
570
+ }
571
+ return { bytes: node.bytes, recipientCount: recipients };
572
+ }
573
+
574
+ /** @internal Both fields are AlgorithmIdentifiers, and both are read the same way: the OID names the
575
+ * algorithm and the parameters travel with it, so a caller answering a challenge copies what it was
576
+ * sent rather than reconstructing it. */
577
+ function _popAlgorithm(node, what) {
578
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.constructed ||
579
+ node.children.length < 1 || node.children.length > 2) {
580
+ throw E("cmc/bad-pop-challenge", "a POP " + what + " is an AlgorithmIdentifier (RFC 5272 sec. 6.7)");
581
+ }
582
+ var algOid;
583
+ try { algOid = asn1.read.oid(node.children[0]); }
584
+ catch (e) {
585
+ throw E("cmc/bad-pop-challenge",
586
+ "a POP " + what + " names its algorithm with an OBJECT IDENTIFIER (RFC 5272 sec. 6.7)", e);
587
+ }
588
+ return {
589
+ oid: algOid,
590
+ name: oid.name(algOid) || null,
591
+ parameters: node.children.length === 2 ? node.children[1].bytes : null,
592
+ bytes: node.bytes,
593
+ };
594
+ }
595
+
596
+ /** @internal One AttributeValue, which is what a control carrying a single structure holds. */
597
+ function _onlyValue(c, what) {
598
+ if (c.values.length !== 1) {
599
+ throw E("cmc/bad-pop-challenge",
600
+ "a " + what + " control carries exactly one AttributeValue, got " + c.values.length);
601
+ }
602
+ return asn1.decode(c.values[0]);
603
+ }
604
+
605
+ function _decodePopChallenges(controls) {
606
+ for (var i = 0; i < controls.length; i++) {
607
+ var c = controls[i];
608
+ if (c.attrType === OID_ENCRYPTED_POP) {
609
+ c.encryptedPOP = parseEncryptedPop(_onlyValue(c, "an Encrypted POP"));
610
+ } else if (c.attrType === OID_DECRYPTED_POP) {
611
+ c.decryptedPOP = parseDecryptedPop(_onlyValue(c, "a Decrypted POP"));
612
+ }
613
+ }
614
+ }
615
+
432
616
  function _collectStatuses(controls) {
433
617
  var out = [];
434
618
  for (var i = 0; i < controls.length; i++) {
@@ -445,6 +629,7 @@ function _collectStatuses(controls) {
445
629
 
446
630
  function _finishBody(body, where) {
447
631
  for (var i = 0; i < body.controls.length; i++) assertControlPlacement(body.controls[i], where);
632
+ _decodePopChallenges(body.controls);
448
633
  body.statuses = _collectStatuses(body.controls);
449
634
  body.unhandled = body.controls.filter(function (c) {
450
635
  return c.attrType !== OID_STATUS_INFO && c.attrType !== OID_STATUS_INFO_V2 && c.attrName === null;
@@ -541,9 +726,20 @@ function parsePkiResponse(input) {
541
726
  * pki.schema.cmc.parse(der).kind; // "pkiData"
542
727
  */
543
728
  function parse(input) {
544
- var parsedCms = (input && typeof input === "object" && !ArrayBuffer.isView(input) &&
545
- !guard.bytes.isByteSource(input) && input.encapContentInfo != null)
546
- ? input : cms.parse(input);
729
+ var alreadyParsed = input && typeof input === "object" && !ArrayBuffer.isView(input) &&
730
+ !guard.bytes.isByteSource(input) && input.encapContentInfo != null;
731
+ var parsedCms;
732
+ if (alreadyParsed) { parsedCms = input; }
733
+ else {
734
+ /** @internal Bytes that are not a CMS message at all are a CMC message this cannot read, and every
735
+ * other format parser answers for its own input in its own domain. */
736
+ try { parsedCms = cms.parse(input); }
737
+ catch (e) {
738
+ throw E("cmc/bad-der",
739
+ "a CMC message is carried in a CMS SignedData or AuthenticatedData, and these bytes are not " +
740
+ "one: " + ((e && e.message) || "malformed") + " (RFC 5272 sec. 3.2 / 4.2)", e);
741
+ }
742
+ }
547
743
  if (CMC_CARRIERS[parsedCms.contentTypeName] !== 1) {
548
744
  throw E("cmc/not-cmc",
549
745
  "a Full PKI Request / Response is carried in a CMS SignedData or AuthenticatedData, got " +
@@ -568,6 +764,8 @@ function parse(input) {
568
764
  }
569
765
 
570
766
  module.exports = {
767
+ parseEncryptedPop: parseEncryptedPop,
768
+ parseTaggedRequest: parseTaggedRequest,
571
769
  parse: parse,
572
770
  parsePkiData: parsePkiData,
573
771
  parsePkiResponse: parsePkiResponse,
package/lib/schema-cms.js CHANGED
@@ -555,7 +555,9 @@ var KEY_AGREE_RECIPIENT_INFO = schema.seq([
555
555
  schema.field("originator", schema.explicit(0, ORIGINATOR_IDENTIFIER_OR_KEY, { code: "cms/bad-kari" })),
556
556
  schema.optional("ukm", schema.octetString(), { tag: 1, explicit: true, emptyCode: "cms/bad-kari" }),
557
557
  schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
558
- schema.field("recipientEncryptedKeys", schema.seqOf(RECIPIENT_ENCRYPTED_KEY, { code: "cms/bad-recipient-encrypted-keys", what: "recipientEncryptedKeys" })),
558
+ /** @internal At least one encrypted key: a key-agreement recipient info that names none names no
559
+ * recipient, so nothing could open the content it is attached to. */
560
+ schema.field("recipientEncryptedKeys", schema.seqOf(RECIPIENT_ENCRYPTED_KEY, { min: 1, code: "cms/bad-recipient-encrypted-keys", what: "recipientEncryptedKeys" })),
559
561
  ], {
560
562
  assert: "constructed", code: "cms/bad-kari", what: "KeyAgreeRecipientInfo",
561
563
  build: function (m) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.47",
3
+ "version": "0.6.49",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:39d93a95-6e56-4af5-9029-94a73a8e00c4",
5
+ "serialNumber": "urn:uuid:082f242e-4786-4518-b201-8797b47662eb",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-06T15:41:11.254Z",
8
+ "timestamp": "2026-09-07T02:07:15.421Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.6.47",
22
+ "bom-ref": "@blamejs/pki@0.6.49",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.47",
25
+ "version": "0.6.49",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.6.47",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.49",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.6.47",
57
+ "ref": "@blamejs/pki@0.6.49",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]