@blamejs/pki 0.8.24 → 0.8.26
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 +46 -0
- package/README.md +2 -2
- package/lib/attrcert-sign.js +26 -5
- package/lib/cmp-verify.js +16 -2
- package/lib/cms-decrypt.js +8 -2
- package/lib/cms-sign.js +41 -4
- package/lib/constants.js +1 -0
- package/lib/guard-name.js +147 -7
- package/lib/inspect.js +8 -7
- package/lib/lint.js +939 -7
- package/lib/oid.js +12 -1
- package/lib/path-validate.js +8 -1
- package/lib/schema-attrcert.js +39 -9
- package/lib/schema-cms.js +23 -0
- package/lib/schema-tsp.js +82 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/lint.js
CHANGED
|
@@ -36,6 +36,9 @@ var x509 = require("./schema-x509");
|
|
|
36
36
|
var crlSchema = require("./schema-crl");
|
|
37
37
|
var csrSchema = require("./schema-csr");
|
|
38
38
|
var ocspSchema = require("./schema-ocsp");
|
|
39
|
+
var cmsSchema = require("./schema-cms");
|
|
40
|
+
var tspSchema = require("./schema-tsp");
|
|
41
|
+
var acSchema = require("./schema-attrcert");
|
|
39
42
|
var pkix = require("./schema-pkix");
|
|
40
43
|
var pkiBuild = require("./pki-build");
|
|
41
44
|
var schema = require("./schema-engine");
|
|
@@ -2527,6 +2530,219 @@ var MICROSOFT_RULES = [
|
|
|
2527
2530
|
},
|
|
2528
2531
|
];
|
|
2529
2532
|
|
|
2533
|
+
/** @internal The seven Trust Purposes appendix A defines, each as the extended key usage set it
|
|
2534
|
+
* requires. Dedication is a SUBSET test rather than equality, which the appendix states twice: a
|
|
2535
|
+
* subordinate CA "MUST contain only the EKU(s) specified for that Trust Purpose", and sec. 2.1.3
|
|
2536
|
+
* repeats it. So {id-kp-serverAuth} is dedicated under Server Authentication and under Legacy TLS
|
|
2537
|
+
* alike, and a set spanning two purposes is under neither. */
|
|
2538
|
+
var APPLE_TRUST_PURPOSES = [
|
|
2539
|
+
["serverAuth"],
|
|
2540
|
+
["serverAuth", "clientAuth"],
|
|
2541
|
+
["emailProtection"],
|
|
2542
|
+
["emailProtection", "clientAuth"],
|
|
2543
|
+
["clientAuth"],
|
|
2544
|
+
["timeStamping"],
|
|
2545
|
+
["brandIndicatorforMessageIdentification"],
|
|
2546
|
+
].map(function (ekus) {
|
|
2547
|
+
return { ekus: ekus.map(function (n) { return oid.byName(n); }) };
|
|
2548
|
+
});
|
|
2549
|
+
/** @internal Every identifier the table names, across all seven purposes. */
|
|
2550
|
+
var APPLE_PURPOSE_EKUS = [];
|
|
2551
|
+
APPLE_TRUST_PURPOSES.forEach(function (p) {
|
|
2552
|
+
p.ekus.forEach(function (d) {
|
|
2553
|
+
if (APPLE_PURPOSE_EKUS.indexOf(d) === -1) APPLE_PURPOSE_EKUS.push(d);
|
|
2554
|
+
});
|
|
2555
|
+
});
|
|
2556
|
+
/** @internal Sec. 1.7 and the change log: an extended key usage extension and the prohibition on
|
|
2557
|
+
* anyExtendedKeyUsage from this date, dedication to a single Trust Purpose from the later one.
|
|
2558
|
+
* Sec. 2's note fixes the comparison at "00:00:00 UTC" and sec. 1.7 defines the signing date as
|
|
2559
|
+
* the date of issuance, including for a renewal, a re-key or a cross-sign. */
|
|
2560
|
+
var APPLE_EKU_FROM = new Date("2026-08-01T00:00:00Z");
|
|
2561
|
+
var APPLE_SINGLE_PURPOSE_FROM = new Date("2027-07-01T00:00:00Z");
|
|
2562
|
+
var APPLE_SMIME_SAN_FROM = new Date("2027-02-01T00:00:00Z");
|
|
2563
|
+
var APPLE_ROOT_RSA_MIN_BITS = 4096;
|
|
2564
|
+
/** @internal "a minimum key size of ... ECDSA 384-bit", so P-384 and above. */
|
|
2565
|
+
var APPLE_ROOT_CURVES = ["secp384r1", "secp521r1"];
|
|
2566
|
+
|
|
2567
|
+
function _isApple(cert, ctx) { return ctx.profile === "apple-root"; }
|
|
2568
|
+
function _isAppleRoot(cert, ctx) {
|
|
2569
|
+
return _isApple(cert, ctx) && ctx.isCaCert && _sameName(cert.issuer, cert.subject) &&
|
|
2570
|
+
_selfSignedIdentifiers(cert, ctx);
|
|
2571
|
+
}
|
|
2572
|
+
function _isAppleSubordinate(cert, ctx) {
|
|
2573
|
+
return _isApple(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
|
|
2574
|
+
}
|
|
2575
|
+
function _isAppleSubscriber(cert, ctx) { return _isApple(cert, ctx) && !ctx.isCaCert; }
|
|
2576
|
+
function _applePurposes(ctx) {
|
|
2577
|
+
var d = ctx.decode("extKeyUsage");
|
|
2578
|
+
return (d && Array.isArray(d.value)) ? d.value : null;
|
|
2579
|
+
}
|
|
2580
|
+
/** @internal Whether an extended key usage set is dedicated to one Trust Purpose: non-empty, and
|
|
2581
|
+
* contained in at least one purpose's required set. WHICH purpose is not returned, and cannot
|
|
2582
|
+
* honestly be: {id-kp-clientAuth} sits inside Client Authentication, Legacy TLS and Legacy S/MIME
|
|
2583
|
+
* alike, which is why the appendix has to say in prose that such a certificate belongs under a
|
|
2584
|
+
* Client Authentication CP/CPS. No finding names a purpose, so the rows ask only the question the
|
|
2585
|
+
* bytes answer. The subset form is the document's, and under
|
|
2586
|
+
* the CURRENT table it decides nothing an equality test would not: every proper non-empty subset
|
|
2587
|
+
* of the two purposes with two identifiers is itself another purpose's whole set. No vector
|
|
2588
|
+
* separates the two readings, and that is a fact about the table rather than about the rule, so a
|
|
2589
|
+
* purpose added later is what the subset form is written for. */
|
|
2590
|
+
function _appleIsDedicated(purposes) {
|
|
2591
|
+
if (!purposes || !purposes.length) return false;
|
|
2592
|
+
for (var i = 0; i < APPLE_TRUST_PURPOSES.length; i++) {
|
|
2593
|
+
var p = APPLE_TRUST_PURPOSES[i];
|
|
2594
|
+
var within = true;
|
|
2595
|
+
for (var j = 0; j < purposes.length; j++) {
|
|
2596
|
+
if (p.ekus.indexOf(purposes[j]) === -1) { within = false; break; }
|
|
2597
|
+
}
|
|
2598
|
+
if (within) return true;
|
|
2599
|
+
}
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
/** @internal Whether the set is under a Trust Purpose at all, which is what separates a certificate
|
|
2603
|
+
* the scoping reaches from one it does not. anyExtendedKeyUsage is not in the appendix's table and
|
|
2604
|
+
* is still in scope: RFC 5280 sec. 4.2.1.12 makes it a certificate "not restricted to any specific
|
|
2605
|
+
* key purpose", so it names EVERY purpose rather than none, and it conforms to no single one. That
|
|
2606
|
+
* is what separates it from id-kp-codeSigning, which names a use the table does not list and
|
|
2607
|
+
* leaves the certificate under no purpose. */
|
|
2608
|
+
function _appleNamesAPurpose(purposes) {
|
|
2609
|
+
if (!purposes) return false;
|
|
2610
|
+
if (purposes.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1) return true;
|
|
2611
|
+
for (var i = 0; i < purposes.length; i++) {
|
|
2612
|
+
if (APPLE_PURPOSE_EKUS.indexOf(purposes[i]) !== -1) return true;
|
|
2613
|
+
}
|
|
2614
|
+
return false;
|
|
2615
|
+
}
|
|
2616
|
+
function _appleSignedBefore(cert, when) {
|
|
2617
|
+
var nb = cert.validity && cert.validity.notBefore;
|
|
2618
|
+
if (!guard.time.isDate(nb)) return false;
|
|
2619
|
+
return guard.time.instantOf(nb) < guard.time.instantOf(when);
|
|
2620
|
+
}
|
|
2621
|
+
function _appleEkuLabels(purposes) {
|
|
2622
|
+
return purposes.map(function (d) { return oid.name(d) || d; });
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/** @internal The Apple Root Program Policy v2.0 profile. */
|
|
2626
|
+
var APPLE_RULES = [
|
|
2627
|
+
{
|
|
2628
|
+
id: "lint/apple-root/root-key-too-small", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.5",
|
|
2629
|
+
/** @internal "Root Inclusion Requests MUST only contain Root CA Certificates that have a
|
|
2630
|
+
* minimum key size of RSA 4096-bit or ECDSA 384-bit." The clause names two families and no
|
|
2631
|
+
* others, so a key outside both is outside it. */
|
|
2632
|
+
message: "a root certificate needs an RSA modulus of at least 4096 bits or a curve of at least NIST P-384",
|
|
2633
|
+
appliesTo: _isAppleRoot,
|
|
2634
|
+
check: function (cert) {
|
|
2635
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
2636
|
+
var name = spki && spki.algorithm && spki.algorithm.name;
|
|
2637
|
+
if (name && RSA_KEY_ALGS[name]) {
|
|
2638
|
+
var bits = _rsaModulusBits(spki);
|
|
2639
|
+
if (bits === null) return { context: { rsaBits: null } };
|
|
2640
|
+
return bits < APPLE_ROOT_RSA_MIN_BITS ? { context: { rsaBits: bits } } : null;
|
|
2641
|
+
}
|
|
2642
|
+
if (name === "ecPublicKey") {
|
|
2643
|
+
var curve = _ecCurveName(spki);
|
|
2644
|
+
return APPLE_ROOT_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
|
|
2645
|
+
}
|
|
2646
|
+
return { context: { algorithm: name || null } };
|
|
2647
|
+
},
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
id: "lint/apple-root/subordinate-ca-eku-missing", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
|
|
2651
|
+
message: "a subordinate CA certificate must carry an extKeyUsage extension",
|
|
2652
|
+
appliesTo: _isAppleSubordinate,
|
|
2653
|
+
effectiveDate: APPLE_EKU_FROM,
|
|
2654
|
+
check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
id: "lint/apple-root/subordinate-ca-eku-any", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
|
|
2658
|
+
message: "a subordinate CA certificate must not assert anyExtendedKeyUsage",
|
|
2659
|
+
appliesTo: _isAppleSubordinate,
|
|
2660
|
+
effectiveDate: APPLE_EKU_FROM,
|
|
2661
|
+
check: function (cert, ctx) {
|
|
2662
|
+
var purposes = _applePurposes(ctx);
|
|
2663
|
+
if (purposes === null) return null;
|
|
2664
|
+
return purposes.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
|
|
2665
|
+
},
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
id: "lint/apple-root/subordinate-ca-mixes-trust-purposes", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
|
|
2669
|
+
/** @internal In this window the clause admits (a) dedication to a single Trust Purpose, or (b)
|
|
2670
|
+
* three conditions together, of which one is in the bytes: "it contains no Extended Key Usage
|
|
2671
|
+
* associated with a Trust Purpose defined in Appendix A". A certificate naming one of those
|
|
2672
|
+
* identifiers has failed (b) already, and if its set is not within a single purpose it has
|
|
2673
|
+
* failed (a) as well, so both arms fail on the bytes alone. A certificate naming none of them
|
|
2674
|
+
* is passed over here, because (b)'s other two conditions are the issuing root's configuration
|
|
2675
|
+
* and a CP/CPS, and the row below picks it up once (b) is gone. */
|
|
2676
|
+
message: "a subordinate CA certificate names extended key usages spanning more than one trust purpose",
|
|
2677
|
+
appliesTo: _isAppleSubordinate,
|
|
2678
|
+
effectiveDate: APPLE_EKU_FROM,
|
|
2679
|
+
check: function (cert, ctx) {
|
|
2680
|
+
if (!_appleSignedBefore(cert, APPLE_SINGLE_PURPOSE_FROM)) return null;
|
|
2681
|
+
var purposes = _applePurposes(ctx);
|
|
2682
|
+
if (!_appleNamesAPurpose(purposes)) return null;
|
|
2683
|
+
return _appleIsDedicated(purposes)
|
|
2684
|
+
? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
id: "lint/apple-root/subordinate-ca-not-single-purpose", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
|
|
2689
|
+
/** @internal "A Subordinate CA Certificate signed on or after 2027-07-01 MUST be dedicated to a
|
|
2690
|
+
* single Trust Purpose." Arm (b) is gone, so the set must be within one purpose whatever it
|
|
2691
|
+
* names, and the certificate the row above passes over is reported here. Two rows rather than
|
|
2692
|
+
* one, because the two windows differ in what fixes them. */
|
|
2693
|
+
message: "a subordinate CA certificate must be dedicated to a single trust purpose",
|
|
2694
|
+
appliesTo: _isAppleSubordinate,
|
|
2695
|
+
effectiveDate: APPLE_SINGLE_PURPOSE_FROM,
|
|
2696
|
+
check: function (cert, ctx) {
|
|
2697
|
+
var purposes = _applePurposes(ctx);
|
|
2698
|
+
if (purposes === null) return null;
|
|
2699
|
+
return _appleIsDedicated(purposes)
|
|
2700
|
+
? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
|
|
2701
|
+
},
|
|
2702
|
+
},
|
|
2703
|
+
{
|
|
2704
|
+
id: "lint/apple-root/smime-subscriber-without-rfc822-name", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 2.3",
|
|
2705
|
+
/** @internal "Effective 2027-02-01, all newly signed Subscriber certificates that contain the
|
|
2706
|
+
* id-kp-emailProtection EKU MUST include at least one rfc822Name in the subjectAltName
|
|
2707
|
+
* extension." */
|
|
2708
|
+
message: "a subscriber certificate carrying id-kp-emailProtection must name at least one rfc822Name in its subjectAltName",
|
|
2709
|
+
appliesTo: function (cert, ctx) {
|
|
2710
|
+
var purposes = _applePurposes(ctx);
|
|
2711
|
+
return _isAppleSubscriber(cert, ctx) && !!purposes &&
|
|
2712
|
+
purposes.indexOf(oid.byName("emailProtection")) !== -1;
|
|
2713
|
+
},
|
|
2714
|
+
effectiveDate: APPLE_SMIME_SAN_FROM,
|
|
2715
|
+
check: function (cert, ctx) {
|
|
2716
|
+
var san = ctx.decode("subjectAltName");
|
|
2717
|
+
var names = san && san.value && san.value.names;
|
|
2718
|
+
if (!Array.isArray(names)) return true;
|
|
2719
|
+
for (var i = 0; i < names.length; i++) {
|
|
2720
|
+
if (names[i] && names[i].tagNumber === 1 && typeof names[i].value === "string" &&
|
|
2721
|
+
names[i].value.length) { return null; }
|
|
2722
|
+
}
|
|
2723
|
+
return true;
|
|
2724
|
+
},
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
id: "lint/apple-root/subscriber-mixes-trust-purposes", severity: "error", source: "apple-root", citation: "Apple Root Program Policy appendix A",
|
|
2728
|
+
/** @internal The appendix's last note is a dated PERMISSION rather than a prohibition:
|
|
2729
|
+
* "Subscriber certificates issued under a Trust Purpose ... and signed before 2027-07-01 MAY
|
|
2730
|
+
* contain EKU combinations that do not conform to the single Trust Purpose scoping defined in
|
|
2731
|
+
* this Appendix". The permission is bounded by its date, so the scoping binds once it passes.
|
|
2732
|
+
* A subscriber naming none of the appendix's identifiers is under no Trust Purpose and is
|
|
2733
|
+
* passed over, which is the reading sec. 1.7 arm (b) takes of the same shape. */
|
|
2734
|
+
message: "a subscriber certificate names extended key usages spanning more than one trust purpose",
|
|
2735
|
+
appliesTo: _isAppleSubscriber,
|
|
2736
|
+
effectiveDate: APPLE_SINGLE_PURPOSE_FROM,
|
|
2737
|
+
check: function (cert, ctx) {
|
|
2738
|
+
var purposes = _applePurposes(ctx);
|
|
2739
|
+
if (!_appleNamesAPurpose(purposes)) return null;
|
|
2740
|
+
return _appleIsDedicated(purposes)
|
|
2741
|
+
? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
|
|
2742
|
+
},
|
|
2743
|
+
},
|
|
2744
|
+
];
|
|
2745
|
+
|
|
2530
2746
|
/** @internal The three purposes ETSI EN 319 412-5 clause 4.2.3 names, of which a QcType declares
|
|
2531
2747
|
* "one and only one". The ASN.1 carries an extension marker, so an identifier outside the three
|
|
2532
2748
|
* is admitted and is not one of the purposes being counted. */
|
|
@@ -4402,6 +4618,421 @@ var RFC5280_CRL_RULES = [
|
|
|
4402
4618
|
var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
|
|
4403
4619
|
var ALL_CRL_RULES = RFC5280_CRL_RULES;
|
|
4404
4620
|
|
|
4621
|
+
/** @internal The RFC 5652 profile is SHORT, and the reason is worth stating where the rows are:
|
|
4622
|
+
* this toolkit's CMS parser already refuses nearly everything sections 5 and 11 require, so those
|
|
4623
|
+
* clauses reach an operator as `lint/unparseable` carrying the parser's own code rather than as a
|
|
4624
|
+
* row here. A row for one of them would be a branch no input can drive. Probed against the
|
|
4625
|
+
* shipped parser, the SignedData and SignerInfo versions, the presence of signedAttrs when the
|
|
4626
|
+
* content type is not id-data, the content-type and message-digest attributes and their binding
|
|
4627
|
+
* to eContentType, every attribute's cardinality and placement, a countersignature's own
|
|
4628
|
+
* restrictions, and a time carrying no seconds, fractional seconds or a local offset are all
|
|
4629
|
+
* refused before a rule sees them. What is left is below.
|
|
4630
|
+
* The UTCTime alternative of sec. 11.3 carries two year digits, which the clause reads as 19YY
|
|
4631
|
+
* for YY >= 50 and 20YY otherwise, so every UTCTime names a year inside 1950..2049 and only the
|
|
4632
|
+
* GeneralizedTime direction of that clause can be broken. */
|
|
4633
|
+
var OID_SIGNING_TIME = oid.byName("signingTime");
|
|
4634
|
+
var OID_SIGNED_DATA = oid.byName("signedData");
|
|
4635
|
+
|
|
4636
|
+
/** @internal The content type decides which of these rows apply, and the parser sets `contentType`
|
|
4637
|
+
* as an own property on every content it builds. A content type that carries no signers has no own
|
|
4638
|
+
* `signerInfos`, so testing for that field answers from whatever the prototype chain holds. */
|
|
4639
|
+
function _isSignedData(cms) { return cms.contentType === OID_SIGNED_DATA; }
|
|
4640
|
+
var OID_COUNTERSIGNATURE = oid.byName("countersignature");
|
|
4641
|
+
/** @internal Every SignerInfo a message carries: its own signers, and the countersignatures their
|
|
4642
|
+
* unsigned attributes hold, which sec. 11.4 defines as SignerInfo values and which carry their own
|
|
4643
|
+
* signedAttrs. A rule about a signed attribute governs one wherever it appears, so a message
|
|
4644
|
+
* cannot put the shape a clause forbids one level down and escape it. The parser validates each
|
|
4645
|
+
* countersignature's shape without keeping the result, so it is decoded here, and a nested one is
|
|
4646
|
+
* reached by the same walk. */
|
|
4647
|
+
function _cmsAllSignerInfos(cms) {
|
|
4648
|
+
var out = [];
|
|
4649
|
+
/** @internal Each entry keeps the index of the message's OWN signer it hangs from, not a
|
|
4650
|
+
* position in this walk: a reader given a flattened index would look up a signer that is not
|
|
4651
|
+
* the one at fault, or one that does not exist. Whether the attribute sat on a countersignature
|
|
4652
|
+
* under that signer is the other half of the answer. */
|
|
4653
|
+
function visit(signerInfos, signerIndex, isCounter, depth) {
|
|
4654
|
+
if (!Array.isArray(signerInfos) || depth > C.LIMITS.CMS_MAX_COUNTERSIGNATURE_DEPTH) return;
|
|
4655
|
+
signerInfos.forEach(function (si, i) {
|
|
4656
|
+
var index = isCounter ? signerIndex : i;
|
|
4657
|
+
out.push({ signerInfo: si, signerIndex: index, countersignature: isCounter });
|
|
4658
|
+
(Array.isArray(si.unsignedAttrs) ? si.unsignedAttrs : []).forEach(function (attr) {
|
|
4659
|
+
if (attr.type !== OID_COUNTERSIGNATURE) return;
|
|
4660
|
+
if (!Array.isArray(attr.values)) return;
|
|
4661
|
+
attr.values.forEach(function (value) {
|
|
4662
|
+
var nested;
|
|
4663
|
+
// allow:swallow-unverified the parser already walked this value, so a fault here leaves the countersignature unread rather than reported
|
|
4664
|
+
try { nested = cmsSchema.walkCountersignature(asn1.decode(value), null); }
|
|
4665
|
+
catch (_e) { return; }
|
|
4666
|
+
if (nested) visit([nested], index, true, depth + 1);
|
|
4667
|
+
});
|
|
4668
|
+
});
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
visit(cms.signerInfos, 0, false, 0);
|
|
4672
|
+
return out;
|
|
4673
|
+
}
|
|
4674
|
+
function _cmsSignedAttr(signerInfo, dotted) {
|
|
4675
|
+
var attrs = signerInfo.signedAttrs;
|
|
4676
|
+
if (!Array.isArray(attrs)) return null;
|
|
4677
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
4678
|
+
if (attrs[i].type === dotted) return attrs[i];
|
|
4679
|
+
}
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
/** @internal Whether two DIGEST AlgorithmIdentifiers name one algorithm, under RFC 5754 sec. 2. The
|
|
4683
|
+
* rule lives in the schema module because the set this row reads is also WRITTEN there, by the verb
|
|
4684
|
+
* that adds a countersigner's digest to it, and a set written under one rule and read under another
|
|
4685
|
+
* would list one algorithm twice. Only digest identifiers pass through here, so no row is loosened
|
|
4686
|
+
* for an algorithm whose parameters carry meaning. */
|
|
4687
|
+
var _cmsSameAlgorithm = cmsSchema.sameDigestAlgorithm;
|
|
4688
|
+
|
|
4689
|
+
var RFC5652_CMS_RULES = [
|
|
4690
|
+
{
|
|
4691
|
+
id: "lint/rfc5652/signing-time-encoding", severity: "error", source: "rfc5652", citation: "RFC 5652 11.3",
|
|
4692
|
+
/** @internal "Dates between 1 January 1950 and 31 December 2049 (inclusive) MUST be encoded as
|
|
4693
|
+
* UTCTime. Any dates with year values before 1950 or after 2049 MUST be encoded as
|
|
4694
|
+
* GeneralizedTime." The value is held as its DER bytes, so the encoding is the first byte and
|
|
4695
|
+
* the year is what the value decodes to. */
|
|
4696
|
+
message: "a signing-time naming a year from 1950 to 2049 must be encoded as UTCTime, not GeneralizedTime",
|
|
4697
|
+
appliesTo: _isSignedData,
|
|
4698
|
+
check: function (cms) {
|
|
4699
|
+
var out = [];
|
|
4700
|
+
_cmsAllSignerInfos(cms).forEach(function (entry) {
|
|
4701
|
+
var attr = _cmsSignedAttr(entry.signerInfo, OID_SIGNING_TIME);
|
|
4702
|
+
if (!attr || !Array.isArray(attr.values) || !attr.values.length) return;
|
|
4703
|
+
var value = attr.values[0];
|
|
4704
|
+
if (!Buffer.isBuffer(value) || value.length < 2) return;
|
|
4705
|
+
if (value[0] !== asn1.TAGS.GENERALIZED_TIME) return;
|
|
4706
|
+
var when;
|
|
4707
|
+
// allow:swallow-unverified the parser already read this value as a Time, so a fault here answers for no signer
|
|
4708
|
+
try { when = asn1.read.time(asn1.decode(value)); } catch (_e) { return; }
|
|
4709
|
+
var year = when.getUTCFullYear();
|
|
4710
|
+
if (year >= 1950 && year <= 2049) {
|
|
4711
|
+
out.push({ context: { signerIndex: entry.signerIndex,
|
|
4712
|
+
countersignature: entry.countersignature, year: year } });
|
|
4713
|
+
}
|
|
4714
|
+
});
|
|
4715
|
+
return out.length ? out : null;
|
|
4716
|
+
},
|
|
4717
|
+
},
|
|
4718
|
+
{
|
|
4719
|
+
id: "lint/rfc5652/signer-digest-not-listed", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.3",
|
|
4720
|
+
/** @internal "The message digest algorithm SHOULD be among those listed in the digestAlgorithms
|
|
4721
|
+
* field of the associated SignerData. Implementations MAY fail to validate signatures that use
|
|
4722
|
+
* a digest algorithm that is not included in the SignedData digestAlgorithms set." A SHOULD
|
|
4723
|
+
* whose cost is a verifier that declines, so a warning.
|
|
4724
|
+
* Sec. 11.4 makes a countersignature a SignerInfo and does not except it from this clause, and
|
|
4725
|
+
* sec. 5.1 has the collection list the digests "employed by all of the signers", so the walk is
|
|
4726
|
+
* the same one the signing-time rule uses and the finding says which signer it hangs from. */
|
|
4727
|
+
message: "a signer's digest algorithm is not among those the SignedData digestAlgorithms set lists",
|
|
4728
|
+
appliesTo: _isSignedData,
|
|
4729
|
+
check: function (cms) {
|
|
4730
|
+
var listed = Array.isArray(cms.digestAlgorithms) ? cms.digestAlgorithms : [];
|
|
4731
|
+
var out = [];
|
|
4732
|
+
_cmsAllSignerInfos(cms).forEach(function (entry) {
|
|
4733
|
+
var mine = entry.signerInfo.digestAlgorithm;
|
|
4734
|
+
if (!mine) return;
|
|
4735
|
+
var found = listed.some(function (alg) { return _cmsSameAlgorithm(alg, mine); });
|
|
4736
|
+
if (!found) {
|
|
4737
|
+
out.push({ context: { signerIndex: entry.signerIndex, countersignature: entry.countersignature,
|
|
4738
|
+
digestAlgorithm: mine.name || mine.oid } });
|
|
4739
|
+
}
|
|
4740
|
+
});
|
|
4741
|
+
return out.length ? out : null;
|
|
4742
|
+
},
|
|
4743
|
+
},
|
|
4744
|
+
{
|
|
4745
|
+
id: "lint/rfc5652/attribute-certificate-v1", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.1",
|
|
4746
|
+
/** @internal "The use of version 1 attribute certificates is strongly discouraged." The
|
|
4747
|
+
* certificates field is a CertificateChoices SET, whose `[1]` alternative is the v1 attribute
|
|
4748
|
+
* certificate; the parser keeps each element's outer tag for exactly this kind of question. */
|
|
4749
|
+
message: "the certificates field carries a version 1 attribute certificate, whose use this section strongly discourages",
|
|
4750
|
+
appliesTo: _isSignedData,
|
|
4751
|
+
check: function (cms) {
|
|
4752
|
+
var certs = Array.isArray(cms.certificates) ? cms.certificates : [];
|
|
4753
|
+
var count = 0;
|
|
4754
|
+
certs.forEach(function (c) {
|
|
4755
|
+
if (c && c.tagClass === "context" && c.tagNumber === 1) count += 1;
|
|
4756
|
+
});
|
|
4757
|
+
return count ? { context: { count: count } } : null;
|
|
4758
|
+
},
|
|
4759
|
+
},
|
|
4760
|
+
{
|
|
4761
|
+
id: "lint/rfc5652/no-signers", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.1",
|
|
4762
|
+
/** @internal "There MAY be any number of elements in the collection, including zero", so a
|
|
4763
|
+
* SignedData with no signers is conformant. It is also a message carrying certificates and
|
|
4764
|
+
* nothing signed, which is worth naming to whoever expected a signature. */
|
|
4765
|
+
message: "the SignedData carries no signers, so it signs nothing and only conveys its certificates and revocation lists",
|
|
4766
|
+
appliesTo: _isSignedData,
|
|
4767
|
+
check: function (cms) { return cms.signerInfos.length === 0 ? true : null; },
|
|
4768
|
+
},
|
|
4769
|
+
{
|
|
4770
|
+
id: "lint/rfc5652/detached-content", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.2",
|
|
4771
|
+
/** @internal eContent is optional, and its absence means a verifier must be handed the content
|
|
4772
|
+
* by another route. The row reads a SignedData that HAS a signer, since the notice exists for
|
|
4773
|
+
* whoever must verify one: a message with no signers has nothing to supply content for. */
|
|
4774
|
+
message: "the SignedData is detached, so verifying it needs the content supplied separately",
|
|
4775
|
+
appliesTo: function (cms) { return _isSignedData(cms) && cms.signerInfos.length > 0; },
|
|
4776
|
+
check: function (cms) {
|
|
4777
|
+
var encap = cms.encapContentInfo;
|
|
4778
|
+
return (encap && encap.eContent) ? null : true;
|
|
4779
|
+
},
|
|
4780
|
+
},
|
|
4781
|
+
];
|
|
4782
|
+
|
|
4783
|
+
var CMS_PROFILES = Object.assign(Object.create(null), { "rfc5652": RFC5652_CMS_RULES });
|
|
4784
|
+
var ALL_CMS_RULES = RFC5652_CMS_RULES;
|
|
4785
|
+
|
|
4786
|
+
/** @internal The RFC 3161 profile is the shortest of the three, for the reason the CMS one is
|
|
4787
|
+
* short: `schema.tsp.parseToken` refuses nearly everything sec. 2.4.2 requires of a token before a
|
|
4788
|
+
* rule sees it, each with its own code. A token that is not a CMS SignedData, encapsulates
|
|
4789
|
+
* something other than id-ct-TSTInfo, carries detached content, carries more than the TSA's own
|
|
4790
|
+
* signerInfo, or omits the SigningCertificate attribute the section requires arrives as
|
|
4791
|
+
* `lint/unparseable`. The TSTInfo version reader admits v1 alone, and the Accuracy field bounds,
|
|
4792
|
+
* the DEFAULT FALSE omissions and the PKIStatus range are all enforced too. */
|
|
4793
|
+
var TSP_SERIAL_MAX_BITS = 160;
|
|
4794
|
+
|
|
4795
|
+
/** @internal The tsa hint as a DECODED GeneralName. The TSTInfo keeps it raw, and the shared
|
|
4796
|
+
* GeneralNames decoder is what the rest of this file reads a name with, so the hint is wrapped in
|
|
4797
|
+
* a one-element SEQUENCE and walked through it rather than compared as bytes: two encodings of one
|
|
4798
|
+
* distinguished name are the same name, and an error-severity row must not say otherwise. */
|
|
4799
|
+
function _tspDecodedHint(hint) {
|
|
4800
|
+
if (!hint || !Buffer.isBuffer(hint.bytes)) return null;
|
|
4801
|
+
var names = _generalNamesOf(asn1.build.sequence([asn1.build.raw(hint.bytes)]));
|
|
4802
|
+
return (names && names.length === 1) ? names[0] : null;
|
|
4803
|
+
}
|
|
4804
|
+
/** @internal Every GeneralName the certificate's subjectAltName carries, decoded, each entry
|
|
4805
|
+
* keeping the raw TLV beside its value so a form with no useful decoded form can still be
|
|
4806
|
+
* compared. */
|
|
4807
|
+
function _tspSanNames(parsedCert) {
|
|
4808
|
+
var raw = null;
|
|
4809
|
+
(parsedCert.extensions || []).forEach(function (e) {
|
|
4810
|
+
if (e.oid === oid.byName("subjectAltName")) raw = e.value;
|
|
4811
|
+
});
|
|
4812
|
+
if (!Buffer.isBuffer(raw)) return [];
|
|
4813
|
+
return _generalNamesOf(raw) || [];
|
|
4814
|
+
}
|
|
4815
|
+
/** @internal Each form of GeneralName carrying text is compared under the RFC 5280 sec. 7 rule for
|
|
4816
|
+
* that form, and each rule lives in `guard.name` so this row and the AC target comparison read one
|
|
4817
|
+
* definition. A dNSName is case-insensitive throughout (sec. 7.2). A mailbox is not: sec. 7.5 has
|
|
4818
|
+
* the local part match exactly and the host part match without regard to ASCII case. A URI is not
|
|
4819
|
+
* either: sec. 7.4 prepares both values, lowercasing only the scheme and host, and then calls for a
|
|
4820
|
+
* "case-sensitive exact match". Folding case over a whole mailbox or URI read two names differing
|
|
4821
|
+
* only in a local part or a path as one, so a hint naming neither went unreported.
|
|
4822
|
+
* A verdict of null says the rule reached none, which the caller reports as nothing rather than as a
|
|
4823
|
+
* mismatch it did not establish. */
|
|
4824
|
+
function _tspSameGeneralName(a, c) {
|
|
4825
|
+
if (!a || !c || a.tagNumber !== c.tagNumber) return false;
|
|
4826
|
+
if (typeof a.value === "string" && typeof c.value === "string") {
|
|
4827
|
+
if (a.tagNumber === 2) return guard.name.lowerAscii(a.value) === guard.name.lowerAscii(c.value);
|
|
4828
|
+
if (a.tagNumber === 1 || a.tagNumber === 6) {
|
|
4829
|
+
var verdict = a.tagNumber === 1 ? guard.name.emailEqual(a.value, c.value)
|
|
4830
|
+
: guard.name.uriEqual(a.value, c.value);
|
|
4831
|
+
if (verdict === "match") return true;
|
|
4832
|
+
if (verdict === "no-match") return false;
|
|
4833
|
+
/** @internal A value neither rule can read is compared as its own encoding, which answers the
|
|
4834
|
+
* one case that is certain: identical bytes are one name. */
|
|
4835
|
+
return a.value === c.value ? true : null;
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
if (Buffer.isBuffer(a.value) && Buffer.isBuffer(c.value)) return a.value.equals(c.value);
|
|
4839
|
+
/** @internal Anything else, including an otherName's structure and a form this profile leaves
|
|
4840
|
+
* undecoded, is compared as its own encoding: two decoded structures would compare by identity
|
|
4841
|
+
* and never match, and two undecoded ones both read as null and would compare equal. */
|
|
4842
|
+
return Buffer.isBuffer(a.bytes) && Buffer.isBuffer(c.bytes) && a.bytes.equals(c.bytes);
|
|
4843
|
+
}
|
|
4844
|
+
/** @internal "one of the subject names included in the certificate", which is the subject AND
|
|
4845
|
+
* every subjectAltName entry: the clause says names, plural, and a hint the subject does not
|
|
4846
|
+
* carry but an alternative name does is one of them. A directoryName is compared under the
|
|
4847
|
+
* RFC 5280 sec. 7.1 rules wherever it appears, since two encodings of one distinguished name are
|
|
4848
|
+
* one name; any other form is compared as its exact encoding. */
|
|
4849
|
+
/** @internal The certificate the token names as the one "to be used to verify the token", or null
|
|
4850
|
+
* when the token does not carry it. Carrying ONE certificate is not that fact: a token may embed an
|
|
4851
|
+
* intermediate and leave the signer's certificate to be supplied out of band, and comparing a hint
|
|
4852
|
+
* against an intermediate reports a conforming token. Which certificate it is comes from the ESS
|
|
4853
|
+
* certHash the schema module resolves, because that value names exactly one certificate while the
|
|
4854
|
+
* SignerInfo sid names none uniquely. */
|
|
4855
|
+
function _tspSignerCertificate(token) {
|
|
4856
|
+
var der = tspSchema.signerCertificateDer(token);
|
|
4857
|
+
if (der === null) return null;
|
|
4858
|
+
// allow:swallow-unverified an element this profile cannot read as a certificate names no subject, so the row passes over the token as it does when the signer is absent
|
|
4859
|
+
try { return x509.parse(der); } catch (_e) { return null; }
|
|
4860
|
+
}
|
|
4861
|
+
function _tspHintMatchesCert(hint, parsedCert) {
|
|
4862
|
+
var decoded = _tspDecodedHint(hint);
|
|
4863
|
+
if (decoded === null) return false;
|
|
4864
|
+
if (decoded.tagNumber === 4 && _sameName(decoded.value, parsedCert.subject)) return true;
|
|
4865
|
+
var san = _tspSanNames(parsedCert), undecided = false;
|
|
4866
|
+
for (var i = 0; i < san.length; i++) {
|
|
4867
|
+
if (decoded.tagNumber === 4) {
|
|
4868
|
+
if (san[i].tagNumber === 4 && _sameName(decoded.value, san[i].value)) return true;
|
|
4869
|
+
continue;
|
|
4870
|
+
}
|
|
4871
|
+
var verdict = _tspSameGeneralName(decoded, san[i]);
|
|
4872
|
+
if (verdict === true) return true;
|
|
4873
|
+
if (verdict === null) undecided = true;
|
|
4874
|
+
}
|
|
4875
|
+
/** @internal A name the comparison rule could not read leaves the row with no verdict on this
|
|
4876
|
+
* certificate, which is reported as nothing rather than as the mismatch it did not establish. */
|
|
4877
|
+
return undecided ? null : false;
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
var RFC3161_TSP_RULES = [
|
|
4881
|
+
{
|
|
4882
|
+
id: "lint/rfc3161/tsa-name-mismatch", severity: "error", source: "rfc3161", citation: "RFC 3161 2.4.2",
|
|
4883
|
+
/** @internal "The purpose of the tsa field is to give a hint in identifying the name of the
|
|
4884
|
+
* TSA. If present, it MUST correspond to one of the subject names included in the certificate
|
|
4885
|
+
* that is to be used to verify the token."
|
|
4886
|
+
* WHICH certificate verifies the token is what the SignerInfo's own identifier names, so the
|
|
4887
|
+
* row resolves it rather than assuming the token carries one certificate and that it is the
|
|
4888
|
+
* signer's. A token that leaves the signer's certificate to be supplied out of band is passed
|
|
4889
|
+
* over, which is the honest edge of reading a hint against bytes that do not hold the answer. */
|
|
4890
|
+
message: "the tsa hint names a subject the signer's own certificate does not carry",
|
|
4891
|
+
appliesTo: function (token) {
|
|
4892
|
+
return !!(token.tstInfo && token.tstInfo.tsa) && _tspSignerCertificate(token) !== null;
|
|
4893
|
+
},
|
|
4894
|
+
check: function (token) {
|
|
4895
|
+
var signerCert = _tspSignerCertificate(token);
|
|
4896
|
+
if (signerCert === null) return null;
|
|
4897
|
+
var hint = token.tstInfo.tsa;
|
|
4898
|
+
if (_tspHintMatchesCert(hint, signerCert) !== false) return null;
|
|
4899
|
+
return { context: { tagNumber: hint.tagNumber } };
|
|
4900
|
+
},
|
|
4901
|
+
},
|
|
4902
|
+
{
|
|
4903
|
+
id: "lint/rfc3161/serial-over-160-bits", severity: "warn", source: "rfc3161", citation: "RFC 3161 2.4.2",
|
|
4904
|
+
/** @internal "Time-Stamping users MUST be ready to accommodate integers up to 160 bits", which
|
|
4905
|
+
* is what a conforming consumer owes and therefore all a TSA may count on. */
|
|
4906
|
+
message: "the serial number is wider than the 160 bits a conforming consumer must accommodate",
|
|
4907
|
+
appliesTo: function (token) { return typeof token.tstInfo.serialNumber === "bigint"; },
|
|
4908
|
+
check: function (token) {
|
|
4909
|
+
var n = token.tstInfo.serialNumber;
|
|
4910
|
+
var magnitude = n < 0n ? -n : n;
|
|
4911
|
+
var bits = magnitude === 0n ? 0 : magnitude.toString(2).length;
|
|
4912
|
+
return bits > TSP_SERIAL_MAX_BITS ? { context: { bits: bits } } : null;
|
|
4913
|
+
},
|
|
4914
|
+
},
|
|
4915
|
+
{
|
|
4916
|
+
id: "lint/rfc3161/accuracy-absent", severity: "notice", source: "rfc3161", citation: "RFC 3161 2.4.2",
|
|
4917
|
+
/** @internal "When the accuracy optional field is not present, then the accuracy may be
|
|
4918
|
+
* available through other means, e.g., the TSAPolicyId." So its absence is conformant and
|
|
4919
|
+
* leaves whoever must order two tokens reading the policy instead. */
|
|
4920
|
+
message: "the token states no accuracy, so bounding its genTime needs the TSA policy instead",
|
|
4921
|
+
check: function (token) {
|
|
4922
|
+
return (token.tstInfo.accuracy === null || token.tstInfo.accuracy === undefined) ? true : null;
|
|
4923
|
+
},
|
|
4924
|
+
},
|
|
4925
|
+
];
|
|
4926
|
+
|
|
4927
|
+
var TSP_PROFILES = Object.assign(Object.create(null), { "rfc3161": RFC3161_TSP_RULES });
|
|
4928
|
+
var ALL_TSP_RULES = RFC3161_TSP_RULES;
|
|
4929
|
+
|
|
4930
|
+
/** @internal Sec. 4.3's six and the ProxyInfo of sec. 7.2, read from the schema module's row set
|
|
4931
|
+
* rather than restated here, so the criticality this profile reports, the one the AC signer writes
|
|
4932
|
+
* and the deviation the extension door reports cannot disagree. The keys are also what this profile
|
|
4933
|
+
* means by membership, which the outside-profile rule below reads. */
|
|
4934
|
+
var AC_FIXED_CRITICALITY = acSchema.extensionCriticality(oid);
|
|
4935
|
+
|
|
4936
|
+
/** @internal The parser has already refused sec. 4.2's structural MUSTs by the time a rule sees an
|
|
4937
|
+
* attribute certificate: the version being v2, the issuer being a v2Form carrying one non-empty
|
|
4938
|
+
* directoryName with no baseCertificateID or objectDigestInfo beside it, a serial that is not a
|
|
4939
|
+
* positive integer or runs past 20 octets, a repeated attribute type, an empty attribute sequence,
|
|
4940
|
+
* and a GeneralizedTime carrying no seconds or a fraction. Those reach an operator as
|
|
4941
|
+
* `lint/unparseable` with the parser's code, or are refused by the builder first. */
|
|
4942
|
+
var RFC5755_AC_RULES = [
|
|
4943
|
+
{
|
|
4944
|
+
id: "lint/rfc5755/extension-criticality", severity: "error", source: "rfc5755", citation: "RFC 5755 4.3",
|
|
4945
|
+
message: "an extension this profile defines does not carry the criticality its clause states",
|
|
4946
|
+
check: function (ac) {
|
|
4947
|
+
var out = [];
|
|
4948
|
+
(ac.extensions || []).forEach(function (x) {
|
|
4949
|
+
var row = AC_FIXED_CRITICALITY[x.oid];
|
|
4950
|
+
if (!row) return;
|
|
4951
|
+
if (x.critical !== row.critical) {
|
|
4952
|
+
out.push({ context: { extension: row.name, clause: row.clause, critical: x.critical === true } });
|
|
4953
|
+
}
|
|
4954
|
+
});
|
|
4955
|
+
return out.length ? out : null;
|
|
4956
|
+
},
|
|
4957
|
+
},
|
|
4958
|
+
{
|
|
4959
|
+
id: "lint/rfc5755/critical-extension-outside-profile", severity: "error", source: "rfc5755", citation: "RFC 5755 4.2.9",
|
|
4960
|
+
/** @internal "If any other critical extension is used, the AC does not conform to this
|
|
4961
|
+
* profile." What counts as "other" is membership in the PROFILE, which is the table above:
|
|
4962
|
+
* sec. 4.3's six and the ProxyInfo of sec. 7.2. Whether this toolkit happens to decode a value
|
|
4963
|
+
* is a different question and answers it wrongly in both directions, since the AC decoder also
|
|
4964
|
+
* reads AAControls, which sec. 7.4 places in "CA and AC issuer PKCs" rather than in an
|
|
4965
|
+
* attribute certificate. A non-critical extension outside the profile is conformant and draws
|
|
4966
|
+
* nothing, which the clause says outright. */
|
|
4967
|
+
message: "a critical extension this profile does not define makes the attribute certificate non-conforming",
|
|
4968
|
+
check: function (ac) {
|
|
4969
|
+
var out = [];
|
|
4970
|
+
(ac.extensions || []).forEach(function (x) {
|
|
4971
|
+
if (x.critical !== true) return;
|
|
4972
|
+
if (AC_FIXED_CRITICALITY[x.oid]) return;
|
|
4973
|
+
out.push({ context: { oid: x.oid, name: x.name || null } });
|
|
4974
|
+
});
|
|
4975
|
+
return out.length ? out : null;
|
|
4976
|
+
},
|
|
4977
|
+
},
|
|
4978
|
+
{
|
|
4979
|
+
id: "lint/rfc5755/holder-multiple-options", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
|
|
4980
|
+
/** @internal "where more than one option is used, there is a potential for confusion as to
|
|
4981
|
+
* which option is 'normative', which is a 'hint' ... this specification RECOMMENDS that only
|
|
4982
|
+
* one of the options be used in any given AC." A RECOMMENDS, so a warning. */
|
|
4983
|
+
message: "the Holder names more than one option, which leaves which of them is normative unstated",
|
|
4984
|
+
check: function (ac) {
|
|
4985
|
+
var holder = ac.holder;
|
|
4986
|
+
if (!holder) return null;
|
|
4987
|
+
var named = [];
|
|
4988
|
+
["baseCertificateID", "entityName", "objectDigestInfo"].forEach(function (k) {
|
|
4989
|
+
if (holder[k] !== null && holder[k] !== undefined) named.push(k);
|
|
4990
|
+
});
|
|
4991
|
+
return named.length > 1 ? { context: { options: named } } : null;
|
|
4992
|
+
},
|
|
4993
|
+
},
|
|
4994
|
+
{
|
|
4995
|
+
id: "lint/rfc5755/holder-entity-name-multiple", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
|
|
4996
|
+
/** @internal "In any other case where the Holder field uses the entityName option, only one name
|
|
4997
|
+
* SHOULD be present." A SHOULD, so a warning. This counts the NAMES inside the one entityName
|
|
4998
|
+
* option, which the sibling row above does not: that one counts how many of the three options
|
|
4999
|
+
* the Holder names, and an entityName carrying several GeneralNames is still one option. */
|
|
5000
|
+
message: "the Holder entityName carries more than one name, where this clause asks for one",
|
|
5001
|
+
check: function (ac) {
|
|
5002
|
+
var entity = ac.holder && ac.holder.entityName;
|
|
5003
|
+
if (!entity || !Array.isArray(entity.names) || entity.names.length <= 1) return null;
|
|
5004
|
+
return { context: { names: entity.names.length } };
|
|
5005
|
+
},
|
|
5006
|
+
},
|
|
5007
|
+
{
|
|
5008
|
+
id: "lint/rfc5755/holder-issuer-uid-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
|
|
5009
|
+
/** @internal "The AC holder.baseCertificateID.issuerUID field MUST only be used if the holder's
|
|
5010
|
+
* PKC contains an issuerUniqueID field." Whether the certificate the baseCertificateID names
|
|
5011
|
+
* carries one is a fact about another certificate, so this reports below error and names the
|
|
5012
|
+
* condition, the way the sec. 4.2.8 row does for the AC's own issuerUniqueID. The three sec. 4.2.2
|
|
5013
|
+
* requirements that ARE answerable from these bytes are refused by the parser instead. */
|
|
5014
|
+
message: "the Holder baseCertificateID carries an issuerUID, which this clause permits only where the certificate it names carries an issuerUniqueID",
|
|
5015
|
+
check: function (ac) {
|
|
5016
|
+
var base = ac.holder && ac.holder.baseCertificateID;
|
|
5017
|
+
if (!base) return null;
|
|
5018
|
+
return (base.issuerUID === null || base.issuerUID === undefined) ? null : true;
|
|
5019
|
+
},
|
|
5020
|
+
},
|
|
5021
|
+
{
|
|
5022
|
+
id: "lint/rfc5755/issuer-unique-id-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.8",
|
|
5023
|
+
/** @internal "This field MUST NOT be used unless it is also used in the AC issuer's PKC, in
|
|
5024
|
+
* which case it MUST be used." Whether the issuer's own certificate carries one is a fact about
|
|
5025
|
+
* another certificate, so the row reports below error and its message names the condition. */
|
|
5026
|
+
message: "an issuerUniqueID is present, which this clause permits only where the AC issuer's own certificate carries one",
|
|
5027
|
+
check: function (ac) {
|
|
5028
|
+
return (ac.issuerUniqueID === null || ac.issuerUniqueID === undefined) ? null : true;
|
|
5029
|
+
},
|
|
5030
|
+
},
|
|
5031
|
+
];
|
|
5032
|
+
|
|
5033
|
+
var AC_PROFILES = Object.assign(Object.create(null), { "rfc5755": RFC5755_AC_RULES });
|
|
5034
|
+
var ALL_AC_RULES = RFC5755_AC_RULES;
|
|
5035
|
+
|
|
4405
5036
|
|
|
4406
5037
|
/** @internal The extensions RFC 6960 and RFC 9654 place on a response, by the list each belongs
|
|
4407
5038
|
* in. The nonce and the extended revoked definition are response extensions; the archive cutoff and
|
|
@@ -4970,12 +5601,13 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
4970
5601
|
"mozilla-root": MOZILLA_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4971
5602
|
"chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4972
5603
|
"microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
5604
|
+
"apple-root": APPLE_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4973
5605
|
});
|
|
4974
5606
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
4975
5607
|
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
|
|
4976
5608
|
/** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
|
|
4977
5609
|
* inert under the default set because each one asks whether the caller named the profile. */
|
|
4978
|
-
.concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES);
|
|
5610
|
+
.concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES);
|
|
4979
5611
|
|
|
4980
5612
|
var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
|
|
4981
5613
|
return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
|
|
@@ -5479,6 +6111,286 @@ function ocsp(input, opts) {
|
|
|
5479
6111
|
return report;
|
|
5480
6112
|
}
|
|
5481
6113
|
|
|
6114
|
+
var _CMS_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
|
|
6115
|
+
var _CMS_CLAIM = ["contentType", "signerInfos", "encapContentInfo"];
|
|
6116
|
+
|
|
6117
|
+
function _ingestCms(input) {
|
|
6118
|
+
var der;
|
|
6119
|
+
var bytes = _derBytesOf(input);
|
|
6120
|
+
if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
|
|
6121
|
+
if (bytes !== null) der = bytes.der;
|
|
6122
|
+
else if (typeof input === "string") {
|
|
6123
|
+
try { der = cmsSchema.pemDecode(input); }
|
|
6124
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER CMS ContentInfo", context: { code: e.code } } }; }
|
|
6125
|
+
} else if (input !== null && typeof input === "object" && !guard.bytes.isByteSource(input)) {
|
|
6126
|
+
return { cms: guard.parsed.fromTrustedSource(input, "cms", _CMS_CLAIM, function (src) {
|
|
6127
|
+
if (!Buffer.isBuffer(src) && typeof src !== "string") throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
|
|
6128
|
+
return cmsSchema.parse(src);
|
|
6129
|
+
}, _cfg, "lint/bad-input",
|
|
6130
|
+
"pki.lint.cms input must be its DER bytes, a PEM string, or an unmodified pki.schema.cms.parse result: the signed attribute bytes, the signature and the fields those bytes encode are separate properties of a parsed object, so a rebuilt structure could have them describe different messages") };
|
|
6131
|
+
} else {
|
|
6132
|
+
throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
|
|
6133
|
+
}
|
|
6134
|
+
try { return { cms: cmsSchema.parse(der) }; }
|
|
6135
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5652", message: "input is not a well-formed CMS ContentInfo", context: { code: e.code } } }; }
|
|
6136
|
+
}
|
|
6137
|
+
|
|
6138
|
+
/**
|
|
6139
|
+
* @primitive pki.lint.cms
|
|
6140
|
+
* @signature pki.lint.cms(pem | der | parsed, opts?) -> LintReport
|
|
6141
|
+
* @since 0.8.26
|
|
6142
|
+
* @status stable
|
|
6143
|
+
* @spec RFC 5652
|
|
6144
|
+
* @related pki.lint.certificate, pki.lint.tsp, pki.schema.cms.parse, pki.cms.sign, pki.cms.verify
|
|
6145
|
+
*
|
|
6146
|
+
* Lint a CMS ContentInfo against the RFC 5652 profile. `input` is a PEM string (`CMS`), a DER
|
|
6147
|
+
* `Buffer`, or an already-parsed `pki.schema.cms.parse` object. It returns the same `LintReport`
|
|
6148
|
+
* shape `pki.lint.certificate` returns, and it never throws on the data path: bytes that are not a
|
|
6149
|
+
* well-formed ContentInfo produce a single `fatal` finding `lint/unparseable` carrying the strict
|
|
6150
|
+
* parser's own code. A content type other than signed-data carries no signerInfos, so every row is
|
|
6151
|
+
* not applicable to it.
|
|
6152
|
+
*
|
|
6153
|
+
* The profile is short, and what makes it short is that the strict parser has already refused most
|
|
6154
|
+
* of what sections 5 and 11 require. The SignedData and SignerInfo versions against their contents,
|
|
6155
|
+
* signedAttrs being present when the content type is not id-data, the content-type and
|
|
6156
|
+
* message-digest attributes and the binding of the first to eContentType, every attribute's
|
|
6157
|
+
* cardinality and placement, a countersignature's own restrictions, and a time carrying no seconds,
|
|
6158
|
+
* fractional seconds or a local offset all arrive as `lint/unparseable` with the parser's code.
|
|
6159
|
+
*
|
|
6160
|
+
* What the rows cover is what survives that: a signing-time naming a year from 1950 to 2049 encoded
|
|
6161
|
+
* as GeneralizedTime where section 11.3 requires UTCTime, a signer whose digest algorithm the
|
|
6162
|
+
* digestAlgorithms set does not list, a version 1 attribute certificate in the certificates field,
|
|
6163
|
+
* and two structural notices, a SignedData carrying no signers and a detached signature whose
|
|
6164
|
+
* content a verifier must be handed separately.
|
|
6165
|
+
*
|
|
6166
|
+
* Verifying a signature, resolving a signer to a certificate and checking a message digest are
|
|
6167
|
+
* `pki.cms.verify`'s, and the certificates a message carries are linted with
|
|
6168
|
+
* `pki.lint.certificate`.
|
|
6169
|
+
*
|
|
6170
|
+
* @opts profile `"rfc5652"`, which is also what the default runs. A certificate, CRL or OCSP
|
|
6171
|
+
* profile name is refused rather than run against a message.
|
|
6172
|
+
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
|
|
6173
|
+
* always reflect the complete, unfiltered result.
|
|
6174
|
+
* @example
|
|
6175
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
6176
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
6177
|
+
* var cert = await pki.x509.sign({ subject: "Example Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
6178
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
6179
|
+
* extensions: { subjectKeyIdentifier: true } }, { key: key });
|
|
6180
|
+
* var der = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
|
|
6181
|
+
* pki.lint.cms(der).findings.length; // -> 0
|
|
6182
|
+
* pki.lint.cms(await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { detached: true })).worst;
|
|
6183
|
+
* // -> "notice"
|
|
6184
|
+
*/
|
|
6185
|
+
function cms(input, opts) {
|
|
6186
|
+
opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
|
|
6187
|
+
guard.identifier.assertKnownKeys(opts, _CMS_OPTS, _cfg, "lint/bad-input",
|
|
6188
|
+
"pki.lint.cms has an unknown option: ");
|
|
6189
|
+
if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
|
|
6190
|
+
throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
|
|
6191
|
+
}
|
|
6192
|
+
var profile = opts.profile;
|
|
6193
|
+
var rules_;
|
|
6194
|
+
if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CMS_RULES;
|
|
6195
|
+
else if (CMS_PROFILES[profile]) rules_ = CMS_PROFILES[profile];
|
|
6196
|
+
else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
|
|
6197
|
+
else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
|
|
6198
|
+
else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
|
|
6199
|
+
else throw _cfg("lint/unknown-profile", "unknown CMS lint profile \"" + profile + "\" (known: " + Object.keys(CMS_PROFILES).join(", ") + ")");
|
|
6200
|
+
|
|
6201
|
+
var ingested = _ingestCms(input);
|
|
6202
|
+
var report;
|
|
6203
|
+
if (ingested.fatal) {
|
|
6204
|
+
report = { findings: [ingested.fatal], ran: [],
|
|
6205
|
+
counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
|
|
6206
|
+
} else {
|
|
6207
|
+
report = _runLints(rules_, ingested.cms, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
|
|
6208
|
+
}
|
|
6209
|
+
report.worst = _worst(report.findings);
|
|
6210
|
+
report.findings = _applyThreshold(report, opts.severity);
|
|
6211
|
+
return report;
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6214
|
+
var _TSP_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
|
|
6215
|
+
|
|
6216
|
+
function _ingestTsp(input) {
|
|
6217
|
+
var der;
|
|
6218
|
+
var bytes = _derBytesOf(input);
|
|
6219
|
+
if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
|
|
6220
|
+
if (bytes !== null) der = bytes.der;
|
|
6221
|
+
else if (typeof input === "string") {
|
|
6222
|
+
try { der = tspSchema.pemDecode(input); }
|
|
6223
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER timestamp token", context: { code: e.code } } }; }
|
|
6224
|
+
} else {
|
|
6225
|
+
throw _cfg("lint/bad-input", "pki.lint.tsp input must be a DER Buffer or a PEM string");
|
|
6226
|
+
}
|
|
6227
|
+
try { return { token: tspSchema.parseToken(der) }; }
|
|
6228
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 3161", message: "input is not a well-formed RFC 3161 timestamp token", context: { code: e.code } } }; }
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
/**
|
|
6232
|
+
* @primitive pki.lint.tsp
|
|
6233
|
+
* @signature pki.lint.tsp(pem | der, opts?) -> LintReport
|
|
6234
|
+
* @since 0.8.26
|
|
6235
|
+
* @status stable
|
|
6236
|
+
* @spec RFC 3161
|
|
6237
|
+
* @related pki.lint.cms, pki.schema.tsp.parseToken, pki.tsp.sign, pki.tsp.verify
|
|
6238
|
+
*
|
|
6239
|
+
* Lint an RFC 3161 timestamp token against the section 2.4.2 profile. `input` is a PEM string or a
|
|
6240
|
+
* DER `Buffer` holding a TimeStampToken, which is a CMS SignedData encapsulating a TSTInfo. It
|
|
6241
|
+
* returns the same `LintReport` shape `pki.lint.certificate` returns and never throws on the data
|
|
6242
|
+
* path: bytes that are not a well-formed token produce a single `fatal` finding `lint/unparseable`
|
|
6243
|
+
* carrying the strict parser's own code.
|
|
6244
|
+
*
|
|
6245
|
+
* This profile is the shortest the toolkit ships, and the reason is that `pki.schema.tsp.parseToken`
|
|
6246
|
+
* already refuses what the section requires. A token that is not a CMS SignedData, encapsulates
|
|
6247
|
+
* something other than id-ct-TSTInfo, carries detached content, carries a signature other than the
|
|
6248
|
+
* TSA's own, or omits the SigningCertificate attribute the section names arrives as
|
|
6249
|
+
* `lint/unparseable` with the parser's code rather than as a row, and so do a TSTInfo version other
|
|
6250
|
+
* than v1, an Accuracy field outside its stated bounds and a DEFAULT FALSE written out.
|
|
6251
|
+
*
|
|
6252
|
+
* What the rows cover is what survives that: a tsa hint naming a subject the signer's own
|
|
6253
|
+
* certificate does not carry, a serial number wider than the 160 bits a conforming consumer must
|
|
6254
|
+
* accommodate, and a token stating no accuracy. The tsa row resolves which certificate verifies the
|
|
6255
|
+
* token from the SignerInfo's own identifier, and passes over a token that leaves that certificate
|
|
6256
|
+
* to be supplied out of band.
|
|
6257
|
+
*
|
|
6258
|
+
* Verifying the signature, resolving the TSA certificate and comparing the imprint against content
|
|
6259
|
+
* are `pki.tsp.verify`'s.
|
|
6260
|
+
*
|
|
6261
|
+
* @opts profile `"rfc3161"`, which is also what the default runs.
|
|
6262
|
+
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
|
|
6263
|
+
* always reflect the complete, unfiltered result.
|
|
6264
|
+
* @example
|
|
6265
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
6266
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
6267
|
+
* var cert = await pki.x509.sign({ subject: "Example TSA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
6268
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
6269
|
+
* extensions: { subjectKeyIdentifier: true, keyUsage: ["digitalSignature"],
|
|
6270
|
+
* extendedKeyUsage: ["timeStamping"], extendedKeyUsageCritical: true } }, { key: key });
|
|
6271
|
+
* var imprint = { hashAlgorithm: "sha256", hashedMessage: Buffer.alloc(32, 7) };
|
|
6272
|
+
* var token = await pki.tsp.sign(imprint, { cert: cert, key: key }, { policy: "1.2.3", serialNumber: 1 });
|
|
6273
|
+
* pki.lint.tsp(token).worst; // -> "notice", since the token states no accuracy
|
|
6274
|
+
*/
|
|
6275
|
+
function tsp(input, opts) {
|
|
6276
|
+
opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
|
|
6277
|
+
guard.identifier.assertKnownKeys(opts, _TSP_OPTS, _cfg, "lint/bad-input",
|
|
6278
|
+
"pki.lint.tsp has an unknown option: ");
|
|
6279
|
+
if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
|
|
6280
|
+
throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
|
|
6281
|
+
}
|
|
6282
|
+
var profile = opts.profile;
|
|
6283
|
+
var rules_;
|
|
6284
|
+
if (profile == null || profile === "all" || profile === "default") rules_ = ALL_TSP_RULES;
|
|
6285
|
+
else if (TSP_PROFILES[profile]) rules_ = TSP_PROFILES[profile];
|
|
6286
|
+
else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
|
|
6287
|
+
else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
|
|
6288
|
+
else throw _cfg("lint/unknown-profile", "unknown timestamp lint profile \"" + profile + "\" (known: " + Object.keys(TSP_PROFILES).join(", ") + ")");
|
|
6289
|
+
|
|
6290
|
+
var ingested = _ingestTsp(input);
|
|
6291
|
+
var report;
|
|
6292
|
+
if (ingested.fatal) {
|
|
6293
|
+
report = { findings: [ingested.fatal], ran: [],
|
|
6294
|
+
counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
|
|
6295
|
+
} else {
|
|
6296
|
+
report = _runLints(rules_, ingested.token, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
|
|
6297
|
+
}
|
|
6298
|
+
report.worst = _worst(report.findings);
|
|
6299
|
+
report.findings = _applyThreshold(report, opts.severity);
|
|
6300
|
+
return report;
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
var _AC_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
|
|
6304
|
+
|
|
6305
|
+
function _ingestAttrCert(input) {
|
|
6306
|
+
var der;
|
|
6307
|
+
var bytes = _derBytesOf(input);
|
|
6308
|
+
if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
|
|
6309
|
+
if (bytes !== null) der = bytes.der;
|
|
6310
|
+
else if (typeof input === "string") {
|
|
6311
|
+
try { der = acSchema.pemDecode(input); }
|
|
6312
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER attribute certificate", context: { code: e.code } } }; }
|
|
6313
|
+
} else {
|
|
6314
|
+
throw _cfg("lint/bad-input", "pki.lint.attrcert input must be a DER Buffer or a PEM string");
|
|
6315
|
+
}
|
|
6316
|
+
try { return { ac: acSchema.parse(der) }; }
|
|
6317
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5755", message: "input is not a well-formed X.509 attribute certificate", context: { code: e.code } } }; }
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6320
|
+
/**
|
|
6321
|
+
* @primitive pki.lint.attrcert
|
|
6322
|
+
* @signature pki.lint.attrcert(pem | der, opts?) -> LintReport
|
|
6323
|
+
* @since 0.8.26
|
|
6324
|
+
* @status stable
|
|
6325
|
+
* @spec RFC 5755
|
|
6326
|
+
* @related pki.lint.certificate, pki.schema.attrcert.parse, pki.attrcert.sign, pki.attrcert.verify
|
|
6327
|
+
*
|
|
6328
|
+
* Lint an X.509 attribute certificate against the RFC 5755 profile. `input` is a PEM string
|
|
6329
|
+
* (`ATTRIBUTE CERTIFICATE`) or a DER `Buffer`. It returns the same `LintReport` shape
|
|
6330
|
+
* `pki.lint.certificate` returns and never throws on the data path: bytes that are not a
|
|
6331
|
+
* well-formed attribute certificate produce a single `fatal` finding `lint/unparseable` carrying
|
|
6332
|
+
* the strict parser's own code.
|
|
6333
|
+
*
|
|
6334
|
+
* Section 4.2's structural requirements are already refused at parse and so carry no rows: the
|
|
6335
|
+
* version being v2, the issuer being a v2Form carrying one non-empty directoryName with no
|
|
6336
|
+
* baseCertificateID or objectDigestInfo beside it, a serial that is not a positive integer or runs
|
|
6337
|
+
* past 20 octets, a repeated attribute type, an empty attribute sequence, a GeneralizedTime carrying
|
|
6338
|
+
* no seconds or a fractional part, and a Holder baseCertificateID whose issuer is anything other than
|
|
6339
|
+
* one non-empty directoryName, which section 4.2.2 requires in the same words section 4.2.3 uses for
|
|
6340
|
+
* the issuer.
|
|
6341
|
+
*
|
|
6342
|
+
* The rows are the clauses left, which are the ones these bytes cannot settle alone or that state a
|
|
6343
|
+
* recommendation rather than a requirement: the criticality section 4.3 fixes for each extension it
|
|
6344
|
+
* defines and the ProxyInfo of section 7.2, a critical extension the profile does not define, a
|
|
6345
|
+
* Holder naming more than one of its three options, a Holder entityName carrying more than one name,
|
|
6346
|
+
* a Holder baseCertificateID issuerUID, and an issuerUniqueID. The last two are permitted only where
|
|
6347
|
+
* another certificate carries the matching field, so each is a warning naming that condition.
|
|
6348
|
+
* Section 4.2.2's remaining requirements are about the public-key certificate the Holder names rather
|
|
6349
|
+
* than about this one, so no row can answer them.
|
|
6350
|
+
*
|
|
6351
|
+
* Verifying the signature and resolving the holder against a public-key certificate are
|
|
6352
|
+
* `pki.attrcert.verify`'s.
|
|
6353
|
+
*
|
|
6354
|
+
* @opts profile `"rfc5755"`, which is also what the default runs.
|
|
6355
|
+
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
|
|
6356
|
+
* always reflect the complete, unfiltered result.
|
|
6357
|
+
* @example
|
|
6358
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
6359
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
6360
|
+
* var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
|
|
6361
|
+
* notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
|
|
6362
|
+
* attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
|
|
6363
|
+
* { name: "CN=Example AA", publicKey: await pki.key.export(pair.publicKey), key: key });
|
|
6364
|
+
* pki.lint.attrcert(der).findings.length; // -> 0 for a conforming attribute certificate
|
|
6365
|
+
*/
|
|
6366
|
+
function attrcert(input, opts) {
|
|
6367
|
+
opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
|
|
6368
|
+
guard.identifier.assertKnownKeys(opts, _AC_OPTS, _cfg, "lint/bad-input",
|
|
6369
|
+
"pki.lint.attrcert has an unknown option: ");
|
|
6370
|
+
if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
|
|
6371
|
+
throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
|
|
6372
|
+
}
|
|
6373
|
+
var profile = opts.profile;
|
|
6374
|
+
var rules_;
|
|
6375
|
+
if (profile == null || profile === "all" || profile === "default") rules_ = ALL_AC_RULES;
|
|
6376
|
+
else if (AC_PROFILES[profile]) rules_ = AC_PROFILES[profile];
|
|
6377
|
+
else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
|
|
6378
|
+
else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
|
|
6379
|
+
else throw _cfg("lint/unknown-profile", "unknown attribute certificate lint profile \"" + profile + "\" (known: " + Object.keys(AC_PROFILES).join(", ") + ")");
|
|
6380
|
+
|
|
6381
|
+
var ingested = _ingestAttrCert(input);
|
|
6382
|
+
var report;
|
|
6383
|
+
if (ingested.fatal) {
|
|
6384
|
+
report = { findings: [ingested.fatal], ran: [],
|
|
6385
|
+
counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
|
|
6386
|
+
} else {
|
|
6387
|
+
report = _runLints(rules_, ingested.ac, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
|
|
6388
|
+
}
|
|
6389
|
+
report.worst = _worst(report.findings);
|
|
6390
|
+
report.findings = _applyThreshold(report, opts.severity);
|
|
6391
|
+
return report;
|
|
6392
|
+
}
|
|
6393
|
+
|
|
5482
6394
|
/**
|
|
5483
6395
|
* @primitive pki.lint.rules
|
|
5484
6396
|
* @signature pki.lint.rules(profile?, artifact?) -> [{id, severity, source, citation}]
|
|
@@ -5504,6 +6416,7 @@ function ocsp(input, opts) {
|
|
|
5504
6416
|
* literal would answer for "constructor" and the rest of Object.prototype. */
|
|
5505
6417
|
var BY_ARTIFACT = Object.assign(Object.create(null), {
|
|
5506
6418
|
certificate: PROFILES, csr: CSR_PROFILES, crl: CRL_PROFILES, ocsp: OCSP_PROFILES,
|
|
6419
|
+
cms: CMS_PROFILES, tsp: TSP_PROFILES, attrcert: AC_PROFILES,
|
|
5507
6420
|
});
|
|
5508
6421
|
|
|
5509
6422
|
function rules(profile, artifact) {
|
|
@@ -5525,7 +6438,8 @@ function rules(profile, artifact) {
|
|
|
5525
6438
|
BY_ARTIFACT[artifact][name].forEach(function (r) { set.push(r); });
|
|
5526
6439
|
});
|
|
5527
6440
|
} else if (profile == null) {
|
|
5528
|
-
set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES)
|
|
6441
|
+
set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES)
|
|
6442
|
+
.concat(ALL_CMS_RULES).concat(ALL_TSP_RULES).concat(ALL_AC_RULES);
|
|
5529
6443
|
} else if (artifact != null) {
|
|
5530
6444
|
set = BY_ARTIFACT[artifact][profile];
|
|
5531
6445
|
if (!set) {
|
|
@@ -5533,7 +6447,9 @@ function rules(profile, artifact) {
|
|
|
5533
6447
|
" lint profile (known: " + Object.keys(BY_ARTIFACT[artifact]).join(", ") + ")");
|
|
5534
6448
|
}
|
|
5535
6449
|
} else {
|
|
5536
|
-
set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
|
|
6450
|
+
set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
|
|
6451
|
+
CSR_PROFILES[profile] || CMS_PROFILES[profile] || TSP_PROFILES[profile] ||
|
|
6452
|
+
AC_PROFILES[profile];
|
|
5537
6453
|
if (!set) throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\"");
|
|
5538
6454
|
}
|
|
5539
6455
|
/** @internal One row per id: a rule reused across artifacts, like the RFC 5280 sec. 4.2
|
|
@@ -5555,16 +6471,29 @@ function rules(profile, artifact) {
|
|
|
5555
6471
|
* @status stable
|
|
5556
6472
|
* @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS BR
|
|
5557
6473
|
*
|
|
5558
|
-
* List the known lint-profile names.
|
|
6474
|
+
* List the known lint-profile names. The two names every verb reads as naming no profile, `"all"`
|
|
6475
|
+
* and `"default"`, are not among them.
|
|
5559
6476
|
* @example
|
|
5560
|
-
* pki.lint.profiles();
|
|
6477
|
+
* pki.lint.profiles();
|
|
6478
|
+
* // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
|
|
6479
|
+
* // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root", "rfc2986",
|
|
6480
|
+
* // "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc3161", "rfc5755"]
|
|
5561
6481
|
*/
|
|
5562
6482
|
/** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
|
|
5563
6483
|
* request form: `pki.lint.csr` takes the same name and runs those rows against what a request
|
|
5564
6484
|
* asks for, so one name means one set of requirements whichever artifact carries it. */
|
|
6485
|
+
/** @internal Enumerated from the SAME registry-of-registries `rules()` dispatches on, so a profile a
|
|
6486
|
+
* verb can run is a profile this answers with. Naming the registries here instead left the three
|
|
6487
|
+
* profiles added with the CMS, timestamp and attribute-certificate verbs undiscoverable while
|
|
6488
|
+
* `rules()` returned their rows, and a fourth verb would have repeated it. The order follows
|
|
6489
|
+
* BY_ARTIFACT's, and a name reachable under two artifacts is listed once. */
|
|
5565
6490
|
function profiles() {
|
|
5566
|
-
var out =
|
|
5567
|
-
Object.keys(
|
|
6491
|
+
var out = [];
|
|
6492
|
+
Object.keys(BY_ARTIFACT).forEach(function (artifact) {
|
|
6493
|
+
Object.keys(BY_ARTIFACT[artifact]).forEach(function (name) {
|
|
6494
|
+
if (out.indexOf(name) === -1) out.push(name);
|
|
6495
|
+
});
|
|
6496
|
+
});
|
|
5568
6497
|
return out;
|
|
5569
6498
|
}
|
|
5570
6499
|
|
|
@@ -5573,6 +6502,9 @@ module.exports = {
|
|
|
5573
6502
|
csr: csr,
|
|
5574
6503
|
crl: crl,
|
|
5575
6504
|
ocsp: ocsp,
|
|
6505
|
+
cms: cms,
|
|
6506
|
+
tsp: tsp,
|
|
6507
|
+
attrcert: attrcert,
|
|
5576
6508
|
rules: rules,
|
|
5577
6509
|
profiles: profiles,
|
|
5578
6510
|
};
|