@blamejs/pki 0.6.50 → 0.6.52

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/cmp-verify.js CHANGED
@@ -32,6 +32,10 @@ var asn1 = require("./asn1-der");
32
32
  var oid = require("./oid");
33
33
  var cmpBuild = require("./cmp-build");
34
34
  var cmp = require("./schema-cmp");
35
+ var cms = require("./schema-cms");
36
+ var cmsVerify = require("./cms-verify");
37
+ var cmsDecrypt = require("./cms-decrypt");
38
+ var pkcs8 = require("./schema-pkcs8");
35
39
  var pkix = require("./schema-pkix");
36
40
  var pbes2 = require("./pbes2");
37
41
  var x509 = require("./schema-x509");
@@ -52,6 +56,7 @@ var _certExtDecoders = pkix.certExtensionDecoders(NS).byOid;
52
56
  var KNOWN_VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), {
53
57
  sharedSecret: 1, signerCert: 1, trustAnchors: 1, intermediates: 1, time: 1,
54
58
  transactionID: 1, expectRecipNonce: 1, revocationChecker: 1, maxIterations: 1, kem: 1,
59
+ messageTimeTolerance: 1,
55
60
  });
56
61
  var KNOWN_KEM_VERIFY_KEYS = intrinsic.assign(intrinsic.create(null),
57
62
  { sharedSecret: 1, transactionID: 1 });
@@ -146,6 +151,30 @@ function _sanGeneralNames(parsed) {
146
151
  return [];
147
152
  }
148
153
 
