@blamejs/pki 0.5.14 → 0.5.16

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.
@@ -33,6 +33,9 @@ var x509 = require("./schema-x509");
33
33
  var signScheme = require("./sign-scheme");
34
34
  var guard = require("./guard-all");
35
35
  var pkix = require("./schema-pkix");
36
+ var schema = require("./schema-engine");
37
+ require("./path-validate"); // side-effect: path-validate injects its signature engine into attrcert-verify at load
38
+ var attrcertVerify = require("./attrcert-verify");
36
39
  var pkiBuild = require("./pki-build");
37
40
  var frameworkError = require("./framework-error");
38
41
 
@@ -364,6 +367,21 @@ function _buildAttributes(attrSpec) {
364
367
 
365
368
  // extensions ::= SEQUENCE OF Extension (RFC 5755 sec. 4.2.9): the recognized object form, or an array
366
369
  // of pre-encoded Extension DER. At most one instance of a particular extension OID. Returns a universal
370
+ // RFC 5755 sec. 6 defines two revocation schemes and closes by making them exclusive: "An AC MUST
371
+ // NOT contain both a noRevAvail extension and a 'pointer in AC'." Held over the OIDs an AC ends up
372
+ // carrying, so BOTH extension forms meet it -- the object form and the pre-encoded array form, which
373
+ // is the one that can name a pointer at all. `seen` is the OID-keyed set each branch already builds.
374
+ function _assertRevocationSchemesExclusive(seen) {
375
+ if (!Object.prototype.hasOwnProperty.call(seen, O("noRevAvail"))) return;
376
+ var names = Object.keys(REVOCATION_POINTERS);
377
+ for (var i = 0; i < names.length; i++) {
378
+ if (Object.prototype.hasOwnProperty.call(seen, names[i])) {
379
+ throw _err("attrcert/bad-input", "an attribute certificate must not carry both noRevAvail and a " +
380
+ REVOCATION_POINTERS[names[i]] + " revocation pointer (RFC 5755 sec. 6)");
381
+ }
382
+ }
383
+ }
384
+
367
385
  // SEQUENCE OF Extension, or null when none are requested.
368
386
  function _buildExtensions(extSpec, aaSpki) {
369
387
  if (extSpec == null) return null;
@@ -392,6 +410,7 @@ function _buildExtensions(extSpec, aaSpki) {
392
410
  } catch (err) { if (err instanceof AttrCertError) throw err; throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
393
411
  return b.raw(der);
394
412
  });
413
+ _assertRevocationSchemesExclusive(seenA);
395
414
  return b.sequence(exts);
396
415
  }
397
416
  if (typeof extSpec !== "object") throw _err("attrcert/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
@@ -406,6 +425,7 @@ function _buildExtensions(extSpec, aaSpki) {
406
425
  seen[eOid] = true;
407
426
  out.push(_b.ext(eOid, meta.critical, _extensionValue(k, extSpec[k], aaSpki)));
408
427
  });
428
+ _assertRevocationSchemesExclusive(seen);
409
429
  if (!out.length) return null;
410
430
  return b.sequence(out);
411
431
  }
@@ -429,7 +449,10 @@ function _buildExtensions(extSpec, aaSpki) {
429
449
  * targetInformation / noRevAvail / aaControls / acProxying / authorityKeyIdentifier, or an array of
430
450
  * pre-encoded Extension DER). `issuer` is the signing AA: `{ cert, key }` (the AA certificate DER/PEM
431
451
  * and its private key) or `{ name, publicKey, key }` (an explicit issuer DN, AA SPKI DER, and key); an
