@blamejs/pki 0.8.20 → 0.8.22

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 CHANGED
@@ -4,6 +4,40 @@ 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.8.22 — 2026-09-24
8
+
9
+ An algorithm identifier is compared as the bytes a root program fixed, not as the algorithm it names.
10
+
11
+ ### Added
12
+
13
+ - `pki.lint.certificate(der, { profile: "mozilla-root" })` runs the Mozilla Root Store Policy v3.1 profile. Naming it is the only way to reach these rows, and passing no profile leaves every one of them inert.
14
+ - The section 5.1 algorithm encodings, compared as DER: a subjectPublicKeyInfo AlgorithmIdentifier must be one of the four the policy writes out, for RSA and for NIST P-256, P-384 and P-521, and a signature AlgorithmIdentifier one of the ten, covering RSASSA-PKCS1-v1_5, RSASSA-PSS at its matching mask and salt lengths, and ECDSA. Only one of a certificate's two algorithm fields is read, because the parser already refuses a certificate whose outer `signatureAlgorithm` differs from `tbsCertificate.signature`.
15
+ - An RSA modulus must be at least 2048 bits and a whole number of octets, and a public exponent of 1 is reported as the unusable key the policy names. An EdDSA key is admitted only in a certificate carrying `id-kp-emailProtection`, and is exempt from the encoding rows, since the policy gives no EdDSA encoding to hold it to. An EdDSA signature is exempt for the same reason and on the issuer's behalf: a permitted EdDSA intermediate signs with that key, and reporting the certificates it signs would fail them for a key they do not carry.
16
+ - The section 5.1.3 SHA-1 prohibitions, each gated to the date it carries: from 1 July 2022 over an end entity certificate carrying `id-kp-emailProtection`, and from 1 July 2023 over a certificate carrying `id-kp-OCSPSigning` or over an intermediate certificate. That clause names intermediates rather than certificate authorities, so a root's own self-signature is not one of the four things it lists. The clause also allows SHA-1 over an end entity only where a list of conditions all hold, so a certificate carrying no extKeyUsage, or one naming `id-kp-serverAuth` or `anyExtendedKeyUsage`, is reported as outside that allowance. Its remaining conditions name the issuing certificate and the Baseline Requirements scope, which are not in these bytes.
17
+ - The section 5.2 practices a certificate's own bytes answer for: an extKeyUsage extension on every end entity certificate with no `anyExtendedKeyUsage` in it, and a serial number greater than zero. The rest of that sentence asks for at least 64 bits of CSPRNG output, which constrains the draw rather than the integer, and a DER INTEGER drops the leading zero octets a draw produces: eight random octets beginning with zero encode to seven of magnitude, which a conforming certificate does about once in 256. So a short magnitude is reported as a warning and the positive-serial half as an error, each at the strength its own clause supports.
18
+ - The section 5.3 rules for intermediate certificates created after 1 January 2019, gated to that date: an extKeyUsage extension is required, `anyExtendedKeyUsage` is not permitted, and `id-kp-serverAuth` and `id-kp-emailProtection` may not appear together. Being an intermediate is a role in a hierarchy that a certificate does not state, so these three read the one thing the bytes give and pass over a certificate issued in its own name. That is every root, which the clause does not govern, and it is also a rollover intermediate reissued in its own name, which the clause does govern, so the three under-report rather than reporting a root for carrying no extended key usage.
19
+
20
+ ### Changed
21
+
22
+ - `pki.lint.rules()` enumerates the new rows, which stay inert under the default rule set because each one asks whether the caller named the profile.
23
+
24
+ ## v0.8.21 — 2026-09-24
25
+
26
+ A qualified certificate is linted against the statements it carries, not just their syntax.
27
+
28
+ ### Added
29
+
30
+ - `pki.lint.certificate(der, { profile: "etsi-qc" })` runs the ETSI EN 319 412-5 v2.4.1 profile, and the rows reach a certificate unprompted because carrying the extension is what the clause governs.
31
+ - The qcStatements extension must not be marked critical, and a certificate names one and only one of the three purposes the clause defines: electronic signature, electronic seal and web site authentication. The clause limits the certificate, so the purposes are counted across every QcType statement it carries and two statements naming one each break it as surely as one naming two. The syntax admits identifiers beyond those three, so one of them is not a second purpose and is not counted.
32
+ - A QcPDS statement must provide an https URL. The clause asks for one as a minimum, so a plain URL beside a secure one satisfies it. Under the requirements an EU qualified certificate carries, that statement must also name a document in English and must not name more than one per language.
33
+ - The semantics identifiers of ETSI EN 319 412-1 v1.5.1 are read, and a subject attribute they govern is held to the structure they require: a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier. A natural person identifier governs the subject serialNumber and admits PAS, IDC, PNO, TAX and TIN; a legal person identifier governs the subject organizationIdentifier and admits VAT, NTR, PSD and LEI. An LEI value must carry the country code XG. A TAX reference is reported as deprecated, which the standard states at a lower strength than the rest, so the finding is a warning.
34
+ - Either identifier may instead name a locally defined reference, two characters and a colon, and one of those requires a nameRegistrationAuthorities element carrying a uniformResourceIdentifier, since that is what makes the local scheme resolvable.
35
+
36
+ ### Changed
37
+
38
+ - The four semantics identifiers of ETSI EN 319 412-1 section 5.1.2 resolve through `pki.oid`, and a decoded SemanticsInformation carries `semanticsIdentifierName` beside the identifier, the way a QcType carries `typeNames` and a QcIdentMethod `methodNames`. It is null for an identifier the registry does not name.
39
+ - Every statement of a kind is graded, not the first of that kind. RFC 3739 section 3.2.6 puts no cardinality on QCStatements, so a certificate may carry two disclosure statements or two semantics statements, and the order they sit in the extension decides nothing.
40
+
7
41
  ## v0.8.20 — 2026-09-24
8
42
 
9
43
  An extended validation code signing certificate is held to the subject its own section requires.
package/README.md CHANGED
@@ -290,7 +290,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
290
290
  | `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` |
291
291
  | `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` |
292
292
  | `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` |
293
- | `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, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), 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), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `rules`, `profiles` |
293
+ | `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, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), 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), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `rules`, `profiles` |
294
294
  | `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
295
295
  | `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 |
296
296
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert\|crl\|ocsp-response>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
package/lib/lint.js CHANGED
@@ -272,6 +272,17 @@ function _rsaModulusBits(spki) {
272
272
  return mod.toString(2).length;
273
273
  } catch (_e) { return null; }
274
274
  }