154
+ /** @internal An anonymous sender: the NULL-DN a requester uses when it knows nothing about its own
155
+ * name (RFC 9810 sec. 5.1.1), encoded as a directoryName with no relative names. */
156
+ function _isNullDnSender(sender) {
157
+ return !!sender && sender.tagClass === "context" && sender.tagNumber === 4 &&
158
+ !!sender.value && _isArray(sender.value.rdns) && sender.value.rdns.length === 0;
159
+ }
160
+
161
+ /** @internal The commonName of a sender given as a directoryName, which is the name a MAC-protected
162
+ * message identifies its shared secret by (RFC 9483 sec. 4.1.5). A sender in any other GeneralName
163
+ * form, or one carrying no commonName, names nothing a senderKID could equal. */
164
+ function _senderCommonName(sender) {
165
+ if (!sender || sender.tagClass !== "context" || sender.tagNumber !== 4) return null;
166
+ var rdns = sender.value && sender.value.rdns;
167
+ if (!_isArray(rdns)) return null;
168
+ var cnOid = oid.byName("commonName");
169
+ for (var i = 0; i < rdns.length; i++) {
170
+ var rdn = rdns[i];
171
+ for (var j = 0; j < rdn.length; j++) {
172
+ if (rdn[j].type === cnOid && typeof rdn[j].value === "string") return rdn[j].value;
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+
149
178
  function _senderBoundToCert(sender, parsed) {
150
179
  if (!sender || !sender.bytes) return false;
151
180
  var subjectRdns = parsed.subject.rdns;
@@ -311,6 +340,15 @@ function _uriEqual(a, b) {
311
340
  return na === nb;
312
341
  }
313
342
 
343
+ /** @internal Whether a declared key identifier names this certificate. Absent means the rule does not
344
+ * apply; present with no subjectKeyIdentifier to compare means it cannot be satisfied, so the message
345
+ * is refused rather than admitted for want of a value. */
346
+ function _senderKidMatchesCert(senderKID, parsed) {
347
+ if (senderKID == null) return true;
348
+ var ski = _certSki(parsed);
349
+ return ski != null && guard.crypto.constantTimeEqual(ski, senderKID);
350
+ }
351
+
314
352
  function _resolveSignerCert(m, opts, extra) {
315
353
  var senderKID = m.header.senderKID;
316
354
  function _signerObj(der, p) { return { der: der, spki: p.subjectPublicKeyInfo.bytes, subject: _subjectDn(p), parsed: p }; }
@@ -318,10 +356,7 @@ function _resolveSignerCert(m, opts, extra) {
318
356
  var p;
319
357
  try { p = x509.parse(der); }
320
358
  catch (_e) { return null; }
321
- if (senderKID != null) {
322
- var ski = _certSki(p);
323
- if (ski == null || !guard.crypto.constantTimeEqual(ski, senderKID)) return null;
324
- }
359
+ if (!_senderKidMatchesCert(senderKID, p)) return null;
325
360
  return _signerObj(der, p);
326
361
  }
327
362
  if (opts.signerCert != null) {
@@ -361,6 +396,22 @@ function _headerChecks(m, opts) {
361
396
  if (opts.expectRecipNonce != null && !_bufEq(m.header.recipNonce, opts.expectRecipNonce)) {
362
397
  return { code: "cmp/bad-recip-nonce", reason: "header.recipNonce does not echo the expected sender nonce" };
363
398
  }
399
+ /** @internal A stated window is checked in both directions: a clock runs fast as readily as slow, and
400
+ * a message dated ahead of the receiver is as much a replay signal as one dated behind it. */
401
+ if (opts.messageTimeTolerance != null && m.header.messageTime != null) {
402
+ var at = guard.time.instantOf(opts.time != null ? opts.time : new _Date());
403
+ var sent = guard.time.instantOf(m.header.messageTime);
404
+ if (!intrinsic.isFinite(at) || !intrinsic.isFinite(sent)) {
405
+ return { code: "cmp/bad-message-time", reason: "the header messageTime could not be read as an instant to compare against the verification time (RFC 9483 sec. 3.5)" };
406
+ }
407
+ // allow:nan-date-comparison-unguarded -- both instants are finite by the check above
408
+ var skew = (at > sent ? at - sent : sent - at) / 1000;
409
+ if (skew > opts.messageTimeTolerance) {
410
+ return { code: "cmp/bad-message-time", reason: "the header messageTime is " + intrinsic.floor(skew) +
411
+ " seconds from the verification time, outside the " + opts.messageTimeTolerance +
412
+ "-second window this caller accepts (RFC 9483 sec. 3.5)" };
413
+ }
414
+ }
364
415
  return null;
365
416
  }
366
417
 
@@ -460,6 +511,23 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
460
511
  if (!guard.crypto.constantTimeEqual(computed, protection.bytes)) {
461
512
  return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 MAC does not verify (a wrong shared secret or a tampered ProtectedPart)");
462
513
  }
514
+ /** @internal Under MAC protection the sender names the shared secret, and sec. 4.1.5 requires the
515
+ * senderKID to carry that same name: the identifier and the name the message is attributed to are
516
+ * one value, so a message pointing them at different entities is refused. */
517
+ /** @internal The name rule governs a sender that HAS a name. An anonymous NULL-DN sender carries a
518
+ * reference number instead (RFC 9810 sec. 5.1.1), which names no commonName to equal, so it is
519
+ * required to be present above and left uncompared here rather than refused for failing a match the
520
+ * base protocol does not ask of it. */
521
+ if (m.header.senderKID != null && !_isNullDnSender(m.header.sender)) {
522
+ var cn = _senderCommonName(m.header.sender);
523
+ if (cn === null || !guard.crypto.constantTimeEqual(_bufferFrom(cn, "utf8"), m.header.senderKID)) {
524
+ return _fail(m, "mac", protectionAlg, "cmp/bad-sender-kid",
525
+ "the header senderKID must carry the same name as the commonName of the sender field, which " +
526
+ "is how a MAC-protected message identifies the shared secret it was protected with " +
527
+ "(RFC 9483 sec. 4.1.5)");
528
+ }
529
+ }
530
+
463
531
  var hc = _headerChecks(m, opts);
464
532
  if (hc) return _fail(m, "mac", protectionAlg, hc.code, hc.reason);
465
533
  return _ok(m, "mac", protectionAlg, true, null);
@@ -485,7 +553,17 @@ function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
485
553
  async function _verifySignature(m, protectedPart, protectionAlg, protection, opts) {
486
554
  var extra = _boundExtraCerts(m.extraCerts);
487
555
  var signer = _resolveSignerCert(m, opts, extra);
488
- if (!signer) return _fail(m, "signature", protectionAlg, "cmp/signer-cert-not-found", "no signer certificate resolved (opts.signerCert, senderKID, or extraCerts)");
556
+ if (!signer) {
557
+ /** @internal Selecting from `extraCerts` narrows by the declared key identifier, so "nothing
558
+ * matched" and "nothing was offered" are different failures. Reporting them apart keeps one
559
+ * defect from carrying two codes depending on how the certificate would have arrived. */
560
+ if (m.header.senderKID != null && extra.length) {
561
+ return _fail(m, "signature", protectionAlg, "cmp/bad-sender-kid",
562
+ "no certificate in extraCerts carries the subjectKeyIdentifier the header senderKID declares, " +
563
+ "so nothing offered identifies the key that would verify the protection (RFC 9483 sec. 3.5)");
564
+ }
565
+ return _fail(m, "signature", protectionAlg, "cmp/signer-cert-not-found", "no signer certificate resolved (opts.signerCert, senderKID, or extraCerts)");
566
+ }
489
567
 
490
568
  var ok = await _engine.verifyWithSpki(protectionAlg, protection.bytes, signer.spki, protectedPart);
491
569
  if (ok !== true) return _fail(m, "signature", protectionAlg, "cmp/protection-failed", "the protection signature does not verify over the ProtectedPart under the declared protectionAlg", signer);
@@ -493,6 +571,16 @@ async function _verifySignature(m, protectedPart, protectionAlg, protection, opt
493
571
  if (!_senderBoundToCert(m.header.sender, signer.parsed)) {
494
572
  return _fail(m, "signature", protectionAlg, "cmp/sender-mismatch", "the header sender field does not match the signer certificate subject, or (for an empty subject) a subjectAltName entry (RFC 9483 sec. 3.1, RFC 5280 sec. 7.1)", signer);
495
573
  }
574
+ /** @internal The declared key identifier is checked against the certificate whose key ACTUALLY
575
+ * verified, on every route that certificate arrives by. Selecting from `extraCerts` already matches
576
+ * it while choosing, and a caller-supplied certificate reaches the same rule here, so a message
577
+ * naming one key and signed by another is refused however the verifier came by the certificate
578
+ * (RFC 9483 sec. 3.5). */
579
+ if (!_senderKidMatchesCert(m.header.senderKID, signer.parsed)) {
580
+ return _fail(m, "signature", protectionAlg, "cmp/bad-sender-kid",
581
+ "the header senderKID does not identify the certificate whose key verified the protection: it " +
582
+ "must equal that certificate's subjectKeyIdentifier (RFC 9483 sec. 3.5)", signer);
583
+ }
496
584
 
497
585
  var hc = _headerChecks(m, opts);
498
586
  if (hc) return _fail(m, "signature", protectionAlg, hc.code, hc.reason, signer);
@@ -640,6 +728,18 @@ function _fixSecret(v, made, label) {
640
728
  return v;
641
729
  }
642
730
 
731
+ /** @internal The freshness window, in seconds. RFC 9483 sec. 3.5 leaves the threshold to the use case
732
+ * and lists the check under local policy, so it is stated by the caller rather than defaulted: a
733
+ * window this library invented would refuse conforming messages from a peer whose clock is merely
734
+ * further off than that number. */
735
+ function _fixTolerance(v) {
736
+ if (v == null) return null;
737
+ if (typeof v !== "number" || !intrinsic.isFinite(v) || v < 0) {
738
+ throw _err("cmp/bad-input", "opts.messageTimeTolerance is the accepted messageTime skew in seconds, a non-negative number");
739
+ }
740
+ return v;
741
+ }
742
+
643
743
  /** @internal The KEM verification material, taken once at the door like every other caller value: the
644
744
  * shared secret is copied and registered for wiping, and the transaction identifier is snapshot, so
645
745
  * neither can answer differently when the derivation reads it. */
@@ -675,8 +775,13 @@ function _fixCertList(v, label) {
675
775
  for (var i = 0; i < n; i++) _append(out, _fixByteish(v[i], label + "[" + i + "]"));
676
776
  return out;
677
777
  }
778
+ /** @internal The verification instant, copied so a caller cannot move it after the fact. A value that
779
+ * is not a Date is refused here rather than carried into a comparison that would throw from whichever
780
+ * check happened to read it first. */
678
781
  function _fixTime(v) {
679
- return guard.time.isDate(v) ? new _Date(guard.time.instantOf(v)) : v;
782
+ if (v == null) return v;
783
+ if (!guard.time.isDate(v)) throw _err("cmp/bad-input", "opts.time must be a Date");
784
+ return new _Date(guard.time.instantOf(v));
680
785
  }
681
786
  function _fixVerifyOptions(opts, made) {
682
787
  if (opts == null) opts = {};
@@ -693,6 +798,7 @@ function _fixVerifyOptions(opts, made) {
693
798
  revocationChecker: opts.revocationChecker,
694
799
  maxIterations: opts.maxIterations,
695
800
  kem: _fixKem(opts.kem, made),
801
+ messageTimeTolerance: _fixTolerance(opts.messageTimeTolerance),
696
802
  };
697
803
 
698
804
  _assertEchoBytes(f.transactionID, "transactionID");
@@ -738,6 +844,315 @@ async function _verify(message, opts) {
738
844
  return _verifySignature(m, protectedPart, protectionAlg, protection, opts);
739
845
  }
740
846
 
847
+ var KNOWN_OPEN_KEY_PACKAGE_OPTS = intrinsic.assign(intrinsic.create(null), {
848
+ key: 1, cert: 1, password: 1, trustAnchors: 1, intermediates: 1, time: 1,
849
+ authorizedBySharedSecret: 1,
850
+ });
851
+ var OID_A_KEY_PACKAGE = oid.byName("aKeyPackage");
852
+ var OID_SIGNED_DATA = oid.byName("signedData");
853
+ var OID_ENVELOPED_DATA = oid.byName("envelopedData");
854
+ var OID_CM_KGA = oid.byName("cmKGA");
855
+
856
+ function _keyPackageErr(reason, cause) { return _err("cmp/bad-key-package", reason, cause); }
857
+
858
+ /** @internal The three shapes sec. 4.1.6 profiles out of an EnvelopedData, read from the parsed
859
+ * container before anything is decrypted: the structure is an EnvelopedData, exactly one party can
860
+ * open it, and what it holds is a SignedData. */
861
+ function _assertKeyPackageContainer(parsed) {
862
+ if (parsed.contentTypeName !== "envelopedData") {
863
+ throw _keyPackageErr("a delivered key package is carried in a CMS EnvelopedData, and this container is a " +
864
+ parsed.contentTypeName + " (RFC 9483 sec. 4.1.6)");
865
+ }
866
+ var ris = parsed.recipientInfos;
867
+ if (!_isArray(ris) || ris.length !== 1) {
868
+ throw _keyPackageErr("a delivered key package names exactly one RecipientInfo, and this container names " +
869
+ guard.text.showValue(ris && ris.length) +
870
+ "; a second recipient is a second party able to open a key generated for this entity (RFC 9483 sec. 4.1.6)");
871
+ }
872
+ /** @internal A `KeyAgreeRecipientInfo` wraps the content-encryption key once per entry in its
873
+ * `recipientEncryptedKeys` (RFC 5652 sec. 6.2.2), each for a different recipient under one
874
+ * originator, so counting RecipientInfos alone would admit a second party under one of them. */
875
+ var reks = ris[0].recipientEncryptedKeys;
876
+ if (_isArray(reks) && reks.length !== 1) {
877
+ throw _keyPackageErr("the key agreement RecipientInfo of a delivered key package wraps the content-encryption key once, and this one wraps it " +
878
+ reks.length + " times; each is a party able to open a key generated for this entity (RFC 9483 sec. 4.1.6, RFC 5652 sec. 6.2.2)");
879
+ }
880
+ var eci = parsed.encryptedContentInfo;
881
+ if (!eci || eci.contentType !== OID_SIGNED_DATA) {
882
+ throw _keyPackageErr("the encrypted content of a delivered key package is declared id-signedData, and this container declares " +
883
+ guard.text.showValue(eci && eci.contentType) + " (RFC 9483 sec. 4.1.6)");
884
+ }
885
+ }
886
+
887
+ /** @internal RFC 5958 sec. 2: AsymmetricKeyPackage ::= SEQUENCE SIZE (1..MAX) OF OneAsymmetricKey.
888
+ * Each element is surfaced as its own DER buffer, which is what a PKCS#8 `PrivateKeyInfo` is
889
+ * (`PrivateKeyInfo ::= OneAsymmetricKey`), so a caller imports it without re-encoding. Copied off the
890
+ * decrypted content rather than sliced from it, so holding one key does not hold the package. */
891
+ var ASYMMETRIC_KEY_PACKAGE = schema.seqOf(
892
+ schema.decode(function (n, ctx) {
893
+ if (!(n.tagClass === "universal" && n.tagNumber === asn1.TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
894
+ throw ctx.E("cmp/bad-key-package", "an AsymmetricKeyPackage element must be a OneAsymmetricKey SEQUENCE (RFC 5958 sec. 2)");
895
+ }
896
+ /** @internal OneAsymmetricKey and a PKCS#8 PrivateKeyInfo are the same structure (RFC 5958
897
+ * sec. 2), so the shipped strict parser decides whether an element is one; a SEQUENCE that merely
898
+ * looks like a container is not a delivered key. */
899
+ try { pkcs8.parse(n.bytes); }
900
+ catch (e) { throw ctx.E("cmp/bad-key-package", "an AsymmetricKeyPackage element is not a OneAsymmetricKey (RFC 5958 sec. 2)", e); }
901
+ return _bufferFrom(n.bytes);
902
+ }),
903
+ { assert: "sequence", min: 1, code: "cmp/bad-key-package", what: "AsymmetricKeyPackage",
904
+ build: function (m) { return _map(m.items, function (it) { return it.value; }); } });
905
+
906
+ function _readKeyPackage(der) {
907
+ var node;
908
+ try { node = asn1.decode(der); }
909
+ catch (e) { throw _keyPackageErr("the signed content is not DER", e); }
910
+ return schema.walk(ASYMMETRIC_KEY_PACKAGE, node, NS).result;
911
+ }
912
+
913
+ function _fixOpenKeyPackageOptions(opts, made) {
914
+ opts = guard.identifier.optionsObject(opts, _err, "cmp/bad-input", "pki.cmp.openKeyPackage options");
915
+ guard.identifier.assertKnownKeys(opts, KNOWN_OPEN_KEY_PACKAGE_OPTS, _err, "cmp/bad-input", "unknown opts field ");
916
+ var f = {
917
+ key: opts.key,
918
+ cert: _fixByteish(opts.cert, "cert"),
919
+ password: _fixSecret(opts.password, made, "password"),
920
+ trustAnchors: _fixCertList(opts.trustAnchors, "trustAnchors"),
921
+ intermediates: _fixPoolCerts(opts.intermediates, "intermediates"),
922
+ time: _fixTime(opts.time),
923
+ exempt: opts.authorizedBySharedSecret === true,
924
+ };
925
+ if (opts.authorizedBySharedSecret != null && typeof opts.authorizedBySharedSecret !== "boolean") {
926
+ throw _err("cmp/bad-input", "opts.authorizedBySharedSecret is a boolean stating that this entity protected its request with the shared secret");
927
+ }
928
+ if (f.key == null && f.password == null) {
929
+ throw _err("cmp/bad-input", "opening a delivered key package needs the key material the content-encryption key was protected to: opts.key (with opts.cert) for the key transport and key agreement techniques, or opts.password for the password technique (RFC 9483 sec. 4.1.6.1 to 4.1.6.3)");
930
+ }
931
+ if (f.key != null && f.password != null) {
932
+ throw _err("cmp/bad-input", "a delivered key package is opened by one key management technique: pass opts.key or opts.password, not both");
933
+ }
934
+ /** @internal sec. 4.1.6 offers the shared-secret exemption only to an entity that protected its
935
+ * request with a MAC, which is the entity opening this container with that secret. Stating it
936
+ * alongside a private key claims the exemption for a signature-protected exchange, where the
937
+ * section's requirement to validate the signer stands. */
938
+ if (f.exempt && f.password == null) {
939
+ throw _err("cmp/bad-input", "opts.authorizedBySharedSecret authorizes the key generation authority by the secret that protected the request, so it applies only to a container opened with opts.password (RFC 9483 sec. 4.1.6)");
940
+ }
941
+ if (f.exempt && f.trustAnchors != null) {
942
+ throw _err("cmp/bad-input", "opts.authorizedBySharedSecret and opts.trustAnchors state two different rules for authorizing the key generation authority; supply one");
943
+ }
944
+ if (!f.exempt && f.trustAnchors == null) {
945
+ throw _err("cmp/bad-input", "authorizing the key generation authority needs opts.trustAnchors, or opts.authorizedBySharedSecret when this entity protected its request with the shared secret (RFC 9483 sec. 4.1.6)");
946
+ }
947
+ return f;
948
+ }
949
+
950
+ async function _openKeyPackage(container, opts) {
951
+ var bytes = _asContentInfo(
952
+ guard.bytes.snapshotSource(_coerceKeyPackageBytes(container), CmpError, "cmp/bad-input", "the delivered key package container"),
953
+ OID_ENVELOPED_DATA, "the delivered key package container");
954
+ var parsed;
955
+ try { parsed = cms.parse(bytes); }
956
+ catch (e) { throw _keyPackageErr("the delivered key package container is not a CMS ContentInfo", e); }
957
+ _assertKeyPackageContainer(parsed);
958
+
959
+ /** @internal The profile fixes the recipient count at one, so the arm to open is named instead of
960
+ * matched: a caller holding only the private key need not supply the certificate as well. */
961
+ /** @internal Every way the container fails to open reports the one code, so the verdict carries no
962
+ * more than "it did not open": `pki.cms.decrypt` already collapses each secret-dependent failure to
963
+ * a single uniform result, and splitting that back out here would rebuild the oracle it removes. */
964
+ var opened;
965
+ try {
966
+ opened = await cmsDecrypt.decrypt(bytes, opts.password != null ? { password: opts.password } : { key: opts.key, cert: opts.cert },
967
+ { recipientIndex: 0 });
968
+ } catch (e) {
969
+ throw _keyPackageErr("the delivered key package could not be opened under the supplied key material", e);
970
+ }
971
+
972
+ var verdict;
973
+ var signedData = _asContentInfo(opened.content, OID_SIGNED_DATA, "the encrypted content");
974
+ try { verdict = await cmsVerify.verify(signedData, _signedDataVerifyOpts(opts)); }
975
+ catch (e) { throw _keyPackageErr("the SignedData wrapping the key package could not be verified", e); }
976
+ if (!verdict.valid) {
977
+ throw _keyPackageErr("the signature over the delivered key package does not verify; the SignedData reports [" +
978
+ intrinsic.join(_map(verdict.signers, function (s) { return s.code || "a signature mismatch"; }), ", ") + "]");
979
+ }
980
+ if (verdict.eContentType !== OID_A_KEY_PACKAGE) {
981
+ throw _keyPackageErr("the signed content type is " + verdict.eContentType + ", not id-ct-KP-aKeyPackage; the signature must bind what it covers to a key package (RFC 9483 sec. 4.1.6, RFC 5958 sec. 2)");
982
+ }
983
+ var signerCert = verdict.signers[0] && verdict.signers[0].cert;
984
+ if (!signerCert) throw _keyPackageErr("the SignedData names no signer certificate to authorize as the key generation authority");
985
+ _authorizeKga(verdict, signerCert, opts);
986
+ if (verdict.eContent == null) throw _keyPackageErr("the SignedData carries no encapsulated content, so no key package was delivered");
987
+ return { keys: _readKeyPackage(verdict.eContent), kga: _bufferFrom(signerCert), trusted: opts.exempt ? false : true };
988
+ }
989
+
990
+ /** @internal sec. 4.1.6: the entity "MUST use a certificate containing the additional extended key
991
+ * usage extension id-kp-cmKGA in order to be accepted by the EE as a legitimate key generation
992
+ * authority", and the EE "MUST validate the signer certificate". `pki.path.validate`'s `requiredEku`
993
+ * answers the RFC 5280 sec. 4.2.1.12 question (does this certificate PERMIT the purpose), which an
994
+ * absent extension satisfies, so the assertion is checked here as well as the chain. */
995
+ function _authorizeKga(verdict, signerCert, opts) {
996
+ if (opts.exempt) return;
997
+ if (!verdict.trusted) {
998
+ throw _err("cmp/unauthorized-kga", "the certificate that signed the delivered key package does not chain to an accepted trust anchor (RFC 9483 sec. 4.1.6)");
999
+ }
1000
+ var parsedSigner;
1001
+ try { parsedSigner = x509.parse(signerCert); }
1002
+ catch (e) { throw _err("cmp/unauthorized-kga", "the certificate that signed the delivered key package could not be read", e); }
1003
+ if (!pkix.assertsPurpose(NS, parsedSigner, OID_CM_KGA)) {
1004
+ throw _err("cmp/unauthorized-kga", "the certificate that signed the delivered key package does not assert the id-kp-cmKGA extended key usage, which is what a key generation authority uses to show it is one (RFC 9483 sec. 4.1.6, RFC 9480 sec. 2.2)");
1005
+ }
1006
+ }
1007
+
1008
+ function _signedDataVerifyOpts(opts) {
1009
+ if (opts.exempt) return {};
1010
+ var v = { trustAnchors: opts.trustAnchors, requiredEku: [OID_CM_KGA] };
1011
+ if (opts.intermediates != null) v.certs = opts.intermediates;
1012
+ if (opts.time != null) v.time = opts.time;
1013
+ return v;
1014
+ }
1015
+
1016
+ /** @internal The certificate options here take one certificate or a list of them, as DER or PEM, which
1017
+ * is the shape `pki.cmp.verify` takes. `pki.cms.verify` takes `certs` as a list of DER, so the pool is
1018
+ * normalized at the door: a single certificate forwarded unwrapped would be walked as a sequence of
1019
+ * its own bytes and contribute nothing, and a PEM string would not be read at all. */
1020
+ function _fixPoolCerts(v, label) {
1021
+ if (v == null) return v;
1022
+ var list = _fixCertList(_isArray(v) ? v : [v], label);
1023
+ return _map(list, function (c, i) {
1024
+ if (typeof c !== "string") return c;
1025
+ try { return x509.pemDecode(c); }
1026
+ catch (e) { throw _err("cmp/bad-input", "opts." + label + "[" + i + "] PEM could not be decoded", e); }
1027
+ });
1028
+ }
1029
+
1030
+ /** @internal Both CMS layers arrive in either of two forms, and the two are structurally exclusive so
1031
+ * neither is guessed at. A ContentInfo opens with an OBJECT IDENTIFIER naming its content type; the
1032
+ * bare structure opens with its own `CMSVersion` INTEGER. Both forms are seen in practice. A CMP
1033
+ * `EncryptedKey` carries the `EnvelopedData` itself (RFC 9810 sec. 5.2.2), and RFC 5652 sec. 6.1
1034
+ * encrypts the CONTENT rather than a ContentInfo around it, so the plaintext under an `id-signedData`
1035
+ * `encryptedContentInfo` is a `SignedData`. Each is wrapped so one code path reads both. Anything
1036
+ * else is left alone for the ContentInfo parse to refuse by name. */
1037
+ function _asContentInfo(der, contentTypeOid, what) {
1038
+ var node;
1039
+ try { node = asn1.decode(der); }
1040
+ catch (e) { throw _keyPackageErr(what + " is not DER", e); }
1041
+ var first = node.constructed && node.children ? node.children[0] : null;
1042
+ if (!first || first.tagClass !== "universal" || first.tagNumber !== asn1.TAGS.INTEGER) return der;
1043
+ return b.sequence([b.oid(contentTypeOid), b.explicit(0, b.raw(der))]);
1044
+ }
1045
+
1046
+ function _coerceKeyPackageBytes(container) {
1047
+ if (typeof container === "string") {
1048
+ try { return cms.pemDecode(container); }
1049
+ catch (e) { throw _err("cmp/bad-input", "the delivered key package container PEM could not be decoded", e); }
1050
+ }
1051
+ return container;
1052
+ }
1053
+
1054
+ /**
1055
+ * @primitive pki.cmp.openKeyPackage
1056
+ * @signature pki.cmp.openKeyPackage(container, opts) -> Promise<{ keys, kga, trusted }>
1057
+ * @since 0.6.52
1058
+ * @status stable
1059
+ * @spec RFC 9483 sec. 4.1.6, RFC 5958, RFC 5652, RFC 9480 sec. 2.2
1060
+ * @defends unauthorized-key-generation-authority (CWE-863)
1061
+ * @related pki.cmp.session, pki.cms.decrypt, pki.cms.verify
1062
+ *
1063
+ * Open a centrally generated private key delivered in a CMP response (RFC 9483 sec. 4.1.6). A CA that
1064
+ * generates the key pair on the end entity's behalf puts it in the `privateKey` field of the granted
1065
+ * `CertifiedKeyPair`: an RFC 5958 `AsymmetricKeyPackage`, signed by the Key Generation Authority in a
1066
+ * CMS `SignedData`, sealed to this entity in a CMS `EnvelopedData`. `container` is those
1067
+ * `EnvelopedData` bytes (DER `Buffer` or PEM), which `pki.cmp.session` surfaces as
1068
+ * `deliveredKey` when the session is told to accept one.
1069
+ *
1070
+ * The two layers are undone in order and the authority is authorized BEFORE any key material is
1071
+ * returned. Which key management technique opens the container follows the protection of the request
1072
+ * that asked for the key: `opts.key` (with `opts.cert`) for the key transport and key agreement
1073
+ * techniques a signature-protected request selects (sec. 4.1.6.1, sec. 4.1.6.2), `opts.password` for
1074
+ * the password technique a MAC-protected request selects (sec. 4.1.6.3).
1075
+ *
1076
+ * Authorization is the point of the verb. The section requires the signer's certificate to carry the
1077
+ * `id-kp-cmKGA` extended key usage "in order to be accepted by the EE as a legitimate key generation
1078
+ * authority", so a certificate that merely PERMITS the purpose by carrying no `extendedKeyUsage` at
1079
+ * all does not authorize: the assertion must be present, and the certificate must chain to an anchor
1080
+ * in `opts.trustAnchors`. The one exception is the section's own: an entity that protected its request
1081
+ * with a shared secret "MAY omit the validation" and authorize the authority by that secret instead.
1082
+ * That is stated with `opts.authorizedBySharedSecret`, it applies only to a container opened with
1083
+ * `opts.password`, and the result then reports `trusted: false`.
1084
+ *
1085
+ * Returns `{ keys, kga, trusted }`. `keys` is every `OneAsymmetricKey` in the package, in order, each
1086
+ * as its own DER `Buffer` that is a PKCS#8 `PrivateKeyInfo` ready for `pki.key.import`. `kga` is the
1087
+ * signer certificate whose signature was checked. `trusted` says whether that certificate chained.
1088
+ * Malformed input, an unopenable container, an unverifiable signature, or an unauthorized authority
1089
+ * all throw a typed `CmpError`; no key material is returned on any of them.
1090
+ *
1091
+ * The container's shape is held to the profile where the shape decides who reads the key or what the
1092
+ * signature covers: it must be an `EnvelopedData`, it must name exactly one `RecipientInfo`, its
1093
+ * encrypted content type must be `id-signedData`, and the signed content type must be
1094
+ * `id-ct-KP-aKeyPackage`. The profile's CMS version literals are not enforced, because RFC 5652
1095
+ * sec. 5.1 and sec. 6.1 derive those from the signer and recipient forms in use and a conforming
1096
+ * sender reaches different values. Its choice-of-form rules, the signer identifier form and the
1097
+ * ordering of the certificates the SignedData carries, are not judged either. Both live inside the
1098
+ * encrypted SignedData rather than in the `container` bytes, and the result carries only the signer
1099
+ * certificate, so a caller holding a stricter profile cannot judge them from this verdict.
1100
+ *
1101
+ * `keys` is the whole RFC 5958 `AsymmetricKeyPackage`, which is `SIZE (1..MAX)`, so a package carrying
1102
+ * several is opened and every key returned. RFC 9483 sec. 4.1.6 profiles that to a sequence of one,
1103
+ * and where the count decides something it is held: a `pki.cmp.session` enrollment requires exactly
1104
+ * one, since one grant certifies one key and every further key is one the issued certificate says
1105
+ * nothing about. A caller using this verb on its own owns that binding.
1106
+ *
1107
+ * @opts
1108
+ * - `key` (Buffer|PEM|CryptoKey) -- this entity's private key, for a container opened by the key
1109
+ * transport or key agreement technique.
1110
+ * - `cert` (Buffer|PEM) -- the certificate for `key`. Optional: the profile fixes the recipient
1111
+ * count at one, so the arm is named rather than matched.
1112
+ * - `password` (string|Buffer) -- the shared secret, for a container opened by the password
1113
+ * technique. Copied at the door and wiped when the call returns.
1114
+ * - `trustAnchors` (Buffer|Buffer[]|PEM) -- the roots this entity accepts for the key generation
1115
+ * authority. REQUIRED unless `authorizedBySharedSecret` is set.
1116
+ * - `intermediates` (Buffer|Buffer[]|PEM) -- extra untrusted pool certificates for path building.
1117
+ * - `time` (Date) -- the validity instant for path validation. Defaults to the current time.
1118
+ * - `authorizedBySharedSecret` (boolean) -- authorize the authority by the secret that protected
1119
+ * the request rather than by a chain, which sec. 4.1.6 permits a MAC-protected exchange. Requires
1120
+ * `opts.password` and refuses `opts.trustAnchors`.
1121
+ * @example
1122
+ * var nb = new Date("2026-01-01T00:00:00Z"), na = new Date("2036-01-01T00:00:00Z");
1123
+ * var kgaPair = await pki.key.generate("Ed25519"); // the key generation authority
1124
+ * var kgaKey = await pki.key.export(kgaPair.privateKey);
1125
+ * var kgaCert = await pki.x509.sign({ subject: "Key Generation Authority",
1126
+ * subjectPublicKey: await pki.key.export(kgaPair.publicKey), notBefore: nb, notAfter: na,
1127
+ * extensions: { basicConstraints: { cA: true }, keyUsage: ["digitalSignature", "keyCertSign"],
1128
+ * extendedKeyUsage: ["cmKGA"], subjectKeyIdentifier: true } }, { key: kgaKey });
1129
+ * var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
1130
+ * var eePair = await pki.key.generate(rsa); // the enrolling end entity
1131
+ * var eeKey = await pki.key.export(eePair.privateKey);
1132
+ * var eeCert = await pki.x509.sign({ subject: "Enrolling client",
1133
+ * subjectPublicKey: await pki.key.export(eePair.publicKey), notBefore: nb, notAfter: na,
1134
+ * extensions: { keyUsage: ["keyEncipherment"] } }, { key: eeKey });
1135
+ * var generated = await pki.key.export((await pki.key.generate("Ed25519")).privateKey);
1136
+ * var signedPackage = await pki.cms.sign(pki.asn1.build.sequence([pki.asn1.build.raw(generated)]),
1137
+ * [{ key: kgaKey, cert: kgaCert }], { eContentType: "aKeyPackage", sid: "ski" });
1138
+ * var deliveredKeyDer = await pki.cms.encrypt(signedPackage, [{ cert: eeCert }],
1139
+ * { contentEncryptionAlgorithm: "aes-256-cbc", contentType: "signedData" });
1140
+ * var opened = await pki.cmp.openKeyPackage(deliveredKeyDer,
1141
+ * { key: eeKey, cert: eeCert, trustAnchors: [kgaCert] });
1142
+ * opened.keys.length; // 1
1143
+ * var privateKey = await pki.key.import(opened.keys[0]);
1144
+ */
1145
+ function openKeyPackage(container, opts) {
1146
+ var made = [];
1147
+ return guard.async.deferred(async function () {
1148
+ try {
1149
+ return await _openKeyPackage(container, _fixOpenKeyPackageOptions(opts, made));
1150
+ } finally {
1151
+ guard.secret.zeroizeAll(made, CmpError, "cmp/bad-input", "the copied key package shared secret");
1152
+ }
1153
+ });
1154
+ }
1155
+
741
1156
  /**
742
1157
  * @primitive pki.cmp.verify
743
1158
  * @signature pki.cmp.verify(message, opts?) -> Promise<verdict>
@@ -763,6 +1178,14 @@ async function _verify(message, opts) {
763
1178
  * carry at least 128 bits; a violation is a `{ valid: false }` verdict carrying `cmp/unsupported-version`,
764
1179
  * `cmp/bad-transaction-id`, or `cmp/bad-sender-nonce`.
765
1180
  *
1181
+ * A present `senderKID` must identify the key material that verified the protection (RFC 9483
1182
+ * sec. 3.5), whichever way that key was resolved. Under signature protection it must equal the
1183
+ * `subjectKeyIdentifier` of the certificate whose key verified, so a certificate supplied as
1184
+ * `opts.signerCert` is held to the declared identifier exactly as one selected out of `extraCerts` is,
1185
+ * and a certificate carrying no `subjectKeyIdentifier` cannot satisfy one. Under MAC protection it must
1186
+ * carry the same name as the `commonName` of the sender field, which is how such a message names the
1187
+ * shared secret it was protected with (sec. 4.1.5). A mismatch is a `cmp/bad-sender-kid` verdict.
1188
+ *
766
1189
  * Returns a verdict (never a bare boolean): `{ valid, trusted, protectionType, protectionAlg, signer,
767
1190
  * transactionID, senderNonce, recipNonce, header, body, code?, reason? }`. `valid` is whether the
768
1191
  * protection is cryptographically intact under the declared algorithm; `trusted` is whether a MAC secret
@@ -775,6 +1198,10 @@ async function _verify(message, opts) {
775
1198
  *
776
1199
  * @opts
777
1200
  * - `sharedSecret` (string|Buffer) -- the PBMAC1 secret; REQUIRED for a MAC-protected message (UTF-8).
1201
+ * - `messageTimeTolerance` (number) -- the accepted `messageTime` skew in seconds. RFC 9483
1202
+ * sec. 3.5 requires a present `messageTime` to be close to the receiver's current time but leaves
1203
+ * the threshold to the use case, so stating it turns the check on; a message outside the window in
1204
+ * either direction is a `cmp/bad-message-time` verdict. Absent, the `messageTime` is not judged.
778
1205
  * - `kem` ({ sharedSecret, transactionID? }) -- REQUIRED for a KEM-protected message (RFC 9810
779
1206
  * sec. 5.1.3.4). `sharedSecret` is what this side encapsulated to the sender's KEM public key, and
780
1207
  * `transactionID` names the message that carried the ciphertext when it was not this one. The
@@ -815,6 +1242,7 @@ module.exports = {
815
1242
  transfer: cmpBuild.transfer,
816
1243
  wellKnownUrl: cmpBuild.wellKnownUrl,
817
1244
  verify: verify,
1245
+ openKeyPackage: openKeyPackage,
818
1246
  setEngine: setEngine,
819
1247
  senderBoundToCert: _senderBoundToCert,
820
1248
  };
package/lib/guard-name.js CHANGED
@@ -332,6 +332,55 @@ function _hostConstraintBody(s) {
332
332
  return _charAt(h, 0) === "." ? _strSlice(h, 1) : h;
333
333
  }
334
334
 
335
+ /** @internal A label with nothing in it, which is what makes a subtree base unmatchable. The
336
+ * comparisons in lib/path-validate.js compare a base against a name by suffix, so a base carrying an
337
+ * empty label asks for a name carrying one, and a name that carries one is not a name. */
338
+ function _hasEmptyLabel(s) {
339
+ if (s.length === 0) return true;
340
+ if (_charAt(s, 0) === "." || _charAt(s, s.length - 1) === ".") return true;
341
+ return _indexOf(s, "..") !== -1;
342
+ }
343
+
344
+ /** @internal Whether the comparison can read a constraint base at all, which is a narrower question
345
+ * than whether a caller may state one. The door holds a caller to a well-formed host name; the
346
+ * comparison asks only whether an empty label leaves the base matching no name, and answers such a
347
+ * base with no verdict rather than with no match, since an excluded subtree that matches nothing
348
+ * excludes nothing. The two differ in both directions. A label carrying an underscore or edged with a
349
+ * hyphen is not a host name the door accepts, and the suffix comparison still applies it to exactly
350
+ * the names it names, so it stays enforced. The reduction here is the comparison's own, so the two
351
+ * cannot part company over a base like "..", whose body is empty only once a leading dot the
352
+ * comparison keeps has been taken off.
353
+ *
354
+ * The rule follows the comparison's own branches rather than the tag alone, since one tag can be
355
+ * compared two ways. An rfc822Name base carrying an at-sign is matched exactly and is read as a
356
+ * mailbox; one without an at-sign constrains the host and is read as a subtree, as a dNSName base is.
357
+ *
358
+ * A base that reduces to nothing is read by tag, because the comparisons do not agree on it. For
359
+ * dNSName it names the root of the namespace and `hostConstraintMatch` matches every name, so it is
360
+ * readable. For rfc822Name the same reduction leaves no domain to compare and `emailMatch` matches no
361
+ * mailbox, and for uniformResourceIdentifier `uriMatch` reads no host, so both are unreadable.
362
+ * @enforced-by behavioral -- the rule has no rename-proof code shape, and the RED vectors in
363
+ * path-validate.test.js driving a CA's own nameConstraints extension are the guard. */
364
+ function constraintBaseUnreadable(tag, base) {
365
+ if (typeof base !== "string") return true;
366
+ /** @internal A base carrying an at-sign names one mailbox, and `emailMatch` compares it to a name
367
+ * exactly, local part and domain alike, rather than as a subtree. Each half is therefore read on
368
+ * its own terms: the local part by the RFC 5321 sec. 4.1.2 Dot-string or Quoted-string rule, which
369
+ * admits an underscore and refuses an empty atom, and the domain by the empty-label rule. A
370
+ * bracketed address literal carries no empty label in either accepted form, so the domain rule
371
+ * passes it through to the comparison, which reads it whole. */
372
+ if (tag === 1 && _lastIndexOf(base, "@") !== -1) {
373
+ var mb = splitMailbox(base);
374
+ if (mb === null || mb === "ambiguous") return true;
375
+ if (!_isMailboxLocal(mb[0])) return true;
376
+ return _hasEmptyLabel(stripTrailingDot(mb[1]));
377
+ }
378
+ var body = stripTrailingDot(base);
379
+ if (body === "") return tag !== 2;
380
+ if (_charAt(body, 0) === ".") body = _strSlice(body, 1);
381
+ return _hasEmptyLabel(body);
382
+ }
383
+
335
384
  /** @internal Why a name-constraint base cannot be enforced, or null when it can. A base outside the
336
385
  * form its tag names is not a narrower namespace: an empty dNSName base matches every name, and a
337
386
  * base holding characters no host name can hold matches none, so the same value that reads as a
@@ -358,11 +407,19 @@ function constraintBaseRefusal(tag, base) {
358
407
  return _isHostName(_hostConstraintBody(base)) ? null
359
408
  : "must be a host name, optionally written as a subtree with a leading dot";
360
409
  }
410
+ /** @internal RFC 5280 sec. 4.2.1.10 says of a URI constraint: "The constraint MUST be specified as
411
+ * a fully qualified domain name and MAY specify a host or a domain. Examples would be
412
+ * host.example.com and .example.com." Its dNSName paragraph states no such requirement, so a single
413
+ * label is a valid dNSName base and is not a valid URI base. A caller meets this rule at the door,
414
+ * whether through `pki.trust.anchor` or through the validator's subtree seeds. A base a certificate
415
+ * carries in its own extension never reaches a door, and `uriMatch` is what holds it to the rule
416
+ * there, answering `path/name-constraint-unsupported` for a host it cannot read as fully qualified.
417
+ * `constraintBaseUnreadable` does not judge label count and does not stand in for that check. */
361
418
  if (tag === 6) {
362
419
  var h = _hostConstraintBody(_toLowerCase(base));
363
420
  var dotted = _isHostName(h) && _indexOf(h, ".") !== -1 && !_allCharsIn(h, _DIGDOT_TABLE);
364
421
  return dotted ? null
365
- : "must be a dotted host name, which is the part of a URI a certificate is compared by";
422
+ : "must be a fully qualified domain name, which RFC 5280 sec. 4.2.1.10 requires of a URI constraint and not of a dNSName one";
366
423
  }
367
424
  return null;
368
425
  }
@@ -473,4 +530,5 @@ module.exports = intrinsic.freeze({
473
530
  lowerAscii: lowerAscii,
474
531
  stripTrailingDot: stripTrailingDot, isFqdnHost: isFqdnHost, splitMailbox: splitMailbox,
475
532
  constraintBaseRefusal: constraintBaseRefusal,
533
+ constraintBaseUnreadable: constraintBaseUnreadable,
476
534
  });