432
- * attribute certificate is never self-signed. The signature algorithm is resolved from the AA key (RSA
452
+ * attribute certificate is never self-signed. RFC 5755 section 6 defines two revocation schemes and
453
+ * makes them exclusive -- "An AC MUST NOT contain both a noRevAvail extension and a 'pointer in AC'"
454
+ * -- so `noRevAvail` beside a `crlDistributionPoints` or `authorityInfoAccess` extension is refused,
455
+ * in the named form and the pre-encoded form alike. The signature algorithm is resolved from the AA key (RSA
433
456
  * PKCS#1 v1.5 or PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm), and the signature is verified
434
457
  * under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE
435
458
  * CERTIFICATE` with `opts.pem`. Malformed input throws a typed `AttrCertError`; where the spec carries
@@ -517,4 +540,490 @@ function _sign(spec, issuer, opts) {
517
540
  });
518
541
  }
519
542
 
520
- module.exports = { sign: sign };
543
+ var KNOWN_VERIFY_OPTS = { time: 1, target: 1, revocationStatus: 1 };
544
+ // The two answers a caller can give for RFC 5755 sec. 6, spelled out so a typo is refused instead of
545
+ // read as the safe one. Built without a prototype, so an inherited name cannot answer the lookup.
546
+ var REVOCATION_STATUS = Object.create(null);
547
+ REVOCATION_STATUS.notRevoked = 1;
548
+ REVOCATION_STATUS.revoked = 1;
549
+ var _NAME_SCHEMA = pkix.name(NS);
550
+
551
+ // A Name DER, read through the one Name reader, so every distinguished name this verb compares is
552
+ // in the shape guard.name.dnEqual takes.
553
+ function _readName(der, what) {
554
+ try { return schema.walk(_NAME_SCHEMA, asn1.decode(der), NS).result; }
555
+ catch (e) {
556
+ if (e instanceof AttrCertError) throw e;
557
+ throw _err("attrcert/bad-input", what + " is not a readable distinguished name", e);
558
+ }
559
+ }
560
+
561
+ // The AC issuer's distinguished name. The parser has already established that issuerName holds
562
+ // exactly one non-empty directoryName [4] (RFC 5755 sec. 4.2.3) and surfaces it as a raw node, so
563
+ // the DN itself is one EXPLICIT unwrap away.
564
+ function _acIssuerName(parsed) {
565
+ var node = asn1.decode(parsed.issuer.v2Form.issuerName.names[0].bytes);
566
+ if (!node.children || node.children.length !== 1) {
567
+ throw _err("attrcert/bad-issuer-name", "the AC issuerName directoryName does not wrap exactly one Name");
568
+ }
569
+ return _readName(node.children[0].bytes, "the AC issuer name");
570
+ }
571
+
572
+ function _coerceAc(input) {
573
+ return guard.parsed.acceptDerived(input, "attributeCertificate", attrcert.parse, _err,
574
+ "attrcert/bad-input", "the attribute certificate");
575
+ }
576
+
577
+ // The AC issuer this caller directly trusts as an AC issuer (RFC 5755 sec. 5, item 4). That is a
578
+ // configuration decision the certificate cannot make for itself, so it is an argument, and an
579
+ // argument this verb refuses to infer.
580
+ function _resolveIssuer(issuer) {
581
+ if (issuer == null || typeof issuer !== "object" || Buffer.isBuffer(issuer)) {
582
+ throw _err("attrcert/bad-input", "an issuer { name, publicKey } is required -- RFC 5755 sec. 5 item 4 makes trusting an AC issuer the verifier's own configuration");
583
+ }
584
+ guard.identifier.assertKnownKeys(issuer, { name: 1, publicKey: 1 }, _err, "attrcert/bad-input",
585
+ "the issuer has an unknown key ");
586
+ if (issuer.publicKey == null) throw _err("attrcert/bad-input", "issuer.publicKey (the AC issuer's SubjectPublicKeyInfo DER) is required");
587
+ if (issuer.name == null) {
588
+ throw _err("attrcert/bad-input", "issuer.name (the distinguished name this verifier trusts as an AC issuer) is required");
589
+ }
590
+ // Every name form pki.attrcert.sign accepts for the issuing AA is a name this verb has to be able
591
+ // to trust: a string, an array of RDNs, or raw Name DER. The forms come from the one Name encoder
592
+ // both sides use, so an AC issued under a multi-RDN AA name is verifiable under that same name.
593
+ //
594
+ // The trusted name is compared as a distinguished name, never as a string: RFC 5280 sec. 7.1
595
+ // identity is not string equality, and guard.name owns that comparison. Encoding the caller's
596
+ // name and reading it back through the SAME Name reader the parser used puts both sides in the
597
+ // one shape dnEqual takes.
598
+ var trustedDer;
599
+ try { trustedDer = _b.encodeName(issuer.name); }
600
+ catch (e) {
601
+ if (e instanceof AttrCertError) throw e;
602
+ throw _err("attrcert/bad-input", "issuer.name is not a distinguished name", e);
603
+ }
604
+ var rdns = _readName(trustedDer, "issuer.name").rdns;
605
+ // Checked on the decoded name, so every input form is held to it. An empty DN names no issuer, and
606
+ // RFC 5755 sec. 4.2.3 requires the AC's own issuerName to be a non-empty distinguished name, so an
607
+ // empty trusted name could never match a conformant AC and would only mask the caller's mistake.
608
+ if (!rdns.length) {
609
+ throw _err("attrcert/bad-input", "issuer.name is an empty distinguished name, which names no AC issuer (RFC 5755 sec. 4.2.3)");
610
+ }
611
+ return {
612
+ rdns: rdns,
613
+ // Copied, never viewed. Signature verification suspends this call, and the signature engine
614
+ // holds this buffer across that suspension -- a composite arm keeps slices of it and imports
615
+ // them only after its digest settles. A view would leave the trust key writable for that whole
616
+ // window, so a caller could hand in an unrelated key of the same length, overwrite it with the
617
+ // real issuer's while the digest ran, and be told the certificate verified under the key they
618
+ // named. The verdict answers for the key the call was made with.
619
+ spki: guard.bytes.snapshot(issuer.publicKey, AttrCertError, "attrcert/bad-input", "issuer.publicKey"),
620
+ };
621
+ }
622
+
623
+ // RFC 5755 sec. 4.3.2: a targeted AC is valid only at the servers it names, and "servers not named
624
+ // MUST reject the AC". An absent extension means untargeted, so there is nothing to check.
625
+ //
626
+ // Only the decoded targetName and targetGroup alternatives are compared. Searching the extension's
627
+ // raw bytes for the caller's encoded GeneralName would also match a name nested inside a targetCert,
628
+ // where it identifies the TARGET CERTIFICATE'S ISSUER rather than a target, and would then accept an
629
+ // AC at a server it never named. A match against a targetGroup does count: sec. 4.3.2 targets "named
630
+ // targets or groups", so a verifier presenting its group name is naming itself among them.
631
+ //
632
+ // The caller's GeneralName goes through the shared builder, then back through the SAME GeneralName
633
+ // reader the parser used, so both sides of every comparison are the one decoded shape. Reading it
634
+ // back rather than dereferencing a context node's `content` also keeps this off a field that is
635
+ // null whenever the node is constructed.
636
+ function _callerTarget(target) {
637
+ return attrcert.readGeneralName(_b.encodeGeneralName(target), "attrcert/bad-input");
638
+ }
639
+
640
+ // RFC 5280 sec. 7.5: two mailboxes match when the local-part is an exact match AND the host-part
641
+ // matches case-insensitively. The split is on the LAST "@", since a quoted local-part may contain
642
+ // one; a value with no "@" has no host-part to fold and compares exactly.
643
+ function _mailboxEqual(a, bx) {
644
+ var i = a.lastIndexOf("@"), j = bx.lastIndexOf("@");
645
+ if (i < 0 || j < 0) return a === bx;
646
+ return a.slice(0, i) === bx.slice(0, j) &&
647
+ a.slice(i + 1).toLowerCase() === bx.slice(j + 1).toLowerCase();
648
+ }
649
+
650
+ // Whether a target the AC names is this verifier, under the matching rule each GeneralName form
651
+ // defines. A form whose rule this build does not implement compares exactly, which can refuse a
652
+ // name a fuller comparison would accept and can never accept one it would refuse.
653
+ function _generalNameMatches(entry, want) {
654
+ if (!entry || entry.tagNumber !== want.tagNumber) return false;
655
+ switch (entry.tagNumber) {
656
+ // RFC 5280 sec. 7.2: "When comparing DNS names for equality, conforming implementations MUST
657
+ // perform a case-insensitive exact match on the entire DNS name."
658
+ case 2: return String(entry.value).toLowerCase() === String(want.value).toLowerCase();
659
+ // sec. 7.5, as above.
660
+ case 1: return _mailboxEqual(String(entry.value), String(want.value));
661
+ // A directoryName is a distinguished name, and sec. 7.1 identity is not string equality, so it
662
+ // goes through the one DN comparator rather than comparing the printed form.
663
+ case 4:
664
+ return !!(entry.value && want.value) &&
665
+ guard.name.dnEqual(entry.value.rdns, want.value.rdns, _err, "attrcert/bad-input", "the target name");
666
+ // sec. 7.4 makes a URI comparison a full RFC 3987 normalization: percent-encoding, path
667
+ // segments and scheme-based rules, not merely lowercasing the scheme and host. A partial
668
+ // normalization would be a guess that could match a URI it should not, so the encoded value is
669
+ // compared as it stands. Re-open condition: an RFC 3987 normalizer in the toolkit.
670
+ default:
671
+ if (Buffer.isBuffer(entry.value) && Buffer.isBuffer(want.value)) return entry.value.equals(want.value);
672
+ if (typeof entry.value === "string" && typeof want.value === "string") return entry.value === want.value;
673
+ // A form whose decoded value is neither (otherName, ediPartyName, x400Address) has no
674
+ // comparison here; the caller is told so rather than given a silent mismatch.
675
+ return null;
676
+ }
677
+ }
678
+
679
+ // `want` is the caller's target already decoded, so this runs against the target the call was made
680
+ // with. See _fixVerifyOptions.
681
+ function _checkTargeting(ac, want) {
682
+ var ext = null;
683
+ for (var i = 0; i < ac.extensions.length; i++) {
684
+ if (ac.extensions[i].name === "targetInformation") ext = ac.extensions[i];
685
+ }
686
+ if (!ext) return { checked: true, ok: true };
687
+ if (want === undefined) {
688
+ return { checked: false, ok: false,
689
+ reason: "this attribute certificate is targeted, and no opts.target names this verifier (RFC 5755 sec. 4.3.2)" };
690
+ }
691
+ // The extension's syntax is a SEQUENCE OF Targets, and its groups are FLATTENED rather than
692
+ // required one by one. RFC 5755 sec. 4.3.2: "If more than one Targets element is found in an AC,
693
+ // the extension MUST be treated as if all Target elements had been found within one Targets
694
+ // element." An issuer conforming to the same paragraph produces only one group anyway, so the
695
+ // nesting carries no meaning to honor; requiring a match in every group would refuse ACs the
696
+ // paragraph says to accept.
697
+ var groups = ext.decoded || [];
698
+ // sec. 4.3.2 defines the check exactly: it "passes if the current server (recipient) is one of the
699
+ // targetName fields in the Targets SEQUENCE, or if the current server is a member of one of the
700
+ // targetGroup fields". The third arm is excluded by the same section -- "The targetCert CHOICE
701
+ // within the Target structure is only present to allow future compatibility with [X.509-2000] and
702
+ // MUST NOT be used" -- and the names inside it identify a certificate, not a target.
703
+ //
704
+ // An AC using it is refused before any match is considered, the same way sec. 6's "An AC MUST NOT
705
+ // contain both a noRevAvail extension and a 'pointer in AC'" is enforced here. Both are MUST NOTs
706
+ // the issuer broke, and a certificate that breaks its own profile is refused rather than partly
707
+ // honored: passing it because some OTHER entry happened to match would resolve the violation in
708
+ // the accepting direction, and would leave the verdict depending on which entries an attacker
709
+ // chose to include alongside the forbidden one. The targets check itself never runs, so its slot
710
+ // says so.
711
+ for (var g0 = 0; g0 < groups.length; g0++) {
712
+ for (var t0 = 0; t0 < groups[g0].length; t0++) {
713
+ if (groups[g0][t0].kind !== "targetCert") continue;
714
+ return { checked: false, ok: false,
715
+ reason: "this attribute certificate carries a targetCert target, which RFC 5755 sec. 4.3.2 says MUST NOT be used, so the targeting check did not run" };
716
+ }
717
+ }
718
+
719
+ var uncomparable = null;
720
+ for (var g = 0; g < groups.length; g++) {
721
+ for (var t = 0; t < groups[g].length; t++) {
722
+ var entry = groups[g][t];
723
+ if (entry.kind !== "targetName" && entry.kind !== "targetGroup") continue;
724
+ var m = _generalNameMatches(entry.name, want);
725
+ if (m === true) return { checked: true, ok: true };
726
+ if (m === null) uncomparable = entry.name && entry.name.tagNumber;
727
+ }
728
+ }
729
+ // A target this build cannot compare leaves the check unperformed, which cannot read as a pass.
730
+ if (uncomparable !== null) {
731
+ return { checked: false, ok: false,
732
+ reason: "this attribute certificate names a target in a GeneralName form ([" + uncomparable +
733
+ "]) this verifier does not compare, so the RFC 5755 sec. 4.3.2 check did not run" };
734
+ }
735
+ return { checked: true, ok: false,
736
+ reason: "this attribute certificate does not name this verifier among its targets (RFC 5755 sec. 4.3.2)" };
737
+ }
738
+
739
+ // The critical extensions this verb SUPPORTS, by OID. RFC 5755 sec. 5 defines support in two parts:
740
+ // the verifier must be able to parse the value, AND "where the extension value causes the AC to be
741
+ // rejected, the AC verifier MUST reject the AC". Parsing alone satisfies only the first, so an
742
+ // extension whose constraints this verb never evaluates cannot be called supported however cleanly
743
+ // it decodes.
744
+ //
745
+ // targetInformation -- evaluated, by the sec. 4.3.2 check below.
746
+ // acAuditIdentity -- sec. 4.3.1 is an audit/logging identity and states no rule that rejects an
747
+ // AC, so there is no second part to satisfy. It rides out on the verdict's
748
+ // `extensions` for the logging the section asks for.
749
+ //
750
+ // Everything else critical is refused, including extensions this build parses but does not act on:
751
+ // aaControls (sec. 7.4) constrains what an AA may delegate and acProxying (sec. 7.2) constrains
752
+ // proxy use, and an unevaluated constraint is exactly what item 7 exists to stop. A critical
753
+ // extension defined in future defaults to refused, which is the safe direction.
754
+ var SUPPORTED_CRITICAL = {};
755
+ SUPPORTED_CRITICAL[O("targetInformation")] = 1;
756
+ SUPPORTED_CRITICAL[O("acAuditIdentity")] = 1;
757
+
758
+ function _unsupportedCritical(ac) {
759
+ for (var i = 0; i < ac.extensions.length; i++) {
760
+ var e = ac.extensions[i];
761
+ if (e.critical !== true) continue;
762
+ // Own-property membership, so an extension OID that happens to name something on Object.prototype
763
+ // cannot read as supported.
764
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_CRITICAL, e.oid)) return e.oid;
765
+ // A supported OID whose value this build could not interpret is still unusable.
766
+ if (e.decoded === null || e.decoded === undefined || e.decoded.opaque === true) return e.oid;
767
+ }
768
+ return null;
769
+ }
770
+
771
+ /**
772
+ * @primitive pki.attrcert.verify
773
+ * @signature pki.attrcert.verify(ac, issuer, opts) -> Promise<{ verified, signatureValid, validityChecked, targetingChecked, revocationChecked, noRevAvail, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }>
774
+ * @since 0.5.15
775
+ * @status stable
776
+ * @spec RFC 5755 sec. 5
777
+ * @defends attribute-certificate-forgery (CWE-347)
778
+ * @related pki.attrcert.sign, pki.path.validate, pki.schema.attrcert.parse
779
+ *
780
+ * Verify an RFC 5755 attribute certificate. An AC carries privilege attributes -- a role, a group, a
781
+ * clearance -- so a consumer that reads them without this grants privileges on unauthenticated
782
+ * input. `ac` is a DER `Buffer`, a PEM string, or a parsed attribute certificate; `issuer` is
783
+ * `{ name, publicKey }`, the AC issuer this verifier trusts as an AC issuer, which section 5 item 4
784
+ * makes the verifier's own configuration and this verb therefore refuses to infer from the AC.
785
+ * `issuer.name` takes every form `pki.attrcert.sign` accepts for the issuing AA -- a string, an
786
+ * array of RDNs, or raw `Name` DER -- and is compared as a distinguished name (RFC 5280 section
787
+ * 7.1), so an AC issued under a multi-RDN authority name is verifiable under that same name.
788
+ *
789
+ * Both options are read at the call, before signature verification suspends it, so a caller that
790
+ * reuses or rewrites its options object -- including calling `setTime` on the `Date` it passed in --
791
+ * cannot change the verdict that call returns.
792
+ *
793
+ * The checks it performs are section 5's items 2, 4, 5, 6 and 7: the signature over the exact
794
+ * `AttributeCertificateInfo` bytes through the one path-validation signature engine; the issuer
795
+ * being the one named; the evaluation instant lying within the validity, where equality with either
796
+ * bound succeeds as the section states; the targeting rule of section 4.3.2, where an AC naming
797
+ * targets is refused at a verifier it does not name; and rejection of any critical extension this
798
+ * verb does not process. Section 5 defines support as parsing the value AND rejecting where the
799
+ * value would reject, so an extension parsed but never evaluated is not supported: targeting is
800
+ * processed and an audit identity states no rule that rejects, while `aaControls` and `acProxying`
801
+ * carry constraints this verb does not evaluate, so a critical one is refused. A critical extension
802
+ * defined in future defaults to refused. The verdict carries `extensions` so an audit identity
803
+ * reaches the caller for the logging section 4.3.1 asks for.
804
+ *
805
+ * Targeting compares `opts.target` against the `targetName` and `targetGroup` alternatives only,
806
+ * under each GeneralName form's own matching rule: a dNSName folds case across the whole name
807
+ * (RFC 5280 sec. 7.2), a mailbox matches its local-part exactly and its host-part case-insensitively
808
+ * (sec. 7.5), and a directoryName goes through the same distinguished-name comparison the rest of
809
+ * the toolkit uses (sec. 7.1). A URI compares as encoded, because sec. 7.4 makes URI equality a full
810
+ * RFC 3987 normalization -- percent-encoding, path segments and scheme-based rules -- and a partial
811
+ * one could match a URI it should not. A form with no comparison here (otherName, ediPartyName,
812
+ * x400Address) leaves `targetingChecked: false` and never a pass. Every one of those limits can
813
+ * refuse a target a fuller comparison would accept, and none can accept one it would refuse.
814
+ * Re-open condition: an RFC 3987 normalizer, at which point URIs compare under sec. 7.4.
815
+ *
816
+ * The third `Target` alternative, `targetCert`, is one section 4.3.2 says "MUST NOT be used", so an
817
+ * attribute certificate carrying one is refused before any match is considered, with
818
+ * `targetingChecked: false`. A match on some other entry does not rescue it: the issuer broke a MUST
819
+ * NOT of its own profile, and letting the verdict turn on which entries rode alongside the forbidden
820
+ * one would leave the outcome to whoever assembled the certificate.
821
+ *
822
+ * Revocation is section 6, outside the seven. This verb implements that section's "never revoke"
823
+ * scheme, which AC users MUST support: it holds no revocation evidence and follows no pointer out
824
+ * of the certificate. The section states the consequence -- "If only the 'never revoke' scheme is
825
+ * supported, then all ACs that do not contain a noRevAvail extension, MUST be rejected" -- because
826
+ * an issuer that omits noRevAvail is stating that revocation status checks are supported, and a
827
+ * verdict that skipped one would grant privileges the issuer expected to be able to withdraw. So an
828
+ * AC carrying `noRevAvail` verifies with `revocationChecked: true` and `noRevAvail: true`, and one
829
+ * without it is refused unless the caller supplies `opts.revocationStatus`. A caller running the
830
+ * section's "pointer in AC" scheme reads the certificate's own `crlDistributionPoints` /
831
+ * `authorityInfoAccess` from the verdict's `extensions`, establishes the status, and passes it back.
832
+ *
833
+ * Items 1 and 3, and the issuer path in item 2, need certificates this verb is not given: the
834
+ * holder's own public-key certificate and its chain, the AC issuer's chain, and the section 4.5
835
+ * profile of the issuer's certificate. Run those through `pki.path.validate` with the certificates
836
+ * you hold. The verdict reports them as `holderBindingChecked: false` and `issuerPathChecked:
837
+ * false` rather than leaving their absence to read as a pass, and `verified` never means more than
838
+ * the checks whose slots are true. Re-open condition: when a caller can pass the holder and issuer
839
+ * certificates, both become checks this verb performs and both slots follow the arguments.
840
+ *
841
+ * @opts
842
+ * time the instant to evaluate the AC at. Omitting it leaves section 5 item 5 unasked, which
843
+ * reports `validityChecked: false` and never `verified: true`.
844
+ * target a GeneralName naming this verifier, for the section 4.3.2 targeting check.
845
+ * revocationStatus
846
+ * `"notRevoked"` or `"revoked"`, the status the caller established for this AC through
847
+ * the section 6 "pointer in AC" scheme. Supplying it answers section 6 for an AC that
848
+ * carries no `noRevAvail`; omitting it leaves such an AC refused.
849
+ *
850
+ * @example
851
+ * var pair = await pki.key.generate("Ed25519");
852
+ * var aaSpki = await pki.key.export(pair.publicKey);
853
+ * var aaKey = await pki.key.export(pair.privateKey);
854
+ * var acDer = await pki.attrcert.sign({
855
+ * holder: { entityName: { directoryName: "CN=Alice" } },
856
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"),
857
+ * notAfterTime: new Date("2027-01-01T00:00:00Z"),
858
+ * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } },
859
+ * // RFC 5755 sec. 6: the issuer states no revocation information will exist for this AC.
860
+ * extensions: { noRevAvail: true },
861
+ * }, { name: "CN=Example AA", publicKey: aaSpki, key: aaKey });
862
+ * var r = await pki.attrcert.verify(acDer, { name: "CN=Example AA", publicKey: aaSpki },
863
+ * { time: new Date("2026-06-01T00:00:00Z") });
864
+ * r.verified; // true when every check whose slot is true passed
865
+ * r.attributes; // the privileges, re-derived from the signed bytes
866
+ */
867
+ function verify(ac, issuer, opts) {
868
+ return guard.async.deferred(function () { return _verify(ac, issuer, opts); });
869
+ }
870
+ // Every caller-owned value the verdict depends on, read HERE and reduced to a value the caller no
871
+ // longer reaches: the instant rather than the Date (a Date is mutable through setTime even when the
872
+ // property is never reassigned), and the target already encoded and decoded rather than the object.
873
+ // Signature verification suspends this call for at least a turn, and an option re-read after it
874
+ // resumes answers for whatever the caller last wrote, so a caller could hand in an expired instant,
875
+ // replace it while WebCrypto works, and receive `verified: true`. The reduction is returned rather
876
+ // than performed at each use, so reading an option late is not an available mistake.
877
+ function _fixVerifyOptions(opts) {
878
+ var o = guard.identifier.optionsObject(opts, _err, "attrcert/bad-input", "pki.attrcert.verify options");
879
+ guard.identifier.assertKnownKeys(o, KNOWN_VERIFY_OPTS, _err, "attrcert/bad-input",
880
+ "pki.attrcert.verify has an unknown option ");
881
+ var at = null;
882
+ if (o.time !== undefined) {
883
+ guard.time.assertValid(o.time, _err, "attrcert/bad-input", "pki.attrcert.verify opts.time");
884
+ at = guard.time.instantOf(o.time);
885
+ }
886
+ var revocationStatus = null;
887
+ if (o.revocationStatus !== undefined) {
888
+ if (typeof o.revocationStatus !== "string" ||
889
+ !Object.prototype.hasOwnProperty.call(REVOCATION_STATUS, o.revocationStatus)) {
890
+ throw _err("attrcert/bad-input", 'pki.attrcert.verify opts.revocationStatus must be "notRevoked" or "revoked"');
891
+ }
892
+ revocationStatus = o.revocationStatus;
893
+ }
894
+ // A supplied target is encoded and read back whether or not the AC turns out to be targeted, so a
895
+ // malformed one is refused at the call that supplied it.
896
+ return {
897
+ at: at, revocationStatus: revocationStatus,
898
+ target: o.target === undefined ? undefined : _callerTarget(o.target),
899
+ };
900
+ }
901
+
902
+ // RFC 5755 sec. 6 "pointer in AC": "ACs may 'point' to sources of revocation status information,
903
+ // using either an authorityInfoAccess extension or a crlDistributionPoints extension within the AC."
904
+ // Named by OID, since the OID is what an issuer signs and a display name is not.
905
+ var REVOCATION_POINTERS = Object.create(null);
906
+ REVOCATION_POINTERS[O("cRLDistributionPoints")] = "crlDistributionPoints";
907
+ REVOCATION_POINTERS[O("authorityInfoAccess")] = "authorityInfoAccess";
908
+
909
+ // The two revocation schemes an AC declares, read from its extensions. RFC 5755 sec. 4.3.6 / sec. 6:
910
+ // noRevAvail is the issuer stating that no revocation information will ever be made available for
911
+ // this AC, and it is a bare NULL, so its presence is the whole content.
912
+ function _revocationScheme(ac) {
913
+ var out = { noRevAvail: false, pointer: null };
914
+ for (var i = 0; i < ac.extensions.length; i++) {
915
+ var e = ac.extensions[i];
916
+ if (e.name === "noRevAvail") out.noRevAvail = true;
917
+ var p = REVOCATION_POINTERS[e.oid];
918
+ if (p !== undefined && Object.prototype.hasOwnProperty.call(REVOCATION_POINTERS, e.oid)) out.pointer = p;
919
+ }
920
+ return out;
921
+ }
922
+
923
+ function _verify(ac, issuer, opts) {
924
+ var fixed = _fixVerifyOptions(opts);
925
+ var parsed = _coerceAc(ac);
926
+ var trust = _resolveIssuer(issuer);
927
+
928
+ var acIssuer = _acIssuerName(parsed);
929
+ var result = {
930
+ holder: parsed.holder,
931
+ issuer: acIssuer,
932
+ attributes: parsed.attributes,
933
+ // Carried so a caller can act on what rode along: sec. 4.3.1 asks that an audit identity be
934
+ // used for audit and logging, which it cannot be if the verdict drops it.
935
+ extensions: parsed.extensions,
936
+ notBefore: parsed.validity.notBeforeTime,
937
+ notAfter: parsed.validity.notAfterTime,
938
+ serialNumberHex: parsed.serialNumberHex,
939
+ signatureValid: false,
940
+ validityChecked: false,
941
+ targetingChecked: false,
942
+ revocationChecked: false,
943
+ noRevAvail: false,
944
+ holderBindingChecked: false, // item 1 needs the holder's certificate, which this verb is not given
945
+ issuerPathChecked: false, // items 2 and 3 need the issuer's chain, likewise
946
+ verified: false,
947
+ reason: undefined,
948
+ };
949
+
950
+ // The signature runs first so `signatureValid` is answered even when a later gate refuses: an
951
+ // operator reading an out-of-window verdict still learns whether the bytes were authentic.
952
+ return Promise.resolve(attrcertVerify.verifyAcSignature(parsed, trust.spki)).then(function (sigOk) {
953
+ result.signatureValid = sigOk === true;
954
+ var fail = function (reason) { result.reason = reason; return result; };
955
+ if (!result.signatureValid) return fail("the signature does not verify under the issuer public key");
956
+
957
+ // Item 4: the caller trusts a named AC issuer, and this AC must be the one they named.
958
+ if (!guard.name.dnEqual(acIssuer.rdns, trust.rdns, _err,
959
+ "attrcert/bad-input", "the AC issuer")) {
960
+ return fail("this attribute certificate names issuer " + JSON.stringify(acIssuer.dn) +
961
+ ", which is not the AC issuer this verifier trusts");
962
+ }
963
+
964
+ // Item 7, before the fields are read: an unsupported critical extension makes the whole AC
965
+ // unusable, so a caller must not act on attributes carried beside one.
966
+ var badExt = _unsupportedCritical(parsed);
967
+ if (badExt) return fail("this attribute certificate carries an unsupported critical extension (" + badExt + "), which RFC 5755 sec. 5 item 7 requires be rejected");
968
+
969
+ // Item 6.
970
+ var targeting = _checkTargeting(parsed, fixed.target);
971
+ result.targetingChecked = targeting.checked;
972
+ if (!targeting.ok) return fail(targeting.reason);
973
+
974
+ // Item 5. An absent instant asks nothing, which cannot read as a pass.
975
+ if (fixed.at === null) {
976
+ return fail("no opts.time was supplied, so RFC 5755 sec. 5 item 5 (the evaluation instant lies within the validity) went unasked");
977
+ }
978
+ result.validityChecked = true;
979
+ // "If the evaluation time is equal to either notBeforeTime or notAfterTime, then the AC is
980
+ // timely and this check succeeds" -- both bounds are inclusive.
981
+ if (fixed.at < guard.time.instantOf(result.notBefore) || fixed.at > guard.time.instantOf(result.notAfter)) {
982
+ return fail("the evaluation instant lies outside this attribute certificate's validity");
983
+ }
984
+
985
+ // RFC 5755 sec. 6. This verb implements the "never revoke" scheme: it holds no revocation
986
+ // evidence of its own and follows no pointer out of the certificate. The section says exactly
987
+ // what that obliges -- "If only the 'never revoke' scheme is supported, then all ACs that do not
988
+ // contain a noRevAvail extension, MUST be rejected" -- because where noRevAvail is absent "the
989
+ // AC issuer is implicitly stating that revocation status checks are supported", so a verdict
990
+ // that skipped one would grant privileges the issuer expected to be able to withdraw. A caller
991
+ // who has established the status passes it as opts.revocationStatus; the AC's own
992
+ // crlDistributionPoints / authorityInfoAccess pointers ride out on the verdict's `extensions`
993
+ // for them to follow.
994
+ //
995
+ // That status is accepted whether or not the AC carries a pointer, because the section closes by
996
+ // saying where it may come from: "An AC verifier MAY use any source for AC revocation status
997
+ // information." Requiring a pointer in the certificate would refuse a verifier who obtained the
998
+ // status from a directory, an out-of-band CRL, or a local record, all of which that sentence
999
+ // permits. The verdict keeps the two apart: `noRevAvail` says the issuer declared none would
1000
+ // exist, and its absence beside `revocationChecked: true` says the caller supplied one.
1001
+ var scheme = _revocationScheme(parsed);
1002
+ result.noRevAvail = scheme.noRevAvail;
1003
+ // The two schemes are alternatives, and the section closes by saying so: "An AC MUST NOT contain
1004
+ // both a noRevAvail extension and a 'pointer in AC'." An AC carrying both tells the verifier two
1005
+ // different things about its own revocation, and reading the never-revoke claim as settling the
1006
+ // question would resolve that contradiction in the accepting direction. It is refused whichever
1007
+ // way the caller answers, since the certificate itself is the thing that is wrong.
1008
+ if (scheme.noRevAvail && scheme.pointer) {
1009
+ return fail("this attribute certificate carries both noRevAvail and a " + scheme.pointer +
1010
+ " revocation pointer, which RFC 5755 sec. 6 says an AC MUST NOT do");
1011
+ }
1012
+ if (fixed.revocationStatus !== null) {
1013
+ result.revocationChecked = true;
1014
+ if (fixed.revocationStatus === "revoked") {
1015
+ return fail("this attribute certificate is revoked, as opts.revocationStatus reports");
1016
+ }
1017
+ } else if (result.noRevAvail) {
1018
+ // The issuer has stated no revocation information will exist, so there is no status to obtain.
1019
+ result.revocationChecked = true;
1020
+ } else {
1021
+ return fail("this attribute certificate carries no noRevAvail extension, so its issuer supports revocation status checks and RFC 5755 sec. 6 requires one; establish the status from the certificate's own pointers and pass it as opts.revocationStatus");
1022
+ }
1023
+
1024
+ result.verified = true;
1025
+ return result;
1026
+ });
1027
+ }
1028
+
1029
+ module.exports = { sign: sign, verify: verify };
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the RFC 5755 attribute-certificate signature seam, shared by pki.attrcert.verify and
6
+ // by any path that accepts an inbound AC. The slot, the wiring assertion and the octet-alignment
7
+ // refusal are the shared seam (lib/verify-seam.js); what is domain-specific is the error class and
8
+ // the byte range the signature covers. It is not wired into index.js and is reached only by require.
9
+
10
+ var frameworkError = require("./framework-error");
11
+ var seam = require("./verify-seam");
12
+
13
+ var _seam = seam.makeSeam("attrcert-verify", frameworkError.AttrCertError, "attrcert/bad-input");
14
+
15
+ // Verify a parsed attribute certificate's signature over its exact AttributeCertificateInfo bytes
16
+ // under the AC issuer's SubjectPublicKeyInfo DER (RFC 5755 sec. 5, item 2).
17
+ function verifyAcSignature(ac, spkiBytes) {
18
+ return _seam.verify(ac.signatureAlgorithm, ac.signatureValue, spkiBytes, ac.tbsBytes);
19
+ }
20
+
21
+ module.exports = { setEngine: _seam.setEngine, verifyAcSignature: verifyAcSignature };