275
+ /** @internal The RSA public exponent, read from the same RSAPublicKey the modulus comes from, or
276
+ * null where the key does not decode as one. */
277
+ function _rsaExponent(spki) {
278
+ try {
279
+ var pk = spki.publicKey && (spki.publicKey.bytes || spki.publicKey);
280
+ if (!Buffer.isBuffer(pk)) return null;
281
+ var seq = asn1.decode(pk);
282
+ if (!seq.children || seq.children.length !== 2) return null;
283
+ return asn1.read.integer(seq.children[1]);
284
+ } catch (_e) { return null; }
285
+ }
275
286
  var RSA_KEY_ALGS = Object.assign(Object.create(null), { rsaEncryption: 1, rsassaPss: 1, rsaesOaep: 1 });
276
287
  var APPROVED_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
277
288
  function _ecCurveName(spki) {
@@ -1602,6 +1613,644 @@ var CABF_SMIME_RULES = [
1602
1613
  },
1603
1614
  ];
1604
1615
 
1616
+ /** @internal Mozilla Root Store Policy v3.1 sec. 5.1.1 and 5.1.2 give the permitted
1617
+ * AlgorithmIdentifier encodings as hex-encoded BYTES, so these are compared as DER rather than as
1618
+ * a decoded triple: the point of the clause is the encoding, and two AlgorithmIdentifiers naming
1619
+ * the same algorithm can differ in whether the parameters are an explicit NULL or absent. */
1620
+ var MOZILLA_SPKI_ALGORITHMS = [
1621
+ "300d06092a864886f70d0101010500",
1622
+ "301306072a8648ce3d020106082a8648ce3d030107",
1623
+ "301006072a8648ce3d020106052b81040022",
1624
+ "301006072a8648ce3d020106052b81040023",
1625
+ ];
1626
+ var MOZILLA_SIGNATURE_ALGORITHMS = [
1627
+ "300d06092a864886f70d0101050500",
1628
+ "300d06092a864886f70d01010b0500",
1629
+ "300d06092a864886f70d01010c0500",
1630
+ "300d06092a864886f70d01010d0500",
1631
+ "304106092a864886f70d01010a3034a00f300d060960864801650304020105" +
1632
+ "00a11c301a06092a864886f70d010108300d06096086480165030402010500a203020120",
1633
+ "304106092a864886f70d01010a3034a00f300d060960864801650304020205" +
1634
+ "00a11c301a06092a864886f70d010108300d06096086480165030402020500a203020130",
1635
+ "304106092a864886f70d01010a3034a00f300d060960864801650304020305" +
1636
+ "00a11c301a06092a864886f70d010108300d06096086480165030402030500a203020140",
1637
+ "300a06082a8648ce3d040302",
1638
+ "300a06082a8648ce3d040303",
1639
+ "300a06082a8648ce3d040304",
1640
+ ];
1641
+ /** @internal The RSASSA-PKCS1-v1_5 with SHA-1 encoding, which sec. 5.1.1 admits and sec. 5.1.3
1642
+ * then restricts by date and by key purpose. */
1643
+ var MOZILLA_SHA1_RSA = "300d06092a864886f70d0101050500";
1644
+ var MOZILLA_SHA1_ECDSA = "300906072a8648ce3d0401";
1645
+ var MOZILLA_SHA1_EMAIL_FROM = new Date("2022-07-01T00:00:00Z");
1646
+ var MOZILLA_SHA1_OCSP_CA_FROM = new Date("2023-07-01T00:00:00Z");
1647
+ /** @internal "Intermediate certificates created after January 1, 2019" (sec. 5.3). */
1648
+ var MOZILLA_INTERMEDIATE_EKU_FROM = new Date("2019-01-01T00:00:00Z");
1649
+ /** @internal "at least 64 bits of output from a CSPRNG" in the serial (sec. 5.2), of which the
1650
+ * eight octets are what a certificate's bytes can answer for. */
1651
+ var MOZILLA_SERIAL_MIN_OCTETS = 8;
1652
+ var MOZILLA_RSA_MIN_BITS = 2048;
1653
+
1654
+ /** @internal The SubjectPublicKeyInfo's AlgorithmIdentifier as DER hex. The parsed record carries
1655
+ * the decoded triple, and this clause is about the encoding, so the bytes are read back out. */
1656
+ function _mozillaSpkiAlgorithmHex(cert) {
1657
+ try {
1658
+ var spki = cert.subjectPublicKeyInfo;
1659
+ if (!spki || !Buffer.isBuffer(spki.bytes)) return null;
1660
+ var node = asn1.decode(spki.bytes);
1661
+ if (!node.children || !node.children.length) return null;
1662
+ return node.children[0].bytes.toString("hex");
1663
+ } catch (_e) { return null; }
1664
+ }
1665
+ /** @internal The signature AlgorithmIdentifier as DER hex, read from the TBSCertificate. Only one
1666
+ * of the two algorithm fields needs reading: schema-x509 refuses a certificate whose outer
1667
+ * signatureAlgorithm differs byte-for-byte from tbsCertificate.signature, so by the time a rule
1668
+ * sees one they cannot disagree. */
1669
+ function _mozillaSignatureAlgorithmHex(cert) {
1670
+ // allow:swallow-unverified the ingest door has already produced a parsed certificate, so this reads bytes the parser accepted and a fault answers null
1671
+ try {
1672
+ var kids = asn1.decode(cert.tbsBytes).children;
1673
+ if (!kids || kids.length < 2) return null;
1674
+ var idx = (kids[0].tagClass === "context" && kids[0].tagNumber === 0) ? 2 : 1;
1675
+ return kids[idx] ? kids[idx].bytes.toString("hex") : null;
1676
+ } catch (_e) { return null; }
1677
+ }
1678
+ function _mozillaIsEdDsa(cert) {
1679
+ var spki = cert.subjectPublicKeyInfo;
1680
+ var name = spki && spki.algorithm && spki.algorithm.name;
1681
+ return name === "Ed25519" || name === "Ed448";
1682
+ }
1683
+ function _mozillaIsSha1Signature(cert) {
1684
+ var hex = _mozillaSignatureAlgorithmHex(cert);
1685
+ return hex === MOZILLA_SHA1_RSA || hex === MOZILLA_SHA1_ECDSA;
1686
+ }
1687
+ /** @internal The profile is NAMED, never detected. Every other profile here is selected by
1688
+ * something the certificate declares; a root program's policy applies because the certificate
1689
+ * chains to a root in that store, which the certificate does not state and a single-certificate
1690
+ * lint cannot know. */
1691
+ function _isMozilla(cert, ctx) { return ctx.profile === "mozilla-root"; }
1692
+ /** @internal An INTERMEDIATE, which is what sec. 5.3 governs. Being an intermediate is a role in a
1693
+ * hierarchy and a certificate does not state it, so this reads the one thing the bytes give: a
1694
+ * certificate issued in its own name is passed over. That is every root, which the clause does not
1695
+ * govern, and it is also a self-issued rollover intermediate, which the clause DOES govern, so the
1696
+ * rows under-report rather than reporting a root for carrying no extended key usage. The
1697
+ * cross-certificate the clause exempts need not carry matching names, which is why each message
1698
+ * names that exemption for the operator to apply. */
1699
+ function _isMozillaIntermediate(cert, ctx) {
1700
+ return _isMozilla(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
1701
+ }
1702
+ function _isMozillaEndEntity(cert, ctx) { return _isMozilla(cert, ctx) && !ctx.isCaCert; }
1703
+
1704
+ /** @internal The Mozilla Root Store Policy v3.1 profile. */
1705
+ var MOZILLA_RULES = [
1706
+ {
1707
+ id: "lint/mozilla-root/spki-algorithm-encoding", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.1 and 5.1.2",
1708
+ /** @internal An EdDSA key is exempt here and answered by its own row: sec. 5.1 admits one in a
1709
+ * certificate carrying id-kp-emailProtection and gives no EdDSA encoding, so holding it to
1710
+ * this list would report every conforming certificate of that kind. */
1711
+ message: "the subjectPublicKeyInfo AlgorithmIdentifier is not one of the encodings this policy names",
1712
+ appliesTo: function (cert, ctx) { return _isMozilla(cert, ctx) && !_mozillaIsEdDsa(cert); },
1713
+ check: function (cert) {
1714
+ var hex = _mozillaSpkiAlgorithmHex(cert);
1715
+ return MOZILLA_SPKI_ALGORITHMS.indexOf(hex) === -1 ? { context: { algorithmIdentifier: hex } } : null;
1716
+ },
1717
+ },
1718
+ {
1719
+ id: "lint/mozilla-root/eddsa-without-email-protection", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1",
1720
+ /** @internal "EdDSA keys MAY be included in certificates that chain to a root certificate in
1721
+ * our root store if the certificate contains id-kp-emailProtection in the EKU extension.
1722
+ * Otherwise, EdDSA keys MUST NOT be included." */
1723
+ message: "an EdDSA key needs id-kp-emailProtection in the extKeyUsage extension",
1724
+ appliesTo: function (cert, ctx) { return _isMozilla(cert, ctx) && _mozillaIsEdDsa(cert); },
1725
+ check: function (cert, ctx) {
1726
+ var d = ctx.decode("extKeyUsage");
1727
+ if (!d || !Array.isArray(d.value)) return true;
1728
+ return d.value.indexOf(oid.byName("emailProtection")) === -1 ? true : null;
1729
+ },
1730
+ },
1731
+ {
1732
+ id: "lint/mozilla-root/signature-algorithm-encoding", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.1 and 5.1.2",
1733
+ /** @internal Membership of the ten, and nothing more: sec. 5.1.2 binds a curve to a digest for
1734
+ * the SIGNING key, which is the issuer's and is not in this certificate. Matching
1735
+ * distinguished names would not prove the subject key made the signature, which is the
1736
+ * cross-certificate shape sec. 5.2 calls out.
1737
+ * An EdDSA signature is outside these clauses rather than in breach of them: sec. 5.1.1 and
1738
+ * sec. 5.1.2 fix the encodings for a signing RSA key and a signing ECDSA key, and sec. 5.1
1739
+ * admits an EdDSA key in a certificate carrying id-kp-emailProtection, which can be an
1740
+ * intermediate and sign with it. Reporting the certificates it signs would fail them for the
1741
+ * issuer's key. */
1742
+ message: "the signature AlgorithmIdentifier is not one of the encodings this policy names",
1743
+ appliesTo: function (cert, ctx) {
1744
+ var name = cert.signatureAlgorithm && cert.signatureAlgorithm.name;
1745
+ return _isMozilla(cert, ctx) && name !== "Ed25519" && name !== "Ed448";
1746
+ },
1747
+ check: function (cert) {
1748
+ var hex = _mozillaSignatureAlgorithmHex(cert);
1749
+ return MOZILLA_SIGNATURE_ALGORITHMS.indexOf(hex) === -1 ? { context: { algorithmIdentifier: hex } } : null;
1750
+ },
1751
+ },
1752
+ {
1753
+ id: "lint/mozilla-root/rsa-modulus-invalid", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1",
1754
+ /** @internal "RSA keys whose modulus size in bits is divisible by 8, and is at least 2048
1755
+ * bits". The AlgorithmIdentifier row above cannot answer for this, since the modulus lives in
1756
+ * the subjectPublicKey rather than in the algorithm field. */
1757
+ message: "an RSA modulus must be at least 2048 bits and a whole number of octets",
1758
+ appliesTo: function (cert, ctx) {
1759
+ var spki = cert.subjectPublicKeyInfo;
1760
+ return _isMozilla(cert, ctx) && !!(spki && spki.algorithm && RSA_KEY_ALGS[spki.algorithm.name]);
1761
+ },
1762
+ check: function (cert) {
1763
+ var bits = _rsaModulusBits(cert.subjectPublicKeyInfo);
1764
+ if (bits === null) return { context: { rsaBits: null } };
1765
+ return (bits < MOZILLA_RSA_MIN_BITS || bits % 8 !== 0) ? { context: { rsaBits: bits } } : null;
1766
+ },
1767
+ },
1768
+ {
1769
+ id: "lint/mozilla-root/rsa-exponent-one", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
1770
+ /** @internal "invalid public keys (e.g., RSA certificates with public exponent equal to 1)",
1771
+ * the one example the clause gives and the one a certificate's bytes settle. */
1772
+ message: "an RSA public exponent of 1 is not a usable key",
1773
+ appliesTo: function (cert, ctx) {
1774
+ var spki = cert.subjectPublicKeyInfo;
1775
+ return _isMozilla(cert, ctx) && !!(spki && spki.algorithm && RSA_KEY_ALGS[spki.algorithm.name]);
1776
+ },
1777
+ check: function (cert) {
1778
+ var e = _rsaExponent(cert.subjectPublicKeyInfo);
1779
+ return e !== null && e <= 1n ? { context: { exponent: e.toString() } } : null;
1780
+ },
1781
+ },
1782
+ {
1783
+ id: "lint/mozilla-root/serial-number-not-positive", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
1784
+ /** @internal "all new certificates MUST have a serial number greater than zero", the half of
1785
+ * the sentence a certificate's bytes settle outright. */
1786
+ message: "a serial number must be greater than zero",
1787
+ appliesTo: _isMozilla,
1788
+ check: function (cert) {
1789
+ if (typeof cert.serialNumber !== "bigint") return null;
1790
+ return cert.serialNumber > 0n ? null : { context: { serialNumber: cert.serialNumber.toString() } };
1791
+ },
1792
+ },
1793
+ {
1794
+ id: "lint/mozilla-root/serial-number-short", severity: "warn", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
1795
+ /** @internal "containing at least 64 bits of output from a CSPRNG" constrains the DRAW, and a
1796
+ * DER INTEGER does not carry the leading zero octets a draw can produce: eight random octets
1797
+ * whose first is zero encode to seven of magnitude, which happens to a conforming certificate
1798
+ * about once in 256. So the magnitude is evidence rather than a verdict and the row is graded
1799
+ * below the sentence's other half, which is its own row above. */
1800
+ message: "a serial number carries fewer than eight octets of magnitude, which 64 bits of CSPRNG output rarely produces",
1801
+ appliesTo: _isMozilla,
1802
+ check: function (cert) {
1803
+ var octets = _serialOctets(cert);
1804
+ return octets < MOZILLA_SERIAL_MIN_OCTETS ? { context: { octets: octets } } : null;
1805
+ },
1806
+ },
1807
+ {
1808
+ id: "lint/mozilla-root/end-entity-eku-missing", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
1809
+ message: "an end entity certificate must carry an extKeyUsage extension",
1810
+ appliesTo: _isMozillaEndEntity,
1811
+ check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
1812
+ },
1813
+ {
1814
+ id: "lint/mozilla-root/end-entity-eku-any", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
1815
+ message: "an end entity certificate's extKeyUsage must not name anyExtendedKeyUsage",
1816
+ appliesTo: _isMozillaEndEntity,
1817
+ check: function (cert, ctx) {
1818
+ var d = ctx.decode("extKeyUsage");
1819
+ if (!d || !Array.isArray(d.value)) return null;
1820
+ return d.value.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
1821
+ },
1822
+ },
1823
+ {
1824
+ id: "lint/mozilla-root/sha1-email-protection", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.3",
1825
+ /** @internal "Effective July 1, 2022, CAs SHALL NOT sign SHA-1 hashes over end entity
1826
+ * certificates with an EKU extension containing the id-kp-emailProtection key purpose." */
1827
+ message: "a SHA-1 signature over an end entity certificate carrying id-kp-emailProtection",
1828
+ appliesTo: _isMozillaEndEntity,
1829
+ effectiveDate: MOZILLA_SHA1_EMAIL_FROM,
1830
+ check: function (cert, ctx) {
1831
+ if (!_mozillaIsSha1Signature(cert)) return null;
1832
+ var d = ctx.decode("extKeyUsage");
1833
+ if (!d || !Array.isArray(d.value)) return null;
1834
+ return d.value.indexOf(oid.byName("emailProtection")) !== -1 ? true : null;
1835
+ },
1836
+ },
1837
+ {
1838
+ id: "lint/mozilla-root/sha1-end-entity-outside-allowance", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.3",
1839
+ /** @internal SHA-1 over an end entity is permitted "only if all the following are true", and
1840
+ * the conditions this certificate answers for are that it "contains an EKU extension that does
1841
+ * not contain the id-kp-serverAuth, id-kp-emailProtection, or anyExtendedKeyUsage key
1842
+ * purposes". The remaining conditions name the issuing certificate and the BR scope, which are
1843
+ * not in these bytes. The id-kp-emailProtection arm has its own dated row, so this one passes
1844
+ * over it and a certificate draws one finding rather than two. */
1845
+ message: "a SHA-1 signature over an end entity certificate outside the allowance this clause gives",
1846
+ appliesTo: _isMozillaEndEntity,
1847
+ check: function (cert, ctx) {
1848
+ if (!_mozillaIsSha1Signature(cert)) return null;
1849
+ var d = ctx.decode("extKeyUsage");
1850
+ if (!d || !Array.isArray(d.value)) return { context: { reason: "no extKeyUsage extension" } };
1851
+ var out = [];
1852
+ ["serverAuth", "anyExtendedKeyUsage"].forEach(function (name) {
1853
+ if (d.value.indexOf(oid.byName(name)) !== -1) out.push({ context: { purpose: name } });
1854
+ });
1855
+ return out.length ? out : null;
1856
+ },
1857
+ },
1858
+ {
1859
+ id: "lint/mozilla-root/sha1-ocsp-or-ca", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.3",
1860
+ /** @internal "Effective July 1, 2023, CAs SHALL NOT sign SHA-1 hashes over: certificates with
1861
+ * an EKU extension containing the id-kp-ocspSigning key purpose; intermediate certificates
1862
+ * that chain up to roots in Mozilla's program; OCSP responses; or CRLs." The two the
1863
+ * certificate answers for are the key purpose and being an INTERMEDIATE. The clause names
1864
+ * intermediate certificates rather than CA certificates, so a root's own self-signature is not
1865
+ * one of the four things it lists, and the intermediate reading is the one the sec. 5.3 rows
1866
+ * use with the limit it carries. */
1867
+ message: "a SHA-1 signature over an OCSP signing certificate or an intermediate certificate",
1868
+ appliesTo: _isMozilla,
1869
+ effectiveDate: MOZILLA_SHA1_OCSP_CA_FROM,
1870
+ check: function (cert, ctx) {
1871
+ if (!_mozillaIsSha1Signature(cert)) return null;
1872
+ if (_isMozillaIntermediate(cert, ctx)) return { context: { reason: "intermediate certificate" } };
1873
+ var d = ctx.decode("extKeyUsage");
1874
+ if (!d || !Array.isArray(d.value)) return null;
1875
+ return d.value.indexOf(oid.byName("ocspSigning")) !== -1
1876
+ ? { context: { reason: "id-kp-OCSPSigning" } } : null;
1877
+ },
1878
+ },
1879
+ {
1880
+ id: "lint/mozilla-root/intermediate-eku-missing", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.3",
1881
+ /** @internal The clause exempts "cross-certificates that share a private key with a
1882
+ * corresponding root certificate", which names a role in a hierarchy that a certificate does
1883
+ * not state, so the message carries the exemption for the operator to apply. */
1884
+ message: "an intermediate certificate must carry an extKeyUsage extension, unless it is a cross-certificate sharing a private key with a root",
1885
+ appliesTo: _isMozillaIntermediate,
1886
+ effectiveDate: MOZILLA_INTERMEDIATE_EKU_FROM,
1887
+ check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
1888
+ },
1889
+ {
1890
+ id: "lint/mozilla-root/intermediate-eku-any", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.3",
1891
+ message: "an intermediate certificate's extKeyUsage must not name anyExtendedKeyUsage, unless it is a cross-certificate sharing a private key with a root",
1892
+ appliesTo: _isMozillaIntermediate,
1893
+ effectiveDate: MOZILLA_INTERMEDIATE_EKU_FROM,
1894
+ check: function (cert, ctx) {
1895
+ var d = ctx.decode("extKeyUsage");
1896
+ if (!d || !Array.isArray(d.value)) return null;
1897
+ return d.value.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
1898
+ },
1899
+ },
1900
+ {
1901
+ id: "lint/mozilla-root/intermediate-eku-server-and-email", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.3",
1902
+ message: "an intermediate certificate must not name both id-kp-serverAuth and id-kp-emailProtection, unless it is a cross-certificate sharing a private key with a root",
1903
+ appliesTo: _isMozillaIntermediate,
1904
+ effectiveDate: MOZILLA_INTERMEDIATE_EKU_FROM,
1905
+ check: function (cert, ctx) {
1906
+ var d = ctx.decode("extKeyUsage");
1907
+ if (!d || !Array.isArray(d.value)) return null;
1908
+ var server = d.value.indexOf(oid.byName("serverAuth")) !== -1;
1909
+ var email = d.value.indexOf(oid.byName("emailProtection")) !== -1;
1910
+ return (server && email) ? true : null;
1911
+ },
1912
+ },
1913
+ ];
1914
+
1915
+ /** @internal The three purposes ETSI EN 319 412-5 clause 4.2.3 names, of which a QcType declares
1916
+ * "one and only one". The ASN.1 carries an extension marker, so an identifier outside the three
1917
+ * is admitted and is not one of the purposes being counted. */
1918
+ var QC_TYPE_PURPOSES = ["qctEsign", "qctEseal", "qctWeb"].map(function (n) { return oid.byName(n); });
1919
+ /** @internal The identity type references EN 319 412-1 sec. 5.1.3 and 5.1.4 define, each three
1920
+ * characters. A reference may instead be the locally defined form, two characters and a colon,
1921
+ * which is three characters as well, so the two forms differ in which references are admitted and
1922
+ * not in where the country code begins. The clause's own examples read "PASSK-P3000180" and
1923
+ * "EI:SE-200007292386". */
1924
+ var QC_NATURAL_TYPES = ["PAS", "IDC", "PNO", "TAX", "TIN"];
1925
+ var QC_LEGAL_TYPES = ["VAT", "NTR", "PSD", "LEI"];
1926
+ /** @internal "The value 'TAX' is deprecated. The value 'TIN' should be used instead."
1927
+ * (NAT-5.1.3-04), a SHOULD, so the row that reports it is graded below the structural ones. */
1928
+ var QC_DEPRECATED_NATURAL_TYPE = "TAX";
1929
+ /** @internal "LEI ... The 2 character ISO 3166-1 country code shall be set to 'XG'"
1930
+ * (LEG-5.1.4-03). */
1931
+ var QC_LEI_COUNTRY = "XG";
1932
+
1933
+ /** @internal An identity type reference as EN 319 412-1 writes it, with the rest of the value, or
1934
+ * null where the value takes neither form. The locally defined form is two characters and a
1935
+ * colon; every defined reference is three characters. Both are followed by a two-character
1936
+ * country code and a hyphen-minus. */
1937
+ function _qcIdentityParts(value) {
1938
+ if (typeof value !== "string") return null;
1939
+ var localForm = value.length > 2 && value.charCodeAt(2) === 58;
1940
+ var ref = value.slice(0, 3);
1941
+ var rest = value.slice(3);
1942
+ if (rest.length < 3 || rest.charCodeAt(2) !== 45) return null;
1943
+ var country = rest.slice(0, 2);
1944
+ for (var i = 0; i < 2; i++) {
1945
+ var c = country.charCodeAt(i);
1946
+ if ((c < 65 || c > 90) && (c < 97 || c > 122)) return null;
1947
+ }
1948
+ if (rest.length <= 3) return null;
1949
+ return { reference: ref, local: localForm, country: country, identifier: rest.slice(3) };
1950
+ }
1951
+ /** @internal Whether an identity type reference is one the clause defines, in either form. */
1952
+ function _qcKnownReference(parts, defined) {
1953
+ if (parts === null) return false;
1954
+ if (parts.local) return true;
1955
+ return defined.indexOf(parts.reference) !== -1;
1956
+ }
1957
+ /** @internal EVERY SemanticsInformation the certificate carries that names an identifier. RFC 3739
1958
+ * sec. 3.2.6 puts no cardinality on QCStatements, so a certificate may carry more than one, and
1959
+ * reading only the first would let their order in the extension suppress a rule. Both statement
1960
+ * versions carry the same syntax. */
1961
+ function _qcSemanticsAll(ctx) {
1962
+ var out = [];
1963
+ var d = ctx.decode("qcStatements");
1964
+ if (!d || !Array.isArray(d.value)) return out;
1965
+ var v1 = oid.byName("qcsPkixQCSyntaxV1"), v2 = oid.byName("qcsPkixQCSyntaxV2");
1966
+ for (var i = 0; i < d.value.length; i++) {
1967
+ var st = d.value[i];
1968
+ if (st.statementId !== v1 && st.statementId !== v2) continue;
1969
+ if (st.info && st.info.semanticsIdentifier) out.push(st.info);
1970
+ }
1971
+ return out;
1972
+ }
1973
+ /** @internal The decoded info of EVERY statement of one name, for the same reason. */
1974
+ function _qcStatementInfos(ctx, name) {
1975
+ var out = [];
1976
+ var d = ctx.decode("qcStatements");
1977
+ if (!d || !Array.isArray(d.value)) return out;
1978
+ var dotted = oid.byName(name);
1979
+ for (var i = 0; i < d.value.length; i++) {
1980
+ if (d.value[i].statementId === dotted) out.push(d.value[i].info === null ? {} : d.value[i].info);
1981
+ }
1982
+ return out;
1983
+ }
1984
+ /** @internal The SemanticsInformation where it names one of the two identifiers EN 319 412-1
1985
+ * defines, or null. The requirements of sec. 5.1.3 and 5.1.4 each sit under their own identifier,
1986
+ * so a certificate naming a private one is held to neither. */
1987
+ /** @internal Whether ANY qcsPkixQCSyntax statement names a given semantics identifier. */
1988
+ function _qcNamesSemantics(ctx, name) {
1989
+ var dotted = oid.byName(name);
1990
+ return _qcSemanticsAll(ctx).some(function (info) { return info.semanticsIdentifier === dotted; });
1991
+ }
1992
+ function _qcEtsiSemanticsAll(ctx) {
1993
+ var nat = oid.byName("semanticsId-Natural"), leg = oid.byName("semanticsId-Legal");
1994
+ return _qcSemanticsAll(ctx).filter(function (info) {
1995
+ return info.semanticsIdentifier === nat || info.semanticsIdentifier === leg;
1996
+ });
1997
+ }
1998
+ /** @internal Whether a subject attribute names the locally defined identity type reference form,
1999
+ * which is what conditions the name registration authority requirements. */
2000
+ function _qcHasLocalReference(cert, attr) {
2001
+ var values = _subjectValuesOf(cert, attr);
2002
+ for (var i = 0; i < values.length; i++) {
2003
+ var parts = _qcIdentityParts(values[i]);
2004
+ if (parts !== null && parts.local) return true;
2005
+ }
2006
+ return false;
2007
+ }
2008
+ function _qcHasStatement(ctx, name) {
2009
+ var d = ctx.decode("qcStatements");
2010
+ if (!d || !Array.isArray(d.value)) return false;
2011
+ var dotted = oid.byName(name);
2012
+ for (var i = 0; i < d.value.length; i++) {
2013
+ if (d.value[i].statementId === dotted) return true;
2014
+ }
2015
+ return false;
2016
+ }
2017
+ /** @internal A certificate carrying the extension at all, which is what clause 4 governs. */
2018
+ function _isQc(cert, ctx) { return !!ctx.raw("qcStatements"); }
2019
+ /** @internal An EU qualified certificate, which is what clause 5 governs. Table 1A reads it off
2020
+ * the bytes: QcCompliance says the certificate is qualified, and QcCClegislation names the
2021
+ * country or countries whose framework it is qualified under, so with that statement present the
2022
+ * certificate claims a framework other than the Regulation and clause 5 does not bind it. */
2023
+ function _isEuQc(cert, ctx) {
2024
+ return _isQc(cert, ctx) && _qcHasStatement(ctx, "qcCompliance") &&
2025
+ !_qcHasStatement(ctx, "qcCClegislation");
2026
+ }
2027
+ /** @internal Whether the name registration authorities carry a uniformResourceIdentifier, which
2028
+ * NAT-5.1.3-06 and LEG-5.1.4-05 both require of them. A decoded GeneralName names its form in
2029
+ * `tagNumber`; the `tag` an accessDescription carries is that record rebuilt by its own decoder,
2030
+ * and these are the plain form. */
2031
+ function _qcHasUriAuthority(info) {
2032
+ var list = info && info.nameRegistrationAuthorities;
2033
+ if (!Array.isArray(list)) return false;
2034
+ for (var i = 0; i < list.length; i++) {
2035
+ if (list[i] && list[i].tagNumber === 6) return true;
2036
+ }
2037
+ return false;
2038
+ }
2039
+
2040
+ /** @internal The ETSI qualified-certificate profile, from EN 319 412-5 V2.4.1 and the semantics
2041
+ * identifiers of EN 319 412-1 V1.5.1. Clause 4 governs any certificate carrying the extension and
2042
+ * clause 5 an EU qualified one, so the two scopes are separate predicates. */
2043
+ var ETSI_QC_RULES = [
2044
+ {
2045
+ id: "lint/etsi-qc/qc-statements-critical", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-5 QCS-4.1-02",
2046
+ message: "the qcStatements extension must not be marked critical",
2047
+ appliesTo: _isQc,
2048
+ check: function (cert, ctx) {
2049
+ var e = ctx.raw("qcStatements");
2050
+ return (e && e.critical === true) ? true : null;
2051
+ },
2052
+ },
2053
+ {
2054
+ id: "lint/etsi-qc/qc-type-multiple-purposes", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-5 4.2.3",
2055
+ /** @internal "declares that a CERTIFICATE is issued as one and only one of the purposes of
2056
+ * electronic signature, electronic seal or web site authentication", so the limit is on the
2057
+ * certificate and the purposes are counted across every QcType statement it carries: two
2058
+ * statements naming one purpose each break the same rule as one naming two. The QcType ASN.1
2059
+ * carries an extension marker, so an identifier outside the three is admitted and is not one
2060
+ * of the purposes being counted. */
2061
+ message: "the certificate names more than one of the three purposes this clause defines",
2062
+ appliesTo: _isQc,
2063
+ check: function (cert, ctx) {
2064
+ var named = [];
2065
+ _qcStatementInfos(ctx, "qcType").forEach(function (info) {
2066
+ if (!Array.isArray(info.types)) return;
2067
+ info.types.forEach(function (d) {
2068
+ if (QC_TYPE_PURPOSES.indexOf(d) !== -1 && named.indexOf(d) === -1) named.push(d);
2069
+ });
2070
+ });
2071
+ return named.length > 1 ? { context: { purposes: named } } : null;
2072
+ },
2073
+ },
2074
+ {
2075
+ id: "lint/etsi-qc/qc-pds-no-https-url", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-5 QCS-4.3.4-03",
2076
+ /** @internal "As a minimum, a URL to a PDS provided in this statement shall use the https
2077
+ * scheme", which is the at-least-one reading: reporting each URL that is not https would fail
2078
+ * a statement the clause admits. */
2079
+ message: "a QcPDS statement provides no https URL",
2080
+ appliesTo: function (cert, ctx) { return _isQc(cert, ctx) && _qcHasStatement(ctx, "qcPDS"); },
2081
+ check: function (cert, ctx) {
2082
+ var out = [];
2083
+ _qcStatementInfos(ctx, "qcPDS").forEach(function (info) {
2084
+ if (!Array.isArray(info.locations)) return;
2085
+ var https = info.locations.filter(function (l) {
2086
+ return pkiBuild.httpUrlScheme(l && l.url) === "https";
2087
+ });
2088
+ if (!https.length) out.push({ context: { locations: info.locations.length } });
2089
+ });
2090
+ return out.length ? out : null;
2091
+ },
2092
+ },
2093
+ {
2094
+ id: "lint/etsi-qc/qc-pds-no-english", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-5 table 2",
2095
+ /** @internal "it shall provide at least one URL to a PDS in English", an additional
2096
+ * requirement Table 2 states on the QcPDS statement, so it binds an EU qualified certificate
2097
+ * and no other. The language is an ISO 639-1 code (QCS-4.3.4-01), where English is "en". */
2098
+ message: "an EU qualified certificate's QcPDS statement names no document in English",
2099
+ appliesTo: function (cert, ctx) { return _isEuQc(cert, ctx) && _qcHasStatement(ctx, "qcPDS"); },
2100
+ check: function (cert, ctx) {
2101
+ var out = [];
2102
+ _qcStatementInfos(ctx, "qcPDS").forEach(function (info) {
2103
+ if (!Array.isArray(info.locations)) return;
2104
+ var english = info.locations.filter(function (l) {
2105
+ return typeof l.language === "string" && l.language.toLowerCase() === "en";
2106
+ });
2107
+ if (!english.length) {
2108
+ out.push({ context: { languages: info.locations.map(function (l) { return l.language; }) } });
2109
+ }
2110
+ });
2111
+ return out.length ? out : null;
2112
+ },
2113
+ },
2114
+ {
2115
+ id: "lint/etsi-qc/qc-pds-duplicate-language", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-5 table 2",
2116
+ /** @internal "it shall not reference more than one PDS per language", the other additional
2117
+ * requirement Table 2 states on the same statement. */
2118
+ message: "an EU qualified certificate's QcPDS statement names more than one document in a language",
2119
+ appliesTo: function (cert, ctx) { return _isEuQc(cert, ctx) && _qcHasStatement(ctx, "qcPDS"); },
2120
+ check: function (cert, ctx) {
2121
+ var out = [];
2122
+ _qcStatementInfos(ctx, "qcPDS").forEach(function (info) {
2123
+ if (!Array.isArray(info.locations)) return;
2124
+ var seen = Object.create(null);
2125
+ info.locations.forEach(function (l) {
2126
+ if (typeof l.language !== "string") return;
2127
+ var key = l.language.toLowerCase();
2128
+ if (seen[key]) { out.push({ context: { language: l.language } }); return; }
2129
+ seen[key] = true;
2130
+ });
2131
+ });
2132
+ return out.length ? out : null;
2133
+ },
2134
+ },
2135
+ {
2136
+ id: "lint/etsi-qc/qc-natural-serial-number-structure", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-1 NAT-5.1.3-02 and NAT-5.1.3-03",
2137
+ /** @internal "When the natural person semantics identifier is included, any present
2138
+ * serialNumber attribute in the subject field shall contain information using the following
2139
+ * structure": a three character identity type reference, a two character country code, a
2140
+ * hyphen-minus, and an identifier. The reference is one the clause defines or the locally
2141
+ * defined form of two characters and a colon. */
2142
+ message: "a subject serialNumber does not take the structure the natural person semantics identifier requires",
2143
+ appliesTo: function (cert, ctx) {
2144
+ return _isQc(cert, ctx) && _qcNamesSemantics(ctx, "semanticsId-Natural") &&
2145
+ _subjectValuesOf(cert, "serialNumber").length > 0;
2146
+ },
2147
+ check: function (cert) {
2148
+ var out = [];
2149
+ _subjectValuesOf(cert, "serialNumber").forEach(function (v) {
2150
+ var parts = _qcIdentityParts(v);
2151
+ if (!_qcKnownReference(parts, QC_NATURAL_TYPES)) out.push({ context: { serialNumber: v } });
2152
+ });
2153
+ return out.length ? out : null;
2154
+ },
2155
+ },
2156
+ {
2157
+ id: "lint/etsi-qc/qc-natural-identity-type-deprecated", severity: "warn", source: "etsi-qc", citation: "ETSI EN 319 412-1 NAT-5.1.3-04",
2158
+ message: "the TAX identity type reference is deprecated and TIN should be used instead",
2159
+ appliesTo: function (cert, ctx) {
2160
+ return _isQc(cert, ctx) && _qcNamesSemantics(ctx, "semanticsId-Natural");
2161
+ },
2162
+ check: function (cert) {
2163
+ var out = [];
2164
+ _subjectValuesOf(cert, "serialNumber").forEach(function (v) {
2165
+ var parts = _qcIdentityParts(v);
2166
+ if (parts && !parts.local && parts.reference === QC_DEPRECATED_NATURAL_TYPE) {
2167
+ out.push({ context: { serialNumber: v } });
2168
+ }
2169
+ });
2170
+ return out.length ? out : null;
2171
+ },
2172
+ },
2173
+ {
2174
+ id: "lint/etsi-qc/qc-legal-organization-identifier-structure", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-1 LEG-5.1.4-02 and LEG-5.1.4-03",
2175
+ message: "a subject organizationIdentifier does not take the structure the legal person semantics identifier requires",
2176
+ appliesTo: function (cert, ctx) {
2177
+ return _isQc(cert, ctx) && _qcNamesSemantics(ctx, "semanticsId-Legal") &&
2178
+ _subjectValuesOf(cert, "organizationIdentifier").length > 0;
2179
+ },
2180
+ check: function (cert) {
2181
+ var out = [];
2182
+ _subjectValuesOf(cert, "organizationIdentifier").forEach(function (v) {
2183
+ var parts = _qcIdentityParts(v);
2184
+ if (!_qcKnownReference(parts, QC_LEGAL_TYPES)) out.push({ context: { organizationIdentifier: v } });
2185
+ });
2186
+ return out.length ? out : null;
2187
+ },
2188
+ },
2189
+ {
2190
+ id: "lint/etsi-qc/qc-legal-lei-country-not-xg", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-1 LEG-5.1.4-03",
2191
+ /** @internal "LEI for a global Legal Entity Identifier as specified in ISO 17442. The 2
2192
+ * character ISO 3166-1 country code shall be set to 'XG'", the one reference whose country
2193
+ * code the clause fixes. */
2194
+ message: "an LEI organizationIdentifier must carry the country code XG",
2195
+ appliesTo: function (cert, ctx) {
2196
+ return _isQc(cert, ctx) && _qcNamesSemantics(ctx, "semanticsId-Legal");
2197
+ },
2198
+ check: function (cert) {
2199
+ var out = [];
2200
+ _subjectValuesOf(cert, "organizationIdentifier").forEach(function (v) {
2201
+ var parts = _qcIdentityParts(v);
2202
+ if (!parts || parts.local || parts.reference !== "LEI") return;
2203
+ if (parts.country !== QC_LEI_COUNTRY) out.push({ context: { country: parts.country } });
2204
+ });
2205
+ return out.length ? out : null;
2206
+ },
2207
+ },
2208
+ {
2209
+ id: "lint/etsi-qc/qc-semantics-registration-authority-missing", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-1 NAT-5.1.3-05 and LEG-5.1.4-05",
2210
+ /** @internal "When a locally defined identity type reference is provided (two characters
2211
+ * followed by ':'), the nameRegistrationAuthorities element of SemanticsInformation shall be
2212
+ * present." The same sentence appears under each semantics identifier, so one row answers for
2213
+ * both and reads whichever attribute the identifier governs. */
2214
+ message: "a locally defined identity type reference requires a nameRegistrationAuthorities element",
2215
+ appliesTo: function (cert, ctx) { return _isQc(cert, ctx) && _qcEtsiSemanticsAll(ctx).length > 0; },
2216
+ check: function (cert, ctx) {
2217
+ var out = [];
2218
+ _qcEtsiSemanticsAll(ctx).forEach(function (info) {
2219
+ var attr = info.semanticsIdentifier === oid.byName("semanticsId-Legal")
2220
+ ? "organizationIdentifier" : "serialNumber";
2221
+ if (!_qcHasLocalReference(cert, attr)) return;
2222
+ var list = info.nameRegistrationAuthorities;
2223
+ if (!Array.isArray(list) || !list.length) out.push({ context: { attribute: attr } });
2224
+ });
2225
+ return out.length ? out : null;
2226
+ },
2227
+ },
2228
+ {
2229
+ id: "lint/etsi-qc/qc-semantics-registration-authority-not-uri", severity: "error", source: "etsi-qc", citation: "ETSI EN 319 412-1 NAT-5.1.3-06 and LEG-5.1.4-05",
2230
+ /** @internal It "shall contain at least a uniformResourceIdentifier generalName", which is what
2231
+ * makes a locally defined reference resolvable. The two clauses state it differently and the
2232
+ * rows follow them: NAT-5.1.3-06 states it of the element itself, so it binds wherever a
2233
+ * natural person identifier carries one, where LEG-5.1.4-05 states it inside the sentence
2234
+ * about a locally defined reference and binds only there. Both sit under their own semantics
2235
+ * identifier, so a certificate naming a private one is held to neither. */
2236
+ message: "a nameRegistrationAuthorities element must carry a uniformResourceIdentifier",
2237
+ appliesTo: function (cert, ctx) { return _isQc(cert, ctx) && _qcEtsiSemanticsAll(ctx).length > 0; },
2238
+ check: function (cert, ctx) {
2239
+ var out = [];
2240
+ _qcEtsiSemanticsAll(ctx).forEach(function (info) {
2241
+ var list = info.nameRegistrationAuthorities;
2242
+ if (!Array.isArray(list) || !list.length) return;
2243
+ var natural = info.semanticsIdentifier === oid.byName("semanticsId-Natural");
2244
+ if (!natural && !_qcHasLocalReference(cert, "organizationIdentifier")) return;
2245
+ if (!_qcHasUriAuthority(info)) {
2246
+ out.push({ context: { semanticsIdentifier: oid.name(info.semanticsIdentifier) } });
2247
+ }
2248
+ });
2249
+ return out.length ? out : null;
2250
+ },
2251
+ },
2252
+ ];
2253
+
1605
2254
  /** @internal The CA/Browser Forum Code Signing BR v3.11.0 subscriber profile. Every severity is
1606
2255
  * graded against the clause the row cites, never against the same-named row of another profile:
1607
2256
  * this document says MUST where the S/MIME one says SHOULD for cRLDistributionPoints criticality,
@@ -3702,9 +4351,14 @@ var PROFILES = Object.assign(Object.create(null), {
3702
4351
  "cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3703
4352
  "cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3704
4353
  "cabf-cs": CABF_CS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4354
+ "etsi-qc": ETSI_QC_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4355
+ "mozilla-root": MOZILLA_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3705
4356
  });
3706
4357
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
3707
- .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES);
4358
+ .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
4359
+ /** @internal The Mozilla rows are in the registry so `rules()` enumerates them, and stay inert
4360
+ * under the default set because each one asks whether the caller named the profile. */
4361
+ .concat(MOZILLA_RULES);
3708
4362
 
3709
4363
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
3710
4364
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
package/lib/oid.js CHANGED
@@ -83,6 +83,13 @@ var FAMILIES = Object.assign(Object.create(null), {
83
83
 
84
84
  etsiQcType: { base: [0, 4, 0, 1862, 1, 6], of: { qctEsign: 1, qctEseal: 2, qctWeb: 3 } },
85
85
 
86
+ /** @internal The semantics identifiers of ETSI EN 319 412-1 sec. 5.1.2, which a
87
+ * SemanticsInformation names to say how a subject serialNumber or organizationIdentifier is to
88
+ * be read. */
89
+ etsiQcsSemantics: { base: [0, 4, 0, 194121, 1], of: {
90
+ "semanticsId-Natural": 1, "semanticsId-Legal": 2,
91
+ "semanticsId-eIDASNatural": 3, "semanticsId-eIDASLegal": 4 } },
92
+
86
93
  msEnrollment: { base: [1, 3, 6, 1, 4, 1, 311, 21], of: {
87
94
  msCaVersion: 1, msPreviousCertHash: 2, msCertificateTemplate: 7, msApplicationPolicies: 10 } },
88
95
 
@@ -1990,7 +1990,10 @@ function certExtensionDecoders(ns) {
1990
1990
  i++;
1991
1991
  }
1992
1992
  if (i !== node.children.length) throw ns.E(C, "SemanticsInformation has unexpected trailing fields");
1993
- return { semanticsIdentifier: sid, nameRegistrationAuthorities: nras };
1993
+ /** @internal The identifier resolves to its registered name the way QcType's and
1994
+ * QcIdentMethod's values do, and stays null for one the registry does not name. */
1995
+ return { semanticsIdentifier: sid, semanticsIdentifierName: sid === null ? null : (ns.oid.name(sid) || null),
1996
+ nameRegistrationAuthorities: nras };
1994
1997
  }
1995
1998
  var qcInfoByOid = _create(null);
1996
1999
  qcInfoByOid[O("qcCompliance")] = _qcPresenceOnly("QcCompliance");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:8eac5f1d-fefa-406c-937a-2a4c3d5e7164",
5
+ "serialNumber": "urn:uuid:cb37fd90-8a30-496c-912b-d77ed9ce4bed",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-24T11:09:13.708Z",
8
+ "timestamp": "2026-09-24T13:20:17.276Z",
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.8.20",
22
+ "bom-ref": "@blamejs/pki@0.8.22",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.20",
25
+ "version": "0.8.22",
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.8.20",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.22",
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.8.20",
57
+ "ref": "@blamejs/pki@0.8.22",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]