@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.
- package/CHANGELOG.md +44 -0
- package/README.md +1 -1
- package/lib/attrcert-sign.js +511 -2
- package/lib/attrcert-verify.js +21 -0
- package/lib/cmp-verify.js +425 -82
- package/lib/guard-all.js +2 -0
- package/lib/guard-async.js +14 -2
- package/lib/guard-bytes.js +183 -93
- package/lib/guard-compress.js +32 -12
- package/lib/guard-der.js +30 -7
- package/lib/guard-encoding.js +28 -12
- package/lib/guard-header.js +18 -7
- package/lib/guard-identifier.js +146 -79
- package/lib/guard-intrinsic.js +318 -0
- package/lib/guard-json.js +39 -12
- package/lib/guard-limits.js +15 -6
- package/lib/guard-name.js +72 -23
- package/lib/guard-parsed.js +83 -35
- package/lib/guard-range.js +17 -4
- package/lib/guard-secret.js +15 -1
- package/lib/guard-text.js +35 -6
- package/lib/guard-time.js +13 -4
- package/lib/path-validate.js +5 -0
- package/lib/schema-attrcert.js +19 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/attrcert-sign.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 };
|