@blamejs/pki 0.7.8 → 0.7.10
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 +26 -0
- package/README.md +1 -1
- package/lib/lint.js +846 -16
- package/lib/ocsp.js +25 -2
- package/lib/path-validate.js +14 -30
- package/lib/schema-pkix.js +53 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.7.10 — 2026-09-10
|
|
8
|
+
|
|
9
|
+
An OCSP response is no longer signed with a status window that runs backwards.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- pki.ocsp.sign raises ocsp/bad-input for a response entry whose thisUpdate is after its nextUpdate, before anything is signed. RFC 6960 section 4.2.2.1 makes the pair a validity interval corresponding to the {thisUpdate, nextUpdate} interval in CRLs, and pki.crl.sign, pki.x509.sign, pki.attrcert.sign and pki.crmf.build each already refuse their own reversed window. Every entry in a batch is measured, not the first alone, and a nextUpdate that came from the default is held to the rule a supplied one is.
|
|
14
|
+
- An entry whose thisUpdate equals its nextUpdate still signs, as the equivalent CRL does: a zero-length window is not forbidden. A response entry that names no certificate still reports that first. The times are compared as given, so a thisUpdate later than its nextUpdate is refused even where the two fall inside one second and would encode to the same GeneralizedTime, which is how pki.x509.sign, pki.crl.sign and pki.crmf.build read their own.
|
|
15
|
+
- A pre-produced response still signs. RFC 6960 section 2.5 lets a responder sign a response ahead of the window it certifies, where producedAt is when the response was signed and thisUpdate is when the status was known correct, so a producedAt earlier than thisUpdate is conforming and is left alone.
|
|
16
|
+
|
|
17
|
+
## v0.7.9 — 2026-09-10
|
|
18
|
+
|
|
19
|
+
Certificate revocation lists can now be linted against the RFC 5280 profile.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- pki.lint.crl(pem | der | parsed, opts) lints a certificate revocation list against RFC 5280 section 5, under the new profile name rfc5280-crl. It returns the same LintReport as pki.lint.certificate, and hostile bytes produce a single fatal lint/unparseable finding carrying the strict parser's code rather than raising.
|
|
24
|
+
- On the CRL itself: nextUpdate presence (section 5.1.2.5) and thisUpdate not following it; cRLNumber presence, and the 20-octet ceiling section 5.2.3 places on a CRL number, measured on both the cRLNumber and the baseCRLNumber a delta CRL carries, which section 5.2.4 defines as a CRLNumber; both halves of the authorityKeyIdentifier requirement, that it is present and that it uses the key identifier method (section 5.2.1); the criticality every profiled extension must carry (section 5.2), with issuerAltName reported separately at warn because section 5.2.2 states its criticality as a recommendation; freshestCRL in a delta CRL, and one carrying the reasons or cRLIssuer fields section 5.2.6 requires a CRL to omit (section 5.2.6); a delta whose cRLNumber does not exceed its baseCRLNumber (section 5.2.4); an issuingDistributionPoint stating a scope the profile does not permit (section 5.2.5), which includes a reason scope naming no reason at all, as an empty mask or one setting only the unused bit does; a distribution point named relative to the CRL issuer, in either the issuingDistributionPoint or the freshestCRL, at warn, since section 4.2.1.13 states that as a recommendation; an authorityInfoAccess that names no caIssuers access method, or that carries one other than caIssuers (section 5.2.7), with a warning where no location it gives is an HTTP or LDAP URI; and an unrecognized critical extension.
|
|
25
|
+
- On each revoked entry: the entry-extension criticality section 5.3 requires; a certificate serial that is not positive or runs past 20 octets, the same section 4.1.2.2 profile a certificate's own serial carries; a removeFromCRL reason code outside a delta CRL (section 5.3.1); a certificateIssuer on a CRL that declares itself direct, or one that omits the issuer distinguished name the clause requires (section 5.3.3); a reasonCode encoding unspecified(0), which section 5.3.1 says should be absent instead, at warn, and as an error where the CRL is scoped to some reason codes, since section 5.2.5 makes a meaningful reason a requirement there; a reason the CRL's own issuingDistributionPoint does not declare, where that extension names at least one reason (section 5.2.5); a serial listed twice on a direct CRL, where a reader cannot tell which entry governs (section 5.1.2.6); and an unrecognized critical entry extension.
|
|
26
|
+
- An extension the section 5 profile names, whose value does not read as the structure that identifier names, is reported in either scope. Only extensions the CRL profile names are read this way: a certificate-only extension carried on a CRL is an unrecognized one, which the profile lets a consumer ignore when it is non-critical and which the unknown-critical row reports when it is not. Recognizing an extension by its identifier says only that the profile names it, so without this a critical issuingDistributionPoint carrying a NULL satisfied the criticality rule, was not an unrecognized extension, and reported clean although no consumer can process it. The issuingDistributionPoint is read through the same decoder certification-path validation uses, so the two verbs cannot disagree about what a well-formed one is. Where that extension does not read, the CRL states neither a scope nor a direction, so the rows that depend on it stand aside and the extension is reported once rather than every entry that would have followed from it.
|
|
27
|
+
- pki.lint.rules("rfc5280-crl") enumerates the CRL registry, and pki.lint.rules() with no argument now returns the certificate and CRL rules together. The two verbs do not accept each other's profile names: passing a CRL profile to pki.lint.certificate, or a certificate profile to pki.lint.crl, raises lint/unknown-profile naming the verb to use instead.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Six section 5 rules have no row, because the strict parser refuses them before a rule could run: a CRL whose version does not match the extensions it carries (section 5.1.2.1), a signatureAlgorithm that does not match the signature field inside tbsCertList (section 5.1.1.2), an empty issuer name (section 5.1.2.3), a present-but-empty revokedCertificates (section 5.1.2.6), a date carried in the wrong time type, which is UTCTime through 2049 and GeneralizedTime from 2050 (sections 5.1.2.4, 5.1.2.6 and 5.3.2), and an extension repeated on the CRL or on an entry (section 4.2). Each reaches a caller as the fatal lint/unparseable finding carrying the parser's own code.
|
|
32
|
+
|
|
7
33
|
## v0.7.8 — 2026-09-10
|
|
8
34
|
|
|
9
35
|
A pre-encoded CRL extension whose value does not decode is refused instead of signed.
|
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
252
252
|
| `pki.sigstore` | Offline verifier for a Sigstore bundle, the artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots and Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key, the ephemeral Fulcio certificate chain validated as of the Rekor log time, the RFC 9162 inclusion proof folded to a Rekor-signed tree root, the log entry bound to this exact signature, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
|
|
253
253
|
| `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
|
|
254
254
|
| `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
|
|
255
|
-
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `rules`, `profiles` |
|
|
255
|
+
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `rules`, `profiles` |
|
|
256
256
|
| `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
|
|
257
257
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
|
|
258
258
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
|
package/lib/lint.js
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* @module pki.lint
|
|
6
6
|
* @nav Tooling
|
|
7
7
|
* @title Lint
|
|
8
|
-
* @fullname Certificate linting against CA/Browser Forum
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* @fullname Certificate and revocation-list linting against RFC 5280 and CA/Browser Forum
|
|
9
|
+
* requirements
|
|
10
|
+
* @intro The LINTING engine, the zlint / pkilint of JavaScript. It walks an ALREADY-PARSED
|
|
11
|
+
* certificate or certificate revocation list (from `pki.schema.x509.parse` or
|
|
12
|
+
* `pki.schema.crl.parse`, whose extension values it decodes with the shared RFC 5280
|
|
13
|
+
* decoders) and emits graded, advisory FINDINGS: each with a stable id, a severity
|
|
14
|
+
* (`fatal` > `error` > `warn` > `notice` > `pass`), a source, a spec-clause citation, and
|
|
15
|
+
* a human message. It ships the RFC 5280 certificate profile, the RFC 5280 section 5
|
|
16
|
+
* revocation-list profile, the post-quantum certificate profiles for ML-KEM, ML-DSA and
|
|
17
|
+
* SLH-DSA, and a representative CA/Browser Forum TLS Baseline Requirements subset.
|
|
16
18
|
*
|
|
17
19
|
* Unlike every other toolkit entry, the lint data path never throws. A linter surveys a
|
|
18
20
|
* corpus that includes malformed members, so `pki.lint.certificate(hostileBytes)` returns
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
* raises a typed `LintError`. This deliberate inversion of the toolkit's fail-closed-throw
|
|
23
25
|
* posture is what lets an operator lint a whole directory without a try/catch per file.
|
|
24
26
|
* @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS Baseline Requirements
|
|
25
|
-
* @card Lint a certificate against RFC 5280 + CABF TLS BR, in pure JS.
|
|
27
|
+
* @card Lint a certificate or CRL against RFC 5280 + CABF TLS BR, in pure JS.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
var frameworkError = require("./framework-error");
|
|
@@ -30,7 +32,9 @@ var asn1 = require("./asn1-der");
|
|
|
30
32
|
var guard = require("./guard-all");
|
|
31
33
|
var oid = require("./oid");
|
|
32
34
|
var x509 = require("./schema-x509");
|
|
35
|
+
var crlSchema = require("./schema-crl");
|
|
33
36
|
var pkix = require("./schema-pkix");
|
|
37
|
+
var schema = require("./schema-engine");
|
|
34
38
|
var path = require("./path-validate");
|
|
35
39
|
var C = require("./constants");
|
|
36
40
|
var ipUtils = require("./ip-utils");
|
|
@@ -149,12 +153,27 @@ function _runLints(rules, cert, ctx) {
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
|
|
156
|
+
/** @internal The octet length of a big-endian magnitude. DER prefixes a 0x00 to a positive integer
|
|
157
|
+
* whose top bit is set, so that octet is encoding overhead rather than part of the number. Every
|
|
158
|
+
* rule that measures a number against a ceiling reads it here, so no two of them can answer
|
|
159
|
+
* differently for one value.
|
|
160
|
+
*
|
|
161
|
+
* The ceiling is measured against the VALUE, not its encoding. Sections 4.1.2.2 and 5.2.3 bound
|
|
162
|
+
* "serialNumber values" and "CRLNumber values", and each requires that users be able to handle
|
|
163
|
+
* values up to 20 octets. Every value from 2^159 through 2^160-1 is a 20-octet value whose DER
|
|
164
|
+
* content needs a 21st octet for the sign, so reading the ceiling against the encoding would make
|
|
165
|
+
* conforming values unrepresentable. Measuring the encoding is a real reading elsewhere in the
|
|
166
|
+
* ecosystem, which is why some CAs clear the top bit of a 20-octet serial; this is a deliberate
|
|
167
|
+
* choice between the two, not an oversight. */
|
|
168
|
+
function _magnitudeOctets(buf) {
|
|
169
|
+
if (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80)) return buf.length - 1;
|
|
170
|
+
return buf.length;
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
function _serialOctets(cert) {
|
|
153
174
|
var hex = cert.serialNumberHex || "";
|
|
154
175
|
if (hex.length % 2) hex = "0" + hex;
|
|
155
|
-
|
|
156
|
-
if (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80)) buf = buf.subarray(1);
|
|
157
|
-
return buf.length;
|
|
176
|
+
return _magnitudeOctets(Buffer.from(hex, "hex"));
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
function _dnsNameProblem(s) { return pkix.dnsNameProblem(s); }
|
|
@@ -559,6 +578,709 @@ var RFC9909_RULES = [
|
|
|
559
578
|
_pqcSigKeyUsageRule("lint/rfc9909/slhdsa-key-usage", "rfc9909", "RFC 9909 6", "SLH-DSA", _isSlhDsa),
|
|
560
579
|
];
|
|
561
580
|
|
|
581
|
+
/** @internal RFC 5280 sec. 5 states the criticality of each CRL extension it profiles, and sec. 5.3
|
|
582
|
+
* does the same for the entry extensions. certificateIssuer is critical (sec. 5.3.3) because a
|
|
583
|
+
* consumer that ignores it attributes an entry to the wrong issuer. */
|
|
584
|
+
/** @internal The extensions section 5.2 profiles for a CRL. This is the RECOGNIZED set, which is
|
|
585
|
+
* wider than the table below: issuerAltName is profiled here but its criticality is a SHOULD, so it
|
|
586
|
+
* is recognized without being held to a required value. */
|
|
587
|
+
var CRL_KNOWN_EXTENSIONS = Object.create(null);
|
|
588
|
+
["authorityKeyIdentifier", "issuerAltName", "cRLNumber", "deltaCRLIndicator",
|
|
589
|
+
"issuingDistributionPoint", "freshestCRL", "authorityInfoAccess"]
|
|
590
|
+
.forEach(function (n) { CRL_KNOWN_EXTENSIONS[oid.byName(n)] = true; });
|
|
591
|
+
|
|
592
|
+
/** @internal Only the extensions whose criticality section 5.2 states as a MUST. issuerAltName is
|
|
593
|
+
* deliberately absent: section 5.2.2 says a conforming issuer SHOULD mark it non-critical, and
|
|
594
|
+
* grading a SHOULD as an error would promote the requirement level the clause states. */
|
|
595
|
+
var CRL_CRITICALITY = Object.create(null);
|
|
596
|
+
[["cRLNumber", false], ["authorityKeyIdentifier", false], ["freshestCRL", false],
|
|
597
|
+
["authorityInfoAccess", false],
|
|
598
|
+
["issuingDistributionPoint", true], ["deltaCRLIndicator", true]]
|
|
599
|
+
.forEach(function (r) { CRL_CRITICALITY[oid.byName(r[0])] = r[1]; });
|
|
600
|
+
var CRL_ENTRY_CRITICALITY = Object.create(null);
|
|
601
|
+
[["reasonCode", false], ["invalidityDate", false], ["certificateIssuer", true]]
|
|
602
|
+
.forEach(function (r) { CRL_ENTRY_CRITICALITY[oid.byName(r[0])] = r[1]; });
|
|
603
|
+
|
|
604
|
+
function _crlExtList(crl) { return crl.crlExtensions || []; }
|
|
605
|
+
function _crlExtByName(crl, name) {
|
|
606
|
+
var dotted = oid.byName(name), list = _crlExtList(crl);
|
|
607
|
+
for (var i = 0; i < list.length; i++) { if (list[i].oid === dotted) return list[i]; }
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
/** @internal An AuthorityKeyIdentifier value read through the shared extension decoder, which holds
|
|
611
|
+
* it to the structure RFC 5280 sec. 4.2.1.1 states. Scanning for a [0] child by hand would admit a
|
|
612
|
+
* SET carrying one, and a SEQUENCE whose [0] is constructed, neither of which is an
|
|
613
|
+
* AuthorityKeyIdentifier. Bytes the decoder refuses come back as null and the caller reports that;
|
|
614
|
+
* nothing here returns a verdict. */
|
|
615
|
+
function _akiValue(bytes) {
|
|
616
|
+
var dec = EXT_DECODERS[oid.byName("authorityKeyIdentifier")];
|
|
617
|
+
if (!dec) return null;
|
|
618
|
+
// allow:swallow-unverified a value the shared decoder refuses is not an AuthorityKeyIdentifier, which the caller reports
|
|
619
|
+
try { return dec(bytes) || null; }
|
|
620
|
+
catch (_e) { return null; }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** @internal The decoded names of a GeneralNames value, or null when the bytes are not one. The
|
|
624
|
+
* value-syntax row reports that separately, so nothing here returns a verdict. */
|
|
625
|
+
function _generalNamesOf(bytes) {
|
|
626
|
+
// allow:swallow-unverified bytes that are not GeneralNames carry no names for the caller to read
|
|
627
|
+
try {
|
|
628
|
+
var m = schema.walk(pkix.generalNames(NS, { decodeValue: true, code: _LINT_VALUE_CODE }), asn1.decode(bytes), NS);
|
|
629
|
+
return (m.result && m.result.names) || null;
|
|
630
|
+
} catch (_e) { return null; }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** @internal A CRLReason value mapped onto its ReasonFlags bit. The two enumerations do NOT line
|
|
634
|
+
* up: RFC 5280 sec. 5.3.1 leaves CRLReason 7 unused and numbers privilegeWithdrawn 9 and
|
|
635
|
+
* aACompromise 10, while sec. 4.2.1.13 gives them ReasonFlags bits 7 and 8. unspecified(0) and
|
|
636
|
+
* removeFromCRL(8) have no bit at all, so neither can sit outside a reason scope and both are
|
|
637
|
+
* absent here; their own rows answer for them. Assuming identity would report conforming CRLs. */
|
|
638
|
+
var REASON_TO_FLAG_BIT = Object.create(null);
|
|
639
|
+
[[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [9, 7], [10, 8]]
|
|
640
|
+
.forEach(function (p) { REASON_TO_FLAG_BIT[p[0]] = p[1]; });
|
|
641
|
+
|
|
642
|
+
/** @internal Whether a ReasonFlags BIT STRING has the given bit set. */
|
|
643
|
+
function _reasonBitSet(bs, bit) {
|
|
644
|
+
if (!bs || !bs.bytes) return false;
|
|
645
|
+
var byteI = bit >> 3;
|
|
646
|
+
if (byteI >= bs.bytes.length) return false;
|
|
647
|
+
return (bs.bytes[byteI] & (0x80 >> (bit & 7))) !== 0;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** @internal Whether a ReasonFlags mask names at least one reason code. Bit 0 is named `unused`,
|
|
651
|
+
* so a mask setting only it, or setting nothing at all, names none and partitions nothing.
|
|
652
|
+
*
|
|
653
|
+
* Bit 0 set ALONGSIDE a real reason is deliberately not reported. Section 5.2.5 requires the reason
|
|
654
|
+
* codes associated with a distribution point to be specified here, and such a mask specifies one;
|
|
655
|
+
* RFC 5280 states nothing about bit 0 being clear, so reporting it would name a violation the
|
|
656
|
+
* profile does not state. */
|
|
657
|
+
function _scopeNamesAnyReason(bs) {
|
|
658
|
+
for (var bit = 1; bit <= 8; bit++) { if (_reasonBitSet(bs, bit)) return true; }
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** @internal The ReasonFlags of a CRL's issuingDistributionPoint, or null when it states no reason
|
|
663
|
+
* scope. Read through the shared decoder, so a malformed one scopes nothing. A mask naming no
|
|
664
|
+
* reason scopes nothing either: it gives an entry no partition to belong to, so the extension is
|
|
665
|
+
* what is at fault and the profile row reports it rather than every entry in turn. */
|
|
666
|
+
function _crlReasonScope(crl) {
|
|
667
|
+
var e = _crlExtByName(crl, "issuingDistributionPoint");
|
|
668
|
+
if (!e || !Buffer.isBuffer(e.value)) return null;
|
|
669
|
+
var idp = pkix.decodeIssuingDistributionPointValue(NS, e.value, _LINT_VALUE_CODE);
|
|
670
|
+
if (idp.malformed || !idp.onlySomeReasonsPresent) return null;
|
|
671
|
+
if (!_scopeNamesAnyReason(idp.onlySomeReasonsValue)) return null;
|
|
672
|
+
return idp.onlySomeReasonsValue || null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** @internal Whether the CRL is scoped to some reason codes, which is what makes a revocation
|
|
676
|
+
* reason load-bearing: the reason decides which partition an entry belongs in. Reads the scope
|
|
677
|
+
* itself so the two cannot answer differently about the same CRL. */
|
|
678
|
+
function _crlHasReasonScope(crl) {
|
|
679
|
+
return _crlReasonScope(crl) !== null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** @internal Whether the CRL affirmatively declares itself DIRECT, read from the same
|
|
683
|
+
* IssuingDistributionPoint decoder certification-path validation uses. Three cases, because a
|
|
684
|
+
* malformed extension is not the same as an absent one: a CRL carrying no issuingDistributionPoint
|
|
685
|
+
* is direct by omission, a well-formed one answers with its indirectCRL flag, and one that does not
|
|
686
|
+
* decode declares neither direction. The rows that only mean something on a direct CRL stand aside
|
|
687
|
+
* for that last case, so an unreadable declaration is reported against the extension by the
|
|
688
|
+
* value-syntax row rather than against each entry in turn. That row is an error, so no CRL reaches
|
|
689
|
+
* a clean report with those rows unevaluated. */
|
|
690
|
+
function _crlDeclaresDirect(crl) {
|
|
691
|
+
var e = _crlExtByName(crl, "issuingDistributionPoint");
|
|
692
|
+
if (!e) return true;
|
|
693
|
+
if (!Buffer.isBuffer(e.value)) return false;
|
|
694
|
+
var idp = pkix.decodeIssuingDistributionPointValue(NS, e.value, _LINT_VALUE_CODE);
|
|
695
|
+
if (idp.malformed) return false;
|
|
696
|
+
return idp.indirect !== true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
var CA_ISSUERS_OID = oid.byName("caIssuers");
|
|
700
|
+
|
|
701
|
+
/** @internal A recognized CRL extension's value, read through the same decoder every other consumer
|
|
702
|
+
* of that extension uses. null when the extension is absent or its value does not read, and the
|
|
703
|
+
* value-syntax row reports the latter on its own. */
|
|
704
|
+
function _crlDecodedExtension(crl, name) {
|
|
705
|
+
var e = _crlExtByName(crl, name);
|
|
706
|
+
if (!e || !Buffer.isBuffer(e.value)) return null;
|
|
707
|
+
var dec = EXT_DECODERS[oid.byName(name)];
|
|
708
|
+
if (!dec) return null;
|
|
709
|
+
try {
|
|
710
|
+
return dec(e.value);
|
|
711
|
+
} catch (_e) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** @internal Whether a string opens with the given URI scheme followed by a colon, compared
|
|
717
|
+
* without regard to case by scanning character codes. */
|
|
718
|
+
function _uriSchemeIs(s, scheme) {
|
|
719
|
+
if (typeof s !== "string" || s.length <= scheme.length) return false;
|
|
720
|
+
for (var i = 0; i < scheme.length; i++) {
|
|
721
|
+
var c = s.charCodeAt(i);
|
|
722
|
+
if (c >= 65 && c <= 90) c += 32;
|
|
723
|
+
if (c !== scheme.charCodeAt(i)) return false;
|
|
724
|
+
}
|
|
725
|
+
return s.charCodeAt(scheme.length) === 58;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** @internal The schemes that satisfy the section 5.2.7 recommendation. The secured forms count:
|
|
729
|
+
* warning about an https or ldaps location would report the more protected spelling of the two the
|
|
730
|
+
* clause names. */
|
|
731
|
+
var AIA_RECOMMENDED_SCHEMES = ["http", "https", "ldap", "ldaps"];
|
|
732
|
+
|
|
733
|
+
/** @internal The DER content length an integer occupies, which is what the 20-octet ceilings in
|
|
734
|
+
* RFC 5280 bound. Measured by re-encoding, since the parser hands back the decoded value; a
|
|
735
|
+
* non-minimal encoding cannot reach here because strict DER refuses one at parse. */
|
|
736
|
+
function _integerOctets(v) {
|
|
737
|
+
if (typeof v !== "bigint") return null;
|
|
738
|
+
// allow:swallow-unverified a value the codec cannot re-encode is not one this can size
|
|
739
|
+
try { return _magnitudeOctets(asn1.decode(asn1.build.integer(v)).content); }
|
|
740
|
+
catch (_e) { return null; }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** @internal The extensions that carry a CRLNumber. RFC 5280 sec. 5.2.4 defines BaseCRLNumber as a
|
|
744
|
+
* CRLNumber, so the sec. 5.2.3 ceiling bounds the delta indicator's value as well as the CRL's own
|
|
745
|
+
* number. The parser decodes cRLNumber to a bigint and leaves deltaCRLIndicator opaque, so each
|
|
746
|
+
* carrier names how its value is read. */
|
|
747
|
+
var CRL_NUMBER_CARRIERS = [
|
|
748
|
+
{ name: "cRLNumber", read: function (e) { return e.value; } },
|
|
749
|
+
{ name: "deltaCRLIndicator", read: function (e) { return _crlIntegerValue(e.value); } },
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
/** @internal An extension value the CRL parser leaves opaque, read as the INTEGER it should be.
|
|
753
|
+
* Bytes that are not one come back as null and the caller decides what that means. */
|
|
754
|
+
function _crlIntegerValue(bytes) {
|
|
755
|
+
if (!Buffer.isBuffer(bytes)) return null;
|
|
756
|
+
// allow:swallow-unverified bytes that are not an INTEGER are reported by the value-syntax row, not decided here
|
|
757
|
+
try {
|
|
758
|
+
var n = asn1.decode(bytes);
|
|
759
|
+
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.INTEGER) return null;
|
|
760
|
+
return asn1.read.integer(n);
|
|
761
|
+
} catch (_e) { return null; }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** @internal How each recognized extension's value is read, for the ones the CRL parser leaves as
|
|
765
|
+
* opaque bytes. Each walks the shared schema for that structure rather than testing an outer tag:
|
|
766
|
+
* a SEQUENCE carrying a NULL is not an IssuingDistributionPoint, and matching the outer type would
|
|
767
|
+
* admit it. Where a shared decoder exists it answers instead, and where the parser already decoded
|
|
768
|
+
* the value to a JavaScript type there is nothing left to check. */
|
|
769
|
+
var _LINT_VALUE_CODE = "lint/bad-extension-value";
|
|
770
|
+
var CRL_VALUE_READER = Object.create(null);
|
|
771
|
+
CRL_VALUE_READER[oid.byName("deltaCRLIndicator")] = function (bytes) {
|
|
772
|
+
var n = asn1.decode(bytes);
|
|
773
|
+
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.INTEGER) throw _cfg(_LINT_VALUE_CODE, "not an INTEGER");
|
|
774
|
+
/** @internal BaseCRLNumber is a CRLNumber, which RFC 5280 sec. 5.2.3 constrains to a
|
|
775
|
+
* non-negative integer, so a negative one is not a BaseCRLNumber at all. */
|
|
776
|
+
if (asn1.read.integer(n) < 0n) throw _cfg(_LINT_VALUE_CODE, "a negative BaseCRLNumber");
|
|
777
|
+
};
|
|
778
|
+
/** @internal Read through the SAME decoder certification-path validation uses, rather than a walk
|
|
779
|
+
* assembled here. The shared IssuingDistributionPoint schema leaves distributionPoint as `any`, so
|
|
780
|
+
* validating only that far accepts a wrapper carrying something that is not a DistributionPointName
|
|
781
|
+
* at all, and the linter would call well formed what the path validator rejects. */
|
|
782
|
+
CRL_VALUE_READER[oid.byName("issuingDistributionPoint")] = function (bytes) {
|
|
783
|
+
if (pkix.decodeIssuingDistributionPointValue(NS, bytes, _LINT_VALUE_CODE).malformed) {
|
|
784
|
+
throw _cfg(_LINT_VALUE_CODE, "malformed IssuingDistributionPoint");
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
/** @internal Only certificateIssuer. The parser decodes reasonCode and invalidityDate itself and
|
|
788
|
+
* refuses a value that is not the type they name, so an entry for either would be dead. */
|
|
789
|
+
var CRL_ENTRY_VALUE_READER = Object.create(null);
|
|
790
|
+
CRL_ENTRY_VALUE_READER[oid.byName("certificateIssuer")] = function (bytes) {
|
|
791
|
+
schema.walk(pkix.generalNames(NS, { code: _LINT_VALUE_CODE }), asn1.decode(bytes), NS);
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
/** @internal Whether a recognized extension's value reads as the structure its identifier names.
|
|
795
|
+
* Returns null when it does, and a reason when it does not. An extension whose value the parser
|
|
796
|
+
* already turned into a JavaScript value needs no check: it could not have been decoded otherwise. */
|
|
797
|
+
function _valueSyntaxFault(e, knownTable, readerTable) {
|
|
798
|
+
/** @internal Only an extension section 5 profiles FOR A CRL has a syntax to enforce here. A
|
|
799
|
+
* certificate-only extension carried on a CRL is an unrecognized one, which section 5.2 lets a
|
|
800
|
+
* consumer ignore when it is non-critical and the unknown-critical row reports when it is not;
|
|
801
|
+
* decoding its value against the CERTIFICATE profile would report a fault no CRL rule states. */
|
|
802
|
+
if (knownTable[e.oid] === undefined) return null;
|
|
803
|
+
var dec = EXT_DECODERS[e.oid];
|
|
804
|
+
if (dec && Buffer.isBuffer(e.value)) {
|
|
805
|
+
// allow:swallow-unverified the decoder's refusal IS the fault this returns
|
|
806
|
+
try { dec(e.value); return null; }
|
|
807
|
+
catch (_e) { return "does not decode as the structure its extension identifier names"; }
|
|
808
|
+
}
|
|
809
|
+
var read = readerTable[e.oid];
|
|
810
|
+
if (read === undefined || !Buffer.isBuffer(e.value)) return null;
|
|
811
|
+
// allow:swallow-unverified the reader's refusal IS the fault this returns
|
|
812
|
+
try { read(e.value); return null; }
|
|
813
|
+
catch (_e) { return "does not decode as the structure its extension identifier names"; }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function _criticalityRows(list, table, what) {
|
|
817
|
+
var out = [];
|
|
818
|
+
(list || []).forEach(function (e) {
|
|
819
|
+
/** @internal The table has a null prototype and boolean values, so an absent OID is undefined. */
|
|
820
|
+
var want = table[e.oid];
|
|
821
|
+
if (want === undefined) return;
|
|
822
|
+
if ((e.critical === true) !== want) {
|
|
823
|
+
out.push({ context: { extension: e.name || e.oid, required: want ? "critical" : "non-critical", was: e.critical === true ? "critical" : "non-critical", scope: what } });
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
return out;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** @internal Three of the section 5 rules have no row here, because the strict parser refuses them
|
|
830
|
+
* before a rule could run and a rule for them would be dead: the version against the extensions
|
|
831
|
+
* present (sec. 5.1.2.1, `crl/bad-version`), an empty issuer name (sec. 5.1.2.3, `crl/bad-issuer`),
|
|
832
|
+
* and a present-but-empty revokedCertificates (sec. 5.1.2.6, `crl/bad-revoked-certificates`). Each
|
|
833
|
+
* reaches a caller as the engine's fatal lint/unparseable carrying that code. */
|
|
834
|
+
var RFC5280_CRL_RULES = [
|
|
835
|
+
{
|
|
836
|
+
id: "lint/rfc5280-crl/next-update-missing", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.2.5",
|
|
837
|
+
message: "a conforming CRL issuer includes the nextUpdate field in every CRL",
|
|
838
|
+
check: function (crl) { return crl.nextUpdate == null ? true : null; },
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
id: "lint/rfc5280-crl/update-times-inverted", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.2.4 / 5.1.2.5",
|
|
842
|
+
message: "a CRL's thisUpdate must not follow its nextUpdate",
|
|
843
|
+
check: function (crl) {
|
|
844
|
+
// allow:nan-date-comparison-unguarded -- both are codec-parsed CRL times (asn1 readTime rejects a NaN instant).
|
|
845
|
+
if (!guard.time.isDate(crl.thisUpdate) || !guard.time.isDate(crl.nextUpdate)) return null;
|
|
846
|
+
return guard.time.instantOf(crl.thisUpdate) > guard.time.instantOf(crl.nextUpdate) ? true : null;
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
id: "lint/rfc5280-crl/crl-number-missing", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.3",
|
|
851
|
+
message: "a conforming CRL issuer includes the cRLNumber extension in every CRL",
|
|
852
|
+
check: function (crl) { return _crlExtByName(crl, "cRLNumber") ? null : true; },
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: "lint/rfc5280-crl/crl-number-too-long", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.3 / 5.2.4",
|
|
856
|
+
/** @internal One requirement over two carriers, so one row measures both and the context names
|
|
857
|
+
* which one it measured. A value that is not an INTEGER sizes as null here and is reported by
|
|
858
|
+
* the value-syntax row instead. */
|
|
859
|
+
message: "a CRL number must not be longer than 20 octets",
|
|
860
|
+
check: function (crl) {
|
|
861
|
+
var out = [];
|
|
862
|
+
CRL_NUMBER_CARRIERS.forEach(function (carrier) {
|
|
863
|
+
var e = _crlExtByName(crl, carrier.name);
|
|
864
|
+
if (!e) return;
|
|
865
|
+
var octets = _integerOctets(carrier.read(e));
|
|
866
|
+
if (octets !== null && octets > 20) out.push({ context: { extension: carrier.name, octets: octets } });
|
|
867
|
+
});
|
|
868
|
+
return out.length ? out : null;
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
id: "lint/rfc5280-crl/aki-missing", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.1",
|
|
873
|
+
/** @internal Section 5.2.1 states this in the same terms section 5.2.3 states cRLNumber:
|
|
874
|
+
* conforming CRL issuers MUST include the extension in all CRLs. Two clauses of the same
|
|
875
|
+
* strength are graded the same, so a caller filtering at `error` sees both or neither. The
|
|
876
|
+
* certificate profile grades its own authorityKeyIdentifier row differently, and that is
|
|
877
|
+
* shipped surface left as it is rather than re-graded from here. */
|
|
878
|
+
message: "a conforming CRL issuer includes an authorityKeyIdentifier in every CRL",
|
|
879
|
+
check: function (crl) { return _crlExtByName(crl, "authorityKeyIdentifier") ? null : true; },
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
id: "lint/rfc5280-crl/aki-without-key-identifier", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.1",
|
|
883
|
+
message: "a CRL's authorityKeyIdentifier must be well formed and use the key identifier method",
|
|
884
|
+
/** @internal Section 5.2.1 states two requirements: include the extension, and use the key
|
|
885
|
+
* identifier method. The row above answers the first. This answers the second, and it also
|
|
886
|
+
* answers for the value being an AuthorityKeyIdentifier at all, since the CRL parser leaves
|
|
887
|
+
* this extension as opaque bytes and nothing else reads it. */
|
|
888
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "authorityKeyIdentifier"); },
|
|
889
|
+
check: function (crl) {
|
|
890
|
+
var e = _crlExtByName(crl, "authorityKeyIdentifier");
|
|
891
|
+
if (!Buffer.isBuffer(e.value)) return null;
|
|
892
|
+
var v = _akiValue(e.value);
|
|
893
|
+
if (v === null) return { context: { undecodable: true } };
|
|
894
|
+
return v.keyIdentifier ? null : true;
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
id: "lint/rfc5280-crl/extension-criticality", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2",
|
|
899
|
+
message: "a CRL extension is marked with a criticality the profile does not allow",
|
|
900
|
+
check: function (crl) {
|
|
901
|
+
var rows = _criticalityRows(_crlExtList(crl), CRL_CRITICALITY, "crlExtensions");
|
|
902
|
+
return rows.length ? rows : null;
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
id: "lint/rfc5280-crl/entry-extension-criticality", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.3",
|
|
907
|
+
message: "a CRL entry extension is marked with a criticality the profile does not allow",
|
|
908
|
+
check: function (crl) {
|
|
909
|
+
/** @internal Appended rather than concatenated: a CRL may list very many entries, and this
|
|
910
|
+
* verb exists to survey ones that arrived from elsewhere, so rebuilding the accumulator per
|
|
911
|
+
* entry would make the rule quadratic in the length of a list an attacker chooses. */
|
|
912
|
+
var rows = [];
|
|
913
|
+
(crl.revokedCertificates || []).forEach(function (r, i) {
|
|
914
|
+
_criticalityRows(r.crlEntryExtensions, CRL_ENTRY_CRITICALITY, "crlEntryExtensions")
|
|
915
|
+
.forEach(function (row) { row.context.entry = i; rows.push(row); });
|
|
916
|
+
});
|
|
917
|
+
return rows.length ? rows : null;
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: "lint/rfc5280-crl/issuer-alt-name-critical", severity: "warn", source: "rfc5280-crl", citation: "RFC 5280 5.2.2",
|
|
922
|
+
message: "a conforming CRL issuer should mark the issuerAltName extension as non-critical",
|
|
923
|
+
check: function (crl) {
|
|
924
|
+
var e = _crlExtByName(crl, "issuerAltName");
|
|
925
|
+
return (e && e.critical === true) ? true : null;
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
id: "lint/rfc5280-crl/duplicate-entry-serial", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.2.6",
|
|
930
|
+
/** @internal On a direct CRL a serial identifies one certificate, so listing it twice states
|
|
931
|
+
* two revocations of the same certificate and a reader cannot tell which entry governs. An
|
|
932
|
+
* INDIRECT CRL is excluded: there a serial is unique per issuer rather than per list, and the
|
|
933
|
+
* effective issuer is carried forward by certificateIssuer, which is a different computation
|
|
934
|
+
* from the one here. */
|
|
935
|
+
appliesTo: function (crl) { return _crlDeclaresDirect(crl); },
|
|
936
|
+
message: "a direct CRL lists the same certificate serial number more than once",
|
|
937
|
+
check: function (crl) {
|
|
938
|
+
var seen = Object.create(null), out = [];
|
|
939
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
940
|
+
if (typeof row.serialNumber !== "bigint") return;
|
|
941
|
+
var key = row.serialNumber.toString(16);
|
|
942
|
+
if (seen[key] !== undefined) out.push({ context: { entry: i, firstSeenAt: seen[key], serialNumber: String(row.serialNumber) } });
|
|
943
|
+
else seen[key] = i;
|
|
944
|
+
});
|
|
945
|
+
return out.length ? out : null;
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
id: "lint/rfc5280-crl/certificate-issuer-on-direct-crl", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.3.3",
|
|
950
|
+
/** @internal certificateIssuer names the issuer an entry belongs to, which only means something
|
|
951
|
+
* on a CRL that declares itself indirect through the issuingDistributionPoint indirectCRL flag.
|
|
952
|
+
* On a direct CRL the entry and the list disagree about whose certificates are covered, and no
|
|
953
|
+
* revocation status follows from it. */
|
|
954
|
+
appliesTo: function (crl) { return _crlDeclaresDirect(crl); },
|
|
955
|
+
message: "a revoked entry carries certificateIssuer on a CRL that does not declare itself indirect",
|
|
956
|
+
check: function (crl) {
|
|
957
|
+
var out = [];
|
|
958
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
959
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
960
|
+
if (e.oid === oid.byName("certificateIssuer")) out.push({ context: { entry: i } });
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
return out.length ? out : null;
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
id: "lint/rfc5280-crl/certificate-issuer-without-dn", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.3.3",
|
|
968
|
+
message: "a certificateIssuer must carry the issuer distinguished name of the certificate its entry names",
|
|
969
|
+
/** @internal Section 5.3.3: a conforming issuer MUST include in this extension the distinguished
|
|
970
|
+
* name from the issuer field of the certificate the entry names. In a GeneralNames that is the
|
|
971
|
+
* directoryName [4] arm, so a certificateIssuer carrying only some other name form does not
|
|
972
|
+
* attribute the entry to an issuer at all. Separate from whether the value decodes. */
|
|
973
|
+
check: function (crl) {
|
|
974
|
+
var out = [];
|
|
975
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
976
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
977
|
+
if (e.oid !== oid.byName("certificateIssuer") || !Buffer.isBuffer(e.value)) return;
|
|
978
|
+
var names = _generalNamesOf(e.value);
|
|
979
|
+
if (names === null) return;
|
|
980
|
+
/** @internal A directoryName wrapping an empty Name is a name container holding no name.
|
|
981
|
+
* RFC 5280 sec. 4.1.2.4 gives a certificate's issuer a non-empty distinguished name, so
|
|
982
|
+
* an empty one cannot be the DN sec. 5.3.3 requires, and matching only the tag would
|
|
983
|
+
* accept it. */
|
|
984
|
+
var hasDn = false;
|
|
985
|
+
names.forEach(function (n) {
|
|
986
|
+
if (n.tagNumber === 4 && n.value && (n.value.rdns || []).length > 0) hasDn = true;
|
|
987
|
+
});
|
|
988
|
+
if (!hasDn) out.push({ context: { entry: i } });
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
return out.length ? out : null;
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
id: "lint/rfc5280-crl/unspecified-reason-in-reason-scoped-crl", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.5",
|
|
996
|
+
/** @internal Section 5.2.5: where the issuingDistributionPoint carries onlySomeReasons, every
|
|
997
|
+
* revoked certificate in scope MUST be assigned a reason other than unspecified, because the
|
|
998
|
+
* reason is what decides which partition the entry belongs in. That is a MUST here, where the
|
|
999
|
+
* same encoding is only a SHOULD under section 5.3.1, so it is reported as an error and the
|
|
1000
|
+
* SHOULD row stands aside. An entry carrying NO reasonCode is untouched: the clause says the
|
|
1001
|
+
* extension need not be present. */
|
|
1002
|
+
appliesTo: function (crl) { return _crlHasReasonScope(crl); },
|
|
1003
|
+
message: "a CRL scoped to some reason codes lists an entry whose reasonCode is unspecified(0)",
|
|
1004
|
+
check: function (crl) {
|
|
1005
|
+
var out = [];
|
|
1006
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1007
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
1008
|
+
if (e.oid === oid.byName("reasonCode") && Number(e.value) === 0) out.push({ context: { entry: i } });
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
return out.length ? out : null;
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
id: "lint/rfc5280-crl/reason-outside-crl-scope", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.5",
|
|
1016
|
+
/** @internal Section 5.2.5: the reason codes associated with a distribution point MUST be
|
|
1017
|
+
* specified in onlySomeReasons, and the section's own partitioning example shows a CRL carrying
|
|
1018
|
+
* only the reasons it declares. An entry whose reason is not among them is a revocation this
|
|
1019
|
+
* CRL says it does not carry, so a reader partitioning by reason would miss it. */
|
|
1020
|
+
appliesTo: function (crl) { return _crlReasonScope(crl) !== null; },
|
|
1021
|
+
message: "a revoked entry states a reason the CRL's issuingDistributionPoint does not declare",
|
|
1022
|
+
check: function (crl) {
|
|
1023
|
+
var scope = _crlReasonScope(crl), out = [];
|
|
1024
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1025
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
1026
|
+
if (e.oid !== oid.byName("reasonCode")) return;
|
|
1027
|
+
var bit = REASON_TO_FLAG_BIT[Number(e.value)];
|
|
1028
|
+
if (bit === undefined) return;
|
|
1029
|
+
if (!_reasonBitSet(scope, bit)) out.push({ context: { entry: i, reasonCode: Number(e.value) } });
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
return out.length ? out : null;
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
id: "lint/rfc5280-crl/reason-code-unspecified", severity: "warn", source: "rfc5280-crl", citation: "RFC 5280 5.3.1",
|
|
1037
|
+
/** @internal Section 5.3.1 states this as a SHOULD, so it is a warning rather than an error:
|
|
1038
|
+
* the reason code extension should be absent instead of encoding unspecified(0), which says
|
|
1039
|
+
* nothing a reader can act on while costing an extension slot. Where the CRL is scoped to some
|
|
1040
|
+
* reason codes the row above answers instead, at the strength section 5.2.5 states. */
|
|
1041
|
+
message: "a reasonCode extension encoding unspecified(0) should be absent instead",
|
|
1042
|
+
appliesTo: function (crl) { return !_crlHasReasonScope(crl); },
|
|
1043
|
+
check: function (crl) {
|
|
1044
|
+
var out = [];
|
|
1045
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1046
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
1047
|
+
if (e.oid === oid.byName("reasonCode") && Number(e.value) === 0) out.push({ context: { entry: i } });
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
return out.length ? out : null;
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
id: "lint/rfc5280-crl/entry-serial-not-positive", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 4.1.2.2",
|
|
1055
|
+
message: "a revoked entry's certificate serial number must be a positive integer",
|
|
1056
|
+
/** @internal userCertificate is a CertificateSerialNumber, so it carries the sec. 4.1.2.2
|
|
1057
|
+
* profile the certificate rows already apply to a certificate's own serial. */
|
|
1058
|
+
check: function (crl) {
|
|
1059
|
+
var out = [];
|
|
1060
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1061
|
+
if (typeof row.serialNumber === "bigint" && row.serialNumber <= 0n) {
|
|
1062
|
+
out.push({ context: { entry: i, serialNumber: String(row.serialNumber) } });
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
return out.length ? out : null;
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
id: "lint/rfc5280-crl/entry-serial-too-long", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 4.1.2.2",
|
|
1070
|
+
message: "a revoked entry's certificate serial number must not be longer than 20 octets",
|
|
1071
|
+
check: function (crl) {
|
|
1072
|
+
var out = [];
|
|
1073
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1074
|
+
var octets = _integerOctets(row.serialNumber);
|
|
1075
|
+
if (octets !== null && octets > 20) out.push({ context: { entry: i, octets: octets } });
|
|
1076
|
+
});
|
|
1077
|
+
return out.length ? out : null;
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
id: "lint/rfc5280-crl/idp-profile", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.5",
|
|
1082
|
+
message: "an issuingDistributionPoint states a scope the profile does not permit",
|
|
1083
|
+
/** @internal A separate decision from whether the value DECODES, which the value-syntax row
|
|
1084
|
+
* answers. These are the constraints section 5.2.5 places on a well-formed one, read from the
|
|
1085
|
+
* same decoder certification-path validation uses. */
|
|
1086
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "issuingDistributionPoint"); },
|
|
1087
|
+
check: function (crl) {
|
|
1088
|
+
var idp = pkix.decodeIssuingDistributionPointValue(NS, _crlExtByName(crl, "issuingDistributionPoint").value, _LINT_VALUE_CODE);
|
|
1089
|
+
if (idp.malformed) return null;
|
|
1090
|
+
var out = [];
|
|
1091
|
+
if (!idp.hasDistributionPoint && !idp.onlyUser && !idp.onlyCa && !idp.onlySomeReasonsPresent && !idp.indirect && !idp.onlyAttr) {
|
|
1092
|
+
out.push({ context: { reason: "states no scope at all" } });
|
|
1093
|
+
}
|
|
1094
|
+
if (idp.onlyUser && idp.onlyCa) out.push({ context: { reason: "sets both onlyContainsUserCerts and onlyContainsCACerts" } });
|
|
1095
|
+
if (idp.onlyAttr) out.push({ context: { reason: "sets onlyContainsAttributeCerts" } });
|
|
1096
|
+
if (idp.onlySomeReasonsPresent && !_scopeNamesAnyReason(idp.onlySomeReasonsValue)) {
|
|
1097
|
+
out.push({ context: { reason: "sets onlySomeReasons to a mask naming no reason code" } });
|
|
1098
|
+
}
|
|
1099
|
+
return out.length ? out : null;
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
id: "lint/rfc5280-crl/freshest-crl-forbidden-field", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.6",
|
|
1104
|
+
/** @internal Section 5.2.6 reuses the cRLDistributionPoints syntax but states that only the
|
|
1105
|
+
* distribution point field is meaningful on a CRL, and that reasons and cRLIssuer are omitted.
|
|
1106
|
+
* The shared decoder accepts both because the certificate extension permits them, so this row
|
|
1107
|
+
* carries the restriction the certificate profile does not have. */
|
|
1108
|
+
message: "a CRL's freshestCRL must omit the reasons and cRLIssuer fields",
|
|
1109
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "freshestCRL"); },
|
|
1110
|
+
check: function (crl) {
|
|
1111
|
+
var dps = _crlDecodedExtension(crl, "freshestCRL");
|
|
1112
|
+
if (dps === null) return null;
|
|
1113
|
+
var out = [];
|
|
1114
|
+
dps.forEach(function (dp, i) {
|
|
1115
|
+
if (dp.reasons) out.push({ context: { index: i, field: "reasons" } });
|
|
1116
|
+
if (dp.cRLIssuer) out.push({ context: { index: i, field: "cRLIssuer" } });
|
|
1117
|
+
});
|
|
1118
|
+
return out.length ? out : null;
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
id: "lint/rfc5280-crl/aia-without-ca-issuers", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.7",
|
|
1123
|
+
/** @internal caIssuers is what this extension is for on a CRL: it names where to fetch the
|
|
1124
|
+
* certificates a reader verifies the CRL's signature with. Present without one, it gives a
|
|
1125
|
+
* reader nothing to fetch. */
|
|
1126
|
+
message: "a CRL's authorityInfoAccess must include an AccessDescription whose method is caIssuers",
|
|
1127
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "authorityInfoAccess"); },
|
|
1128
|
+
check: function (crl) {
|
|
1129
|
+
var descs = _crlDecodedExtension(crl, "authorityInfoAccess");
|
|
1130
|
+
if (descs === null) return null;
|
|
1131
|
+
var found = descs.some(function (d) { return d.accessMethod === CA_ISSUERS_OID; });
|
|
1132
|
+
if (found) return null;
|
|
1133
|
+
return { context: { methods: descs.map(function (d) { return oid.name(d.accessMethod) || d.accessMethod; }) } };
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
id: "lint/rfc5280-crl/aia-forbidden-access-method", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.7",
|
|
1138
|
+
message: "a CRL's authorityInfoAccess must not carry an access method other than caIssuers",
|
|
1139
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "authorityInfoAccess"); },
|
|
1140
|
+
check: function (crl) {
|
|
1141
|
+
var descs = _crlDecodedExtension(crl, "authorityInfoAccess");
|
|
1142
|
+
if (descs === null) return null;
|
|
1143
|
+
var out = [];
|
|
1144
|
+
descs.forEach(function (d, i) {
|
|
1145
|
+
if (d.accessMethod === CA_ISSUERS_OID) return;
|
|
1146
|
+
out.push({ context: { index: i, method: oid.name(d.accessMethod) || d.accessMethod } });
|
|
1147
|
+
});
|
|
1148
|
+
return out.length ? out : null;
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
id: "lint/rfc5280-crl/aia-location-not-http-or-ldap", severity: "warn", source: "rfc5280-crl", citation: "RFC 5280 5.2.7",
|
|
1153
|
+
/** @internal Section 5.2.7 states this one as a SHOULD, so it is a warning where the two rules
|
|
1154
|
+
* above are errors. */
|
|
1155
|
+
message: "at least one authorityInfoAccess location should be an HTTP or LDAP URI",
|
|
1156
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "authorityInfoAccess"); },
|
|
1157
|
+
check: function (crl) {
|
|
1158
|
+
var descs = _crlDecodedExtension(crl, "authorityInfoAccess");
|
|
1159
|
+
if (descs === null || descs.length === 0) return null;
|
|
1160
|
+
var reachable = descs.some(function (d) {
|
|
1161
|
+
var loc = d.accessLocation;
|
|
1162
|
+
if (!loc || loc.tag !== 6) return false;
|
|
1163
|
+
return AIA_RECOMMENDED_SCHEMES.some(function (s) { return _uriSchemeIs(loc.value, s); });
|
|
1164
|
+
});
|
|
1165
|
+
return reachable ? null : true;
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
id: "lint/rfc5280-crl/distribution-point-name-relative", severity: "warn", source: "rfc5280-crl", citation: "RFC 5280 5.2.5 / 5.2.6 / 4.2.1.13",
|
|
1170
|
+
/** @internal Sections 5.2.5 and 5.2.6 both give their distribution point the syntax and
|
|
1171
|
+
* encoding conventions section 4.2.1.13 states, and there naming a distribution point relative
|
|
1172
|
+
* to the CRL issuer is a SHOULD NOT, so this is a warning rather than an error. One requirement
|
|
1173
|
+
* over two carriers, so one row reads both and the context names which it read. The MUST NOT
|
|
1174
|
+
* stated beside it is conditioned on cRLIssuer carrying more than one name; an
|
|
1175
|
+
* IssuingDistributionPoint has no cRLIssuer field, and section 5.2.6 requires freshestCRL to
|
|
1176
|
+
* omit its own, so that sentence cannot reach a CRL either way. */
|
|
1177
|
+
message: "a distribution point name should not be given relative to the CRL issuer",
|
|
1178
|
+
appliesTo: function (crl) {
|
|
1179
|
+
return !!_crlExtByName(crl, "issuingDistributionPoint") || !!_crlExtByName(crl, "freshestCRL");
|
|
1180
|
+
},
|
|
1181
|
+
check: function (crl) {
|
|
1182
|
+
var out = [];
|
|
1183
|
+
var e = _crlExtByName(crl, "issuingDistributionPoint");
|
|
1184
|
+
if (e && Buffer.isBuffer(e.value)) {
|
|
1185
|
+
var idp = pkix.decodeIssuingDistributionPointValue(NS, e.value, _LINT_VALUE_CODE);
|
|
1186
|
+
if (!idp.malformed && idp.distributionPoint && idp.distributionPoint.kind === "rdn") {
|
|
1187
|
+
out.push({ context: { extension: "issuingDistributionPoint", form: "nameRelativeToCRLIssuer" } });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
var dps = _crlDecodedExtension(crl, "freshestCRL");
|
|
1191
|
+
if (dps) {
|
|
1192
|
+
dps.forEach(function (dp, i) {
|
|
1193
|
+
if (!dp.distributionPoint || dp.distributionPoint.kind !== "rdn") return;
|
|
1194
|
+
out.push({ context: { extension: "freshestCRL", index: i, form: "nameRelativeToCRLIssuer" } });
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return out.length ? out : null;
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
id: "lint/rfc5280-crl/delta-number-not-advancing", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.4",
|
|
1202
|
+
message: "a delta CRL's cRLNumber must be greater than its baseCRLNumber",
|
|
1203
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "deltaCRLIndicator"); },
|
|
1204
|
+
check: function (crl) {
|
|
1205
|
+
var num = _crlExtByName(crl, "cRLNumber");
|
|
1206
|
+
var base = _crlIntegerValue(_crlExtByName(crl, "deltaCRLIndicator").value);
|
|
1207
|
+
if (base === null || !num || typeof num.value !== "bigint") return null;
|
|
1208
|
+
return num.value > base ? null : { context: { crlNumber: String(num.value), baseCRLNumber: String(base) } };
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
id: "lint/rfc5280-crl/remove-from-crl-outside-delta", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.3.1",
|
|
1213
|
+
message: "the removeFromCRL reason code may appear only in a delta CRL",
|
|
1214
|
+
appliesTo: function (crl) { return !_crlExtByName(crl, "deltaCRLIndicator"); },
|
|
1215
|
+
check: function (crl) {
|
|
1216
|
+
var out = [];
|
|
1217
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1218
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
1219
|
+
if (e.oid === oid.byName("reasonCode") && Number(e.value) === 8) out.push({ context: { entry: i } });
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
return out.length ? out : null;
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
id: "lint/rfc5280-crl/freshest-in-delta", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2.6",
|
|
1227
|
+
message: "the freshestCRL extension must not appear in a delta CRL",
|
|
1228
|
+
appliesTo: function (crl) { return !!_crlExtByName(crl, "deltaCRLIndicator"); },
|
|
1229
|
+
check: function (crl) { return _crlExtByName(crl, "freshestCRL") ? true : null; },
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: "lint/rfc5280-crl/extension-value-syntax", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2 / 5.3",
|
|
1233
|
+
message: "a recognized extension's value does not read as the structure its identifier names",
|
|
1234
|
+
/** @internal Recognizing an extension by its identifier says only that the profile names it.
|
|
1235
|
+
* Without this, a critical extension the profile recognizes but whose value no consumer can
|
|
1236
|
+
* process reports clean: the criticality row is satisfied and the unknown-critical row does not
|
|
1237
|
+
* fire, because the identifier IS known. Both scopes are walked. */
|
|
1238
|
+
check: function (crl) {
|
|
1239
|
+
var out = [];
|
|
1240
|
+
_crlExtList(crl).forEach(function (e) {
|
|
1241
|
+
var why = _valueSyntaxFault(e, CRL_KNOWN_EXTENSIONS, CRL_VALUE_READER);
|
|
1242
|
+
if (why) out.push({ context: { extension: e.name || e.oid, scope: "crlExtensions", reason: why } });
|
|
1243
|
+
});
|
|
1244
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1245
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
1246
|
+
var why = _valueSyntaxFault(e, CRL_ENTRY_CRITICALITY, CRL_ENTRY_VALUE_READER);
|
|
1247
|
+
if (why) out.push({ context: { extension: e.name || e.oid, scope: "crlEntryExtensions", entry: i, reason: why } });
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
return out.length ? out : null;
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
id: "lint/rfc5280-crl/unknown-critical-extension", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.2 / 5.3",
|
|
1255
|
+
message: "a critical extension is not recognized by the profile -- a conforming consumer must not use the CRL",
|
|
1256
|
+
/** @internal Sections 5.2 and 5.3 state the same obligation, once for the CRL's extensions and
|
|
1257
|
+
* once for a revoked entry's, so both lists are walked. The recognized set for each scope is
|
|
1258
|
+
* the extensions that section profiles, which is exactly the table its criticality rule keys
|
|
1259
|
+
* on. The certificate registry cannot answer here: it does not recognize deltaCRLIndicator or
|
|
1260
|
+
* issuingDistributionPoint, which section 5.2 REQUIRES to be critical, and it does recognize
|
|
1261
|
+
* certificate-only extensions that have no meaning on a CRL. */
|
|
1262
|
+
check: function (crl) {
|
|
1263
|
+
var out = [];
|
|
1264
|
+
_crlExtList(crl).forEach(function (e) {
|
|
1265
|
+
if (e.critical === true && CRL_KNOWN_EXTENSIONS[e.oid] === undefined) {
|
|
1266
|
+
out.push({ context: { oid: e.oid, name: e.name || null, scope: "crlExtensions" } });
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
(crl.revokedCertificates || []).forEach(function (row, i) {
|
|
1270
|
+
(row.crlEntryExtensions || []).forEach(function (e) {
|
|
1271
|
+
if (e.critical === true && CRL_ENTRY_CRITICALITY[e.oid] === undefined) {
|
|
1272
|
+
out.push({ context: { oid: e.oid, name: e.name || null, scope: "crlEntryExtensions", entry: i } });
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
return out.length ? out : null;
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
];
|
|
1280
|
+
|
|
1281
|
+
var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
|
|
1282
|
+
var ALL_CRL_RULES = RFC5280_CRL_RULES;
|
|
1283
|
+
|
|
562
1284
|
var PROFILES = Object.assign(Object.create(null), {
|
|
563
1285
|
"rfc5280": RFC5280_RULES,
|
|
564
1286
|
"rfc9881": RFC9881_RULES,
|
|
@@ -570,7 +1292,10 @@ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat
|
|
|
570
1292
|
|
|
571
1293
|
function _selectRules(profile) {
|
|
572
1294
|
if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
|
|
573
|
-
if (!PROFILES[profile])
|
|
1295
|
+
if (!PROFILES[profile]) {
|
|
1296
|
+
if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
|
|
1297
|
+
throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\" (known: " + Object.keys(PROFILES).join(", ") + ")");
|
|
1298
|
+
}
|
|
574
1299
|
return PROFILES[profile];
|
|
575
1300
|
}
|
|
576
1301
|
|
|
@@ -636,6 +1361,107 @@ function certificate(input, opts) {
|
|
|
636
1361
|
return report;
|
|
637
1362
|
}
|
|
638
1363
|
|
|
1364
|
+
var _CRL_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
|
|
1365
|
+
|
|
1366
|
+
/** @internal The same door the certificate path uses. A parsed CRL is admitted only when the shared
|
|
1367
|
+
* guard says it is one, so an object that merely carries a `crlExtensions` property is refused with
|
|
1368
|
+
* the typed config error rather than reaching a rule and faulting there. */
|
|
1369
|
+
function _derivedCrl(o) {
|
|
1370
|
+
if (!o || typeof o !== "object" || guard.bytes.isByteSource(o)) return null;
|
|
1371
|
+
// allow:swallow-unverified an object the guard refuses is not a parsed CRL, and the byte paths below answer for it
|
|
1372
|
+
try {
|
|
1373
|
+
var p = guard.parsed.acceptDerived(o, "crl", crlSchema.parse, _cfg, "lint/bad-input", "the CRL");
|
|
1374
|
+
return guard.parsed.isCrl(p) ? p : null;
|
|
1375
|
+
} catch (_e) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function _ingestCrl(input) {
|
|
1381
|
+
var derived = _derivedCrl(input);
|
|
1382
|
+
if (derived) return { crl: derived };
|
|
1383
|
+
var der;
|
|
1384
|
+
if (Buffer.isBuffer(input)) der = input;
|
|
1385
|
+
else if (typeof input === "string") {
|
|
1386
|
+
try { der = crlSchema.pemDecode(input); }
|
|
1387
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER CRL", context: { code: e.code } } }; }
|
|
1388
|
+
} else {
|
|
1389
|
+
throw _cfg("lint/bad-input", "pki.lint.crl input must be a parsed CRL, a DER Buffer, or a PEM string");
|
|
1390
|
+
}
|
|
1391
|
+
try { return { crl: crlSchema.parse(der) }; }
|
|
1392
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5280", message: "input is not a well-formed X.509 CRL", context: { code: e.code } } }; }
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* @primitive pki.lint.crl
|
|
1397
|
+
* @signature pki.lint.crl(pem | der | parsed, opts?) -> LintReport
|
|
1398
|
+
* @since 0.7.9
|
|
1399
|
+
* @status stable
|
|
1400
|
+
* @spec RFC 5280 sec. 5
|
|
1401
|
+
* @related pki.lint.certificate, pki.schema.crl.parse, pki.crl.sign
|
|
1402
|
+
*
|
|
1403
|
+
* Lint a certificate revocation list against the RFC 5280 section 5 profile. `input` is a PEM
|
|
1404
|
+
* string, a DER `Buffer`, or an already-parsed `pki.schema.crl` object. It returns the same
|
|
1405
|
+
* `LintReport` shape `pki.lint.certificate` returns, and it never throws on the data path:
|
|
1406
|
+
* bytes that are not a well-formed CRL produce a single `fatal` finding `lint/unparseable`
|
|
1407
|
+
* carrying the strict parser's own code.
|
|
1408
|
+
*
|
|
1409
|
+
* The rows cover the CRL's update times (nextUpdate presence and ordering), its extensions
|
|
1410
|
+
* (cRLNumber presence and length, authorityKeyIdentifier presence and the key identifier method,
|
|
1411
|
+
* the criticality every profiled extension is required to carry, the scope an
|
|
1412
|
+
* issuingDistributionPoint states, freshestCRL in a delta CRL, and an unrecognized critical
|
|
1413
|
+
* extension), and each revoked entry (the serial profile, a reason code against the scope the CRL
|
|
1414
|
+
* declares, certificateIssuer, and entry-extension criticality).
|
|
1415
|
+
*
|
|
1416
|
+
* Six section 5 rules never appear here because the strict parser refuses them first, and a rule
|
|
1417
|
+
* for them could not fire. A CRL carrying extensions without the version that admits them
|
|
1418
|
+
* (section 5.1.2.1), a `signatureAlgorithm` differing from the `signature` field inside
|
|
1419
|
+
* `tbsCertList` (section 5.1.1.2), an empty issuer name (section 5.1.2.3), a present-but-empty
|
|
1420
|
+
* `revokedCertificates` (section 5.1.2.6), a date carried in the wrong time type, which is UTCTime
|
|
1421
|
+
* through 2049 and GeneralizedTime from 2050 (sections 5.1.2.4, 5.1.2.6 and 5.3.2), and an
|
|
1422
|
+
* extension repeated on the CRL or on an entry (section 4.2) each arrive as `lint/unparseable`
|
|
1423
|
+
* carrying the parser's own code.
|
|
1424
|
+
*
|
|
1425
|
+
* @opts profile `"rfc5280-crl"`, the only CRL profile (default runs every CRL rule). A
|
|
1426
|
+
* certificate profile name is refused rather than run against a CRL.
|
|
1427
|
+
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
|
|
1428
|
+
* always reflect the complete, unfiltered result.
|
|
1429
|
+
* @example
|
|
1430
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1431
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
1432
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n },
|
|
1433
|
+
* { name: "Example CA", publicKey: await pki.key.export(pair.publicKey), key: pair.privateKey });
|
|
1434
|
+
* var report = pki.lint.crl(crlDer);
|
|
1435
|
+
* report.worst; // -> "error", for the missing AKI
|
|
1436
|
+
* report.findings.map(function (f) { return f.id; }); // -> ["lint/rfc5280-crl/aki-missing"]
|
|
1437
|
+
*/
|
|
1438
|
+
function crl(input, opts) {
|
|
1439
|
+
opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
|
|
1440
|
+
guard.identifier.assertKnownKeys(opts, _CRL_OPTS, _cfg, "lint/bad-input",
|
|
1441
|
+
"pki.lint.crl has an unknown option: ");
|
|
1442
|
+
if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
|
|
1443
|
+
throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
|
|
1444
|
+
}
|
|
1445
|
+
var profile = opts.profile;
|
|
1446
|
+
var rules_;
|
|
1447
|
+
if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CRL_RULES;
|
|
1448
|
+
else if (CRL_PROFILES[profile]) rules_ = CRL_PROFILES[profile];
|
|
1449
|
+
else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
|
|
1450
|
+
else throw _cfg("lint/unknown-profile", "unknown CRL lint profile \"" + profile + "\" (known: " + Object.keys(CRL_PROFILES).join(", ") + ")");
|
|
1451
|
+
|
|
1452
|
+
var ingested = _ingestCrl(input);
|
|
1453
|
+
var report;
|
|
1454
|
+
if (ingested.fatal) {
|
|
1455
|
+
report = { findings: [ingested.fatal], ran: [],
|
|
1456
|
+
counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
|
|
1457
|
+
} else {
|
|
1458
|
+
report = _runLints(rules_, ingested.crl, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
|
|
1459
|
+
}
|
|
1460
|
+
report.worst = _worst(report.findings);
|
|
1461
|
+
report.findings = _applyThreshold(report, opts.severity);
|
|
1462
|
+
return report;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
639
1465
|
/**
|
|
640
1466
|
* @primitive pki.lint.rules
|
|
641
1467
|
* @signature pki.lint.rules(profile?) -> [{id, severity, source, citation}]
|
|
@@ -650,7 +1476,10 @@ function certificate(input, opts) {
|
|
|
650
1476
|
* pki.lint.rules("rfc5280").length; // -> a positive count
|
|
651
1477
|
*/
|
|
652
1478
|
function rules(profile) {
|
|
653
|
-
var set = profile == null
|
|
1479
|
+
var set = profile == null
|
|
1480
|
+
? ALL_RULES.concat(ALL_CRL_RULES)
|
|
1481
|
+
: (PROFILES[profile] || CRL_PROFILES[profile]
|
|
1482
|
+
|| (function () { throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\""); })());
|
|
654
1483
|
return set.map(function (r) { return { id: r.id, severity: r.severity, source: r.source, citation: r.citation }; });
|
|
655
1484
|
}
|
|
656
1485
|
|
|
@@ -663,12 +1492,13 @@ function rules(profile) {
|
|
|
663
1492
|
*
|
|
664
1493
|
* List the known lint-profile names.
|
|
665
1494
|
* @example
|
|
666
|
-
* pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls"]
|
|
1495
|
+
* pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "rfc5280-crl"]
|
|
667
1496
|
*/
|
|
668
|
-
function profiles() { return Object.keys(PROFILES); }
|
|
1497
|
+
function profiles() { return Object.keys(PROFILES).concat(Object.keys(CRL_PROFILES)); }
|
|
669
1498
|
|
|
670
1499
|
module.exports = {
|
|
671
1500
|
certificate: certificate,
|
|
1501
|
+
crl: crl,
|
|
672
1502
|
rules: rules,
|
|
673
1503
|
profiles: profiles,
|
|
674
1504
|
};
|
package/lib/ocsp.js
CHANGED
|
@@ -326,10 +326,33 @@ function _buildSingleResponse(r, opts) {
|
|
|
326
326
|
if (r.certID != null) certIdP = Promise.resolve(b.raw(guard.bytes.view(r.certID, OcspError, "ocsp/bad-input", "a response entry's certID")));
|
|
327
327
|
else if (r.cert != null && r.issuer != null) certIdP = _buildCertID(_certOf(r.cert, "a response certificate"), _certOf(r.issuer, "a response issuer"), r.hashAlgorithm || "sha1");
|
|
328
328
|
else return Promise.reject(_err("ocsp/bad-input", "each response entry needs { certID } or { cert, issuer }"));
|
|
329
|
+
/** @internal Every per-entry refusal stays INSIDE this chain. These entries are built through a
|
|
330
|
+
* map into Promise.all, so a synchronous throw here would abort the map before Promise.all
|
|
331
|
+
* attached handlers, and an earlier entry's rejection would be orphaned: the caller catches
|
|
332
|
+
* sign()'s failure and the process still sees an unhandled rejection. */
|
|
329
333
|
return certIdP.then(function (certID) {
|
|
334
|
+
var thisUpdate = _asDate(r.thisUpdate) || new Date();
|
|
335
|
+
var nextUpdate = r.nextUpdate === null ? null : (_asDate(r.nextUpdate) || _defaultNextUpdate());
|
|
336
|
+
/** @internal RFC 6960 sec. 4.2.2.1 makes thisUpdate and nextUpdate a validity interval, and says
|
|
337
|
+
* it corresponds to the {thisUpdate, nextUpdate} interval in CRLs. An interval whose end
|
|
338
|
+
* precedes its start states a status window that closed before it opened, and pki.ocsp.verify
|
|
339
|
+
* reports such a response unknown, so signing one emits what nothing can act on. pki.x509.sign,
|
|
340
|
+
* pki.crl.sign, pki.attrcert.sign and pki.crmf.build each refuse their own reversed window; this
|
|
341
|
+
* is that rule in the one place it was missing. The RESOLVED values are compared rather than the
|
|
342
|
+
* caller's, since a defaulted nextUpdate reaches a relying party the same way a supplied one
|
|
343
|
+
* does. An equal pair is left alone, as it is for a CRL: a zero-length window is not forbidden.
|
|
344
|
+
*
|
|
345
|
+
* The CALLER's instants are compared, at their full precision, which is what the four sibling
|
|
346
|
+
* verbs do. Two instants inside one second encode to the same whole-second GeneralizedTime, so
|
|
347
|
+
* comparing the encoded values instead would accept a descending pair and quietly emit the
|
|
348
|
+
* zero-length window it truncates to. A caller naming a later thisUpdate than nextUpdate has
|
|
349
|
+
* stated a reversed one whatever the seconds round to, and hears so here. */
|
|
350
|
+
if (nextUpdate !== null && guard.time.instantOf(thisUpdate) > guard.time.instantOf(nextUpdate)) {
|
|
351
|
+
throw _err("ocsp/bad-input", "thisUpdate must not be after nextUpdate (RFC 6960 sec. 4.2.2.1)");
|
|
352
|
+
}
|
|
330
353
|
var statusNode = _certStatusNode(r.status, opts);
|
|
331
|
-
var srChildren = [certID, statusNode, b.generalizedTime(
|
|
332
|
-
if (
|
|
354
|
+
var srChildren = [certID, statusNode, b.generalizedTime(thisUpdate)];
|
|
355
|
+
if (nextUpdate !== null) srChildren.push(b.explicit(0, b.generalizedTime(nextUpdate)));
|
|
333
356
|
if (r.singleExtensions && r.singleExtensions.length) srChildren.push(b.explicit(1, b.sequence(r.singleExtensions)));
|
|
334
357
|
return b.sequence(srChildren);
|
|
335
358
|
});
|
package/lib/path-validate.js
CHANGED
|
@@ -1387,8 +1387,6 @@ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
|
|
|
1387
1387
|
var OID_CRL_NUMBER = oid.byName("cRLNumber");
|
|
1388
1388
|
var OID_FRESHEST_CRL = oid.byName("freshestCRL");
|
|
1389
1389
|
|
|
1390
|
-
var IDP_SCHEMA = pkix.issuingDistributionPoint("path/bad-idp");
|
|
1391
|
-
|
|
1392
1390
|
var ALL_REASONS = 0x1FE;
|
|
1393
1391
|
|
|
1394
1392
|
function reasonMaskFromBitString(bs) {
|
|
@@ -1411,37 +1409,23 @@ function interimReasonMask(idpMask, dpMask) {
|
|
|
1411
1409
|
return ALL_REASONS;
|
|
1412
1410
|
}
|
|
1413
1411
|
|
|
1412
|
+
/** @internal The structural read is shared with certificate-revocation-list linting through
|
|
1413
|
+
* schema-pkix, so the two cannot disagree about what a well-formed IssuingDistributionPoint is.
|
|
1414
|
+
* Only the reason-flags MASK is added here, because only path validation compares against it. */
|
|
1414
1415
|
function decodeIdp(ext) {
|
|
1415
|
-
var
|
|
1416
|
-
var
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1427
|
-
catch (_e) { out.malformed = true; return out; }
|
|
1428
|
-
function flag(f) {
|
|
1429
|
-
if (!f.present) return false;
|
|
1430
|
-
var isSet = f.value === true;
|
|
1431
|
-
if (!isSet) out.malformed = true;
|
|
1432
|
-
return isSet;
|
|
1433
|
-
}
|
|
1434
|
-
out.hasDistributionPoint = m.fields.distributionPoint.present;
|
|
1435
|
-
out.onlyUser = flag(m.fields.onlyContainsUserCerts);
|
|
1436
|
-
out.onlyCa = flag(m.fields.onlyContainsCACerts);
|
|
1437
|
-
out.onlySomeReasons = m.fields.onlySomeReasons.present ? true : null;
|
|
1438
|
-
out.onlySomeReasonsMask = null;
|
|
1439
|
-
if (m.fields.onlySomeReasons.present) {
|
|
1440
|
-
out.onlySomeReasonsMask = reasonMaskFromBitString(m.fields.onlySomeReasons.value);
|
|
1416
|
+
var s = pkix.decodeIssuingDistributionPointValue(NS, ext.value, "path/bad-idp");
|
|
1417
|
+
var out = {
|
|
1418
|
+
hasDistributionPoint: s.hasDistributionPoint, distributionPoint: s.distributionPoint,
|
|
1419
|
+
onlyUser: s.onlyUser, onlyCa: s.onlyCa,
|
|
1420
|
+
onlySomeReasons: s.onlySomeReasonsPresent ? true : null,
|
|
1421
|
+
indirect: s.indirect, onlyAttr: s.onlyAttr, malformed: s.malformed,
|
|
1422
|
+
onlySomeReasonsMask: null,
|
|
1423
|
+
};
|
|
1424
|
+
if (out.malformed) return out;
|
|
1425
|
+
if (s.onlySomeReasonsPresent) {
|
|
1426
|
+
out.onlySomeReasonsMask = reasonMaskFromBitString(s.onlySomeReasonsValue);
|
|
1441
1427
|
if (out.onlySomeReasonsMask === null) out.malformed = true;
|
|
1442
1428
|
}
|
|
1443
|
-
out.indirect = flag(m.fields.indirectCRL);
|
|
1444
|
-
out.onlyAttr = flag(m.fields.onlyContainsAttributeCerts);
|
|
1445
1429
|
return out;
|
|
1446
1430
|
}
|
|
1447
1431
|
|
package/lib/schema-pkix.js
CHANGED
|
@@ -643,6 +643,58 @@ function issuingDistributionPoint(code) {
|
|
|
643
643
|
], { assert: "sequence", code: code, what: "IssuingDistributionPoint" });
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
+
/** @internal The STRUCTURAL read of an IssuingDistributionPoint value, shared so that certification
|
|
647
|
+
* path validation and certificate-revocation-list linting cannot disagree about what a well-formed
|
|
648
|
+
* one is. It lives here rather than being exported from a consumer, because this module is not on
|
|
649
|
+
* the public surface and a decoder taking one module's internal shape should not become API.
|
|
650
|
+
* Reports a fault as `malformed` rather than throwing, since both callers survey hostile input.
|
|
651
|
+
* The reason-flags MASK is deliberately absent: it is a path-validation concern, and that caller
|
|
652
|
+
* composes it on top of this. */
|
|
653
|
+
function decodeIssuingDistributionPointValue(ns, bytes, code) {
|
|
654
|
+
var out = { malformed: false, hasDistributionPoint: false, distributionPoint: null,
|
|
655
|
+
onlyUser: false, onlyCa: false, onlySomeReasonsPresent: false, onlySomeReasonsValue: null,
|
|
656
|
+
indirect: false, onlyAttr: false, fields: null };
|
|
657
|
+
var m;
|
|
658
|
+
try {
|
|
659
|
+
m = schema.walk(issuingDistributionPoint(code), asn1.decode(bytes), ns);
|
|
660
|
+
if (m.fields.distributionPoint.present) {
|
|
661
|
+
var wrap = m.fields.distributionPoint.node;
|
|
662
|
+
if (!wrap.children || wrap.children.length !== 1) {
|
|
663
|
+
throw ns.E(code, "IssuingDistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
|
|
664
|
+
}
|
|
665
|
+
out.distributionPoint = distributionPointName(ns, wrap.children[0], code);
|
|
666
|
+
}
|
|
667
|
+
} catch (_e) {
|
|
668
|
+
out.malformed = true;
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
function flag(f) {
|
|
672
|
+
if (!f.present) return false;
|
|
673
|
+
if (f.value !== true) { out.malformed = true; return false; }
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
out.fields = m.fields;
|
|
677
|
+
out.hasDistributionPoint = m.fields.distributionPoint.present;
|
|
678
|
+
out.onlyUser = flag(m.fields.onlyContainsUserCerts);
|
|
679
|
+
out.onlyCa = flag(m.fields.onlyContainsCACerts);
|
|
680
|
+
out.onlySomeReasonsPresent = m.fields.onlySomeReasons.present;
|
|
681
|
+
out.onlySomeReasonsValue = m.fields.onlySomeReasons.present ? m.fields.onlySomeReasons.value : null;
|
|
682
|
+
/** @internal ReasonFlags is a NamedBitList, so DER requires the trailing zero bits be dropped.
|
|
683
|
+
* That is an encoding rule rather than a path-validation policy, so it belongs to the structural
|
|
684
|
+
* read both callers share: a value keeping its trailing zeros is malformed for either of them. */
|
|
685
|
+
if (out.onlySomeReasonsPresent) {
|
|
686
|
+
var bs = out.onlySomeReasonsValue;
|
|
687
|
+
if (!bs || !bs.bytes) out.malformed = true;
|
|
688
|
+
else {
|
|
689
|
+
try { schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (msg) { throw ns.E(code, msg); }); }
|
|
690
|
+
catch (_e) { out.malformed = true; } // allow:swallow-unverified the refusal IS the malformed flag this sets
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
out.indirect = flag(m.fields.indirectCRL);
|
|
694
|
+
out.onlyAttr = flag(m.fields.onlyContainsAttributeCerts);
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
|
|
646
698
|
var _T = asn1.TAGS;
|
|
647
699
|
|
|
648
700
|
function assertPolicyQualifiers(qNode, fail) {
|
|
@@ -1337,6 +1389,7 @@ module.exports = {
|
|
|
1337
1389
|
generalNames: generalNames,
|
|
1338
1390
|
distributionPointName: distributionPointName,
|
|
1339
1391
|
issuingDistributionPoint: issuingDistributionPoint,
|
|
1392
|
+
decodeIssuingDistributionPointValue: decodeIssuingDistributionPointValue,
|
|
1340
1393
|
keyUsageOf: keyUsageOf,
|
|
1341
1394
|
extKeyUsageOf: extKeyUsageOf,
|
|
1342
1395
|
assertsPurpose: assertsPurpose,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:34b0c9f8-e610-4b07-817f-7e260dc2355a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-10T21:28:40.246Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.7.10",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.10",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.7.10",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.7.
|
|
57
|
+
"ref": "@blamejs/pki@0.7.10",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|