@blamejs/pki 0.8.25 → 0.8.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ 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.26 — 2026-09-28
8
+
9
+ Three lint verbs, and each is short because the parser that feeds it is strict.
10
+
11
+ ### Added
12
+
13
+ - `pki.lint.cms(pem | der | parsed, opts?)` lints a CMS ContentInfo against the RFC 5652 profile, under `profile: "rfc5652"`. A content type this parser reads that is not signed-data carries no signerInfos, so every row is not applicable to it rather than reported against it. A content type the parser does not read, which includes id-data, arrives as a single fatal `lint/unparseable` carrying the parser's own code.
14
+ - Its rows are what the parser leaves: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where section 11.3 requires UTCTime, a signer whose digest algorithm the digestAlgorithms set does not list, which section 5.1 has list the digests every signer employs and so covers a countersigner's digest as well, a version 1 attribute certificate in the certificates field, and two notices, a SignedData carrying no signers and a detached signature whose content a verifier must be handed separately. The other direction of section 11.3 cannot be broken at all: a UTCTime carries two year digits that the clause reads as 19YY or 20YY, so every one of them names a year inside the window.
15
+ - The signing-time rule reads the message's own signers and the countersignatures section 11.4 places in an unsigned attribute, including any nested inside those. A rule about a signed attribute governs one wherever it appears, so a signer cannot put the shape the clause forbids inside a countersignature and escape it. An encapsulated content that is itself a CMS message is linted by running the verb on that message, the way the certificates a message carries are linted with `pki.lint.certificate`. The finding names the message's own signer and says whether the attribute sat on a countersignature under it, rather than a position in that walk, which would send a reader to a different signer. Two digest identifiers naming the same algorithm are compared as RFC 5754 section 2 requires: absent parameters and an explicit NULL are the same digest, so a set that writes one beside a signer that writes the other is listing that signer's algorithm.
16
+ - `pki.lint.tsp(pem | der, opts?)` lints an RFC 3161 timestamp token under `profile: "rfc3161"`. Its rows are a tsa hint naming a subject the token's own certificate does not carry, a serial number wider than the 160 bits a conforming consumer must accommodate, and a token stating no accuracy. The tsa row resolves which certificate verifies the token from the hash the ESS SigningCertificate or SigningCertificateV2 signed attribute carries, which names one certificate and which section 2.4.2 requires every token to hold. Either attribute answers: the original one carries no hash algorithm and RFC 2634 section 5.4 implies SHA-1 for it, so a token written that way resolves its signer as a v2 one does. A token carrying both, which RFC 5035 section 7 permits so a reader that knows only the original keeps its protection against certificate substitution, has each read on its own as that section requires, so whichever of them names a certificate the token embeds resolves the signer; two naming two different embedded certificates leave which one signed the token unsettled, and the row is passed over. RFC 5035 section 4 fixes no set of algorithms for the v2 attribute's hashAlgorithm, so the row reads a certHash under any digest this build computes, at the output lengths RFC 8702 section 2 fixes for id-shake128 and id-shake256. That hash says which of the certificates a token already carries the token names, which is a different question from whether a binding is strong enough to trust, and `pki.tsp.verify` keeps its own narrower set for the latter. The SignerInfo identifier is not that fact: an issuer with a serial number, and a subject key identifier, are both values an issuer wrote, two certificates can carry either, and the certificate set a token embeds sits outside the token's signature, so a hint read against the first identifier match is a hint read against whichever certificate the sender put first. A token whose signer certificate is not embedded is passed over, to be supplied out of band. Carrying one certificate is not the same fact as carrying the signer's, since a token may embed an intermediate, and comparing a hint against that would report a conforming token. The clause says subject names, plural, so the row reads the subject and every subjectAltName entry alike. A distinguished name is compared under the RFC 5280 section 7.1 rules wherever it appears, since two encodings of one name are one name, a DNS name and the text of a mailbox and a URI are compared without regard to ASCII case, since DER canonicalizes the encoding and not the letters, and every other form is compared as its exact encoding, since a structured name such as an otherName is not settled by comparing decoded values.
17
+ - `pki.lint.attrcert(pem | der, opts?)` lints an X.509 attribute certificate under `profile: "rfc5755"`. Its rows are the criticality each clause fixes for the extensions the profile defines, section 4.3's six and the ProxyInfo of section 7.2, a critical extension the profile does not define, a Holder naming more than one of its three options, a Holder entityName carrying more than one name where section 4.2.2 asks for one, a Holder baseCertificateID issuerUID, and an issuerUniqueID. The last two are permitted only where another certificate carries the matching field, so each is a warning naming that condition rather than an error. Section 4.2.2's requirements that these bytes DO settle are refused at parse instead, so they reach an operator as the parser's own code: a Holder baseCertificateID whose issuer is anything other than one non-empty directoryName is refused, which section 4.2.2 requires in the same words section 4.2.3 uses for the issuer. The remaining clauses of that section describe the public-key certificate the Holder names rather than the attribute certificate, so no row can answer them. What counts as outside the profile is membership in that set rather than whether this toolkit can decode the value: the attribute certificate decoder also reads AAControls, which section 7.4 places in a CA or AC issuer public-key certificate rather than in an attribute certificate.
18
+ - `pki.lint.rules()` and `pki.lint.rules(profile, artifact)` reach the three new sets, with `"cms"`, `"tsp"` and `"attrcert"` joining the artifacts a profile name can be read under. `pki.lint.profiles()` lists the three beside the others, and now enumerates the same registry the verbs dispatch on, so a profile a verb can run is one that call answers with.
19
+ - A fuzz target for each of the three, holding the contract every lint verb holds: hostile bytes return a report and never raise. Each drives the raw bytes at the ingestion boundary, splices them over a well-framed structure so the rules run on attacker-controlled values, and then puts them in each attribute or extension a rule reads a value out of.
20
+
21
+ ### Changed
22
+
23
+ - Each of the three verbs refuses a profile name belonging to another one by name, the way the existing verbs do, rather than running a certificate's rows against a message.
24
+ - `pki.cms.countersign` adds the countersigner's digest algorithm to the SignedData `digestAlgorithms` set. RFC 5652 section 5.1 has that set list the digests every signer employs so a consumer can process the message in one pass, and section 11.4 makes a countersignature a SignerInfo. Whether the digest is already listed is decided by RFC 5754 section 2, under which absent parameters and an explicit NULL are one algorithm, so a message whose producer wrote the NULL spelling does not gain a second entry for the digest it already lists. The set is covered by no signature in the message, so every signature a countersigned message already carried still verifies.
25
+ - `pki.attrcert.sign` writes an `aaControls` extension non-critical, where it wrote it critical before, and accepts a pre-encoded one at either criticality. RFC 5755 section 7.4 calls AAControls a PKC extension "intended to be used in CA and AC issuer PKCs" and fixes no criticality for an attribute certificate, so section 4.2.9 governs it there: an extension outside section 4.3 conforms when it is non-critical and does not when it is critical. An attribute certificate carrying a critical one is still refused at `pki.attrcert.verify`, and `pki.lint.attrcert` reports it.
26
+ - `pki.attrcert.verify` compares a `uniformResourceIdentifier` target under RFC 5280 section 7.4, where it compared the two values as written before. A caller naming a target whose scheme case, default port, dot segment or percent-encoded unreserved character differs from the one the attribute certificate carries now matches it, which the clause's five preparation steps make the same URI. A target differing anywhere the clause leaves case-sensitive, a path among them, is still refused.
27
+ - `pki.schema.attrcert.decodeExtension` cites RFC 5755 section 7.2 for the criticality it fixes for `acProxying`, where it cited section 7.3 before. Section 7.2 is Proxying; section 7.3 is the use of ObjectDigestInfo. The same call no longer states a fixed criticality for `aaControls`, because no clause fixes one.
28
+
29
+ ### Fixed
30
+
31
+ - `pki.lint.tsp` compares a tsa hint against each form of name under the RFC 5280 section 7 rule for that form. A mailbox matches when its local part matches exactly and its host part matches without regard to ASCII case (section 7.5), and two URIs are prepared as section 7.4 requires, which lowercases the scheme and host, normalizes percent-encoding and path segments, and drops a default port for the ldap, http, https and ftp schemes that clause names, before the exact match it then calls for. Before, the whole value was folded to lowercase for both forms, so a hint differing from every name the certificate carries in a mailbox local part or a URI path read as one of them and went unreported. A dNSName is unchanged: section 7.2 makes the whole name case-insensitive. A hint in a form neither rule can read is passed over rather than reported, which is the verdict the row already gives where the signer's certificate is absent.
32
+ - `pki.cms.decrypt` reads RFC 4055 section 4.1's OAEP defaults as the values that section assigns rather than inferring one from another. An absent `maskGenFunc` names mgf1SHA1, so a parameter set whose `hashFunc` is SHA-256 and whose `maskGenFunc` is omitted is the same set as one that writes MGF1-SHA-1 out, and both are now refused with `cms/unsupported-algorithm`. Before, the rule that the two hashes must agree was applied to the spelled-out form and skipped when the field was omitted, so a sender could step around it by leaving a field out. A set whose fields do agree, including one that omits `maskGenFunc` beside a SHA-1 `hashFunc`, still decrypts.
33
+ - `pki.inspect` renders an IPv6 address in the form RFC 5952 fixes, lowercase with the longest run of zero groups compressed, wherever it appears. A subjectAltName entry and an RFC 3779 address block carrying the same octets now read the same; before, the first printed `2001:DB8:0:0:0:0:0:1` where the second printed `2001:db8::1`, so one report could show one address two ways. A name constraint's address and mask is still written as both halves joined by a slash, since that pair is not an address.
34
+ - `pki.schema.x509.parse`, `pki.schema.cms.parse` and every other parser that reads an AlgorithmIdentifier refuse a present parameters field on nine identifiers whose specification says to omit it: `ecdsaWithSHA1`, `ecdsaWithSHA256`, `ecdsaWithSHA384`, `ecdsaWithSHA512` and `dsaWithSha1` (RFC 3279 sections 2.2.2 and 2.2.3, RFC 5758 section 3.2), and `shake128`, `shake256`, `id-RSASSA-PSS-SHAKE128` and `id-RSASSA-PSS-SHAKE256` (RFC 8702 sections 2, 3.1 and 3.2). The verdict is `<domain>/bad-algorithm-parameters`. The ECDSA and SHAKE shapes were already refused, a layer later and under a less specific code; they are now refused where the encoding is read. An identifier whose specification requires the field PRESENT and NULL, which is every `*WithRSAEncryption` and `rsaEncryption` itself under RFC 4055, is unaffected.
35
+
7
36
  ## v0.8.25 — 2026-09-24
8
37
 
9
38
  A dated permission that lapses is a requirement, and a certificate's own extended key usages name the purpose it was issued for.
package/README.md CHANGED
@@ -270,7 +270,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
270
270
  | `pki.identity` | Service identity (RFC 9525, which obsoletes RFC 6125). `match(cert, references, opts)` says whether a certificate presents a name the client was trying to reach, which is a separate answer from whether it chains: §1.2 has an application need both, and `pki.path.validate` takes `opts.identity` to run them together and report each. A reference identity is a domain name (DNS-ID), a textual IP address or a 4- or 16-octet `BufferSource` (IP-ID), `{ type: "srv", service: "_imap", value: "example.com" }` (SRV-ID, RFC 4985), or `{ type: "uri", scheme: "https", value: "www.example.com" }` (URI-ID). A record carries its service type and its domain together, so no comparison can pair the service of one reference with the domain of another (§6.5). The subject `commonName` is never read, under any option or fallback, including when the certificate carries no subjectAltName at all (§2). Names are compared label by label under an ASCII-only fold with one trailing dot normalized on both sides; an A-label is compared as the ASCII it is and never decoded, so a reference carrying a character outside ASCII is refused rather than guessed at, this toolkit shipping no IDNA implementation. An IP-ID is compared octet for octet against an `iPAddress` entry, never against a `dNSName` holding the same address as text, and no prefix or mask applies (§6.2, §6.4). Wildcards are on and `opts.wildcards: false` turns them off, which §3 requires stating either way: one wildcard, the whole of the left-most label, reaching exactly one reference label, and a presented name failing those rules is ignored with the search continuing rather than refusing the certificate (§6.3). A wildcard spanning an administrative boundary such as `*.co.uk` is out of scope as it is in §7.1, since no public-suffix list ships here. The verdict names which reference matched, so a caller can use it as the validated identity (§6.6), and lists every entry that was skipped with the reason — `match` |
271
271
  | `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, and the RFC 6960 OCSP no-check responder marker, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
272
272
  | `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985). `sign(spec, key, opts)` builds and signs a `CertificationRequest` from a `spec` of subject (which may be empty), the public key being certified, an optional `extensionRequest` carrying the requested v3 extensions a CA copies into the issued certificate (subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, the RFC 3739 qualified-certificate statements, the Microsoft certificate template and application policies, subject information access, subject directory attributes, or an array of pre-encoded Extension DER; an extension the issuing CA assigns, such as the authority key identifier or the OCSP no-check marker, is refused by name rather than requested), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned, which is what `openssl req -verify` checks. The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. `verify(request)` asks the same question of a request someone else produced, over its exact signed `certificationRequestInfo` bytes under the `subjectPKInfo` inside them, through the one path-validation signature engine. It answers `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, all re-derived from the signed bytes, so a CA issues from the fields the signature covers rather than from the object it passed in. A `verified: true` says the producer held that private key and that the subject and requested extensions are the ones they signed; who they are is the enrollment protocol's question, not this one. Parsing stays at `pki.schema.csr.parse` — `sign`, `verify` |
273
- | `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, cRLDistributionPoints, authorityInfoAccess, or pre-encoded Extension DER) each with its RFC 5755 criticality. The authorityKeyIdentifier is emitted without being named (sec. 4.3.3) and holds the AA certificate's subject key identifier; both extension forms are held to the rules sec. 4.3 and 4.4 place on the issuer (an audit identity of 1 to 20 octets, one Targets element and no targetCert, an OCSP location that is an HTTP URL, one distribution point naming a single DN or HTTP / LDAP URL, a role name that is a URI). An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. `verify(ac, issuer, opts)` is the receiving side, performing the RFC 5755 sec. 5 checks an attribute certificate and a named issuer can settle between them: the signature over the exact `AttributeCertificateInfo` bytes through the one path-validation signature engine, the AC naming the issuer this verifier trusts (compared as a distinguished name), the evaluation instant lying within the validity with equality at either bound succeeding as the section states, the sec. 4.3.2 targeting rule, and rejection of any critical extension this verb does not process (section 5 defines support as parsing the value AND rejecting where the value would reject, so `aaControls` and `acProxying`, whose constraints it does not evaluate, are refused when critical). It answers `{ verified, signatureValid, validityChecked, targetingChecked, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }` with the fields re-derived from the signed bytes; the holder's own certificate chain and the AC issuer's chain need certificates the verb is not given, so those slots report `false` rather than letting their absence read as a pass. Parsing stays at `pki.schema.attrcert.parse` — `sign`, `verify` |
273
+ | `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest; a `baseCertificateID` names its issuer as one non-empty directoryName, which sec. 4.2.2 requires in the same words sec. 4.2.3 uses for the AC issuer, and `pki.schema.attrcert.parse` refuses any other shape), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, cRLDistributionPoints, authorityInfoAccess, or pre-encoded Extension DER) each with its RFC 5755 criticality. The authorityKeyIdentifier is emitted without being named (sec. 4.3.3) and holds the AA certificate's subject key identifier; both extension forms are held to the rules sec. 4.3 and 4.4 place on the issuer (an audit identity of 1 to 20 octets, one Targets element and no targetCert, an OCSP location that is an HTTP URL, one distribution point naming a single DN or HTTP / LDAP URL, a role name that is a URI). An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. `verify(ac, issuer, opts)` is the receiving side, performing the RFC 5755 sec. 5 checks an attribute certificate and a named issuer can settle between them: the signature over the exact `AttributeCertificateInfo` bytes through the one path-validation signature engine, the AC naming the issuer this verifier trusts (compared as a distinguished name), the evaluation instant lying within the validity with equality at either bound succeeding as the section states, the sec. 4.3.2 targeting rule, and rejection of any critical extension this verb does not process (section 5 defines support as parsing the value AND rejecting where the value would reject, so `aaControls` and `acProxying`, whose constraints it does not evaluate, are refused when critical). It answers `{ verified, signatureValid, validityChecked, targetingChecked, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }` with the fields re-derived from the signed bytes; the holder's own certificate chain and the AC issuer's chain need certificates the verb is not given, so those slots report `false` rather than letting their absence read as a pass. Parsing stays at `pki.schema.attrcert.parse` — `sign`, `verify` |
274
274
  | `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. A key that cannot sign, which is where an ML-KEM enrollment sits, takes a `POPOPrivKey` proof instead (RFC 4211 sec. 4.2, sec. 4.3): `pop.type` of `keyEncipherment` or `keyAgreement` with `pop.method: 'subsequentMessage'` declares whether an encrypted certificate (`encrCert`) or a challenge (`challengeResp`) completes the proof over the enrolling protocol's next exchange, and `pop.method: 'encryptedKey'` encloses the requester's private key for the CA in a CMS `EnvelopedData` under `id-ct-encKeyWithID`, requiring `pop.privateKey`, `pop.recipients`, the `pop.identifier` sec. 4.2.1 makes mandatory, and `pop.archive: true` because RFC 9810 sec. 5.2.8.3.1 permits the method only where archival is intended. The enclosed key must be the private half of `certTemplate.publicKey`, or the proof demonstrates possession of a key the request never asked to have certified. The `thisMessage` and `dhMAC` alternatives the specification deprecates are refused with their successors named. `pop.method: 'agreeMAC'` completes the four: `pop.key` is the requester's finite-field Diffie-Hellman private key, as a PKCS#8 DER, a PEM `PRIVATE KEY` block or a `KeyObject`, and `pop.caCert` the authority's certificate it already holds; the two agree a static secret (RFC 2875 sec. 3, obsoleted by RFC 6955), and the key derived from it MACs the DER `certReq` under `id-dhPop-static-HMAC-SHA1`. Either key may name its group in the PKCS#3 or the X9.42 encoding. `pop.key` must be the private half of `certTemplate.publicKey`, and the template must name both a subject and a public key. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. `verifyPop(messages)` is the receiving side: for each `CertReqMsg` it verifies the `POPOSigningKey` signature over the bytes RFC 4211 sec. 4.1 names — the DER of `poposkInput` when present, of `certReq` when not — through the one path-validation signature engine, and answers `{ verified, messages: [{ verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }` with the fields re-derived from the message's own bytes. Each verdict reports only what its preimage covers: `publicKey` is the key possession was proven for, and `subject` is the requested name when the `certReq` was signed. A `poposkInput` preimage covers the key and the sender alone, so any subject beside it is unsigned and is withheld with `subjectBound: false`. A `raVerified` proof is an RA's out-of-band assertion and a `keyEncipherment` or `keyAgreement` proof completes over a later exchange, so each is reported unverified with its `method` named instead of guessed. Parsing stays at `pki.schema.crmf.parse` — `build`, `verifyPop` |
275
275
  | `pki.cmp` | CMP message building, transfer, and verification (RFC 9810). `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` and `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata; a `transactionID` or `senderNonce` the caller omits is filled with 16 fresh random bytes (RFC 9810 §5.1.1, the width a receiver requires under RFC 9483 §3.5), and a `senderNonce` given under 128 bits is refused; a signature-protected `sender` the caller omits is filled with the signer certificate's subject (§3.1); `message.body` is a single-key object naming the arm — request-side `ir`, `cr`, `kur`, `krr` (key recovery, §5.3.7), `ccr` (cross-certification, §5.3.11: a `CertReqMessages` at the normative floor, where the requesting CA keeps its private key so a private-key-transport encryptedKey proof-of-possession is refused, and the optional App. D.6 single-request cardinality is not enforced), `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`, and the registration-authority `nested` wrapper of complete PKIMessages (RFC 9810 §5.1.3.5). Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived), or `opts.kem`, a KEM-based MAC for a client whose key can only establish secrets (RFC 9810 §5.1.3.4): the ciphertext a peer encapsulated to its ML-KEM key is decapsulated, the key is derived under HKDF-SHA256 with a context bound to the transaction identifier of the message that carried the ciphertext, and `pki.cmp.verify` checks it from the shared secret. Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. A `p10cr` request's PKCS#10 proof-of-possession is verified before the message is protected, so one whose self-signature does not verify under its subject public key is refused rather than sent. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a MAC algorithm it does not compute (reported as MAC-protected under that algorithm, never as a failed signature), an omitted keyLength, or a SHA-1 PRF, and returns a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signer certificate is fully path-validated (RFC 5280 §6.1 plus the RFC 9483 §3.2 `keyUsage.digitalSignature` gate) at a trusted current time, or an explicit `opts.time` for historical verification and never the message's self-asserted `messageTime`, before it is reported trusted; without one the verdict is crypto-only and the signer certificate is surfaced to anchor. `session(opts)` returns a stateful enrollment session whose `enroll(request)` drives a full `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport, composing `build`, `transfer`, and `verify`. Every response is protection-verified, signer-trusted, and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` and echoed-`recipNonce` chain) before its body is read, with a bounded `pollReq` / `pollRep` loop for a `waiting` status and a `certConf` / `pkiConf` (or implicit) confirmation carrying an explicit `hashAlg` for a signature algorithm that does not convey its hash. It returns a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`. A certification authority can take longer to answer than a process is held open, so a `poll-timeout` carries a `resumeToken` and `resumePoll(token)` continues the same transaction in a later process: the token is plain JSON naming the transaction, the nonce the next request echoes, the polled request and the arm its grant arrives on, and the key the grant must certify. It carries no secret and grants nothing, since the protection material and the endpoint come from the session built to resume with, and the resumed poll verifies, nonce-binds, and key-binds every response. Its fields are what the resumed exchange is held to, so store it where the enrollment's own state is stored: someone who can rewrite it can widen what the resumed session accepts, as far as that session's trust anchors and the authority's protection allow and no further. The same session also drives the other two RFC 9483 operations under that shell: `revoke(request)` sends an `rr` naming a certificate as `{ certificate }` or `{ certDetails }` with an optional CRLReason `reason`, and returns `revoked` / `rejected` / `poll-timeout`; `info(request)` sends a `genm` for one of the four support messages — `{ caCerts }`, `{ rootCaCert }`, `{ certReqTemplate }`, `{ crlUpdate }` — and returns `answered` with the decoded `value` and a `present` flag, since an absent response value is how each says nothing is available. A session revokes its own certificate: the signature over the request is the proof of authorization (§4.2), so the named certificate must be `opts.cert` and a PBMAC1 session is refused. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. `openKeyPackage(container, opts)` opens a private key the CA generated centrally and delivered in the `privateKey` field of a granted `CertifiedKeyPair` (RFC 9483 §4.1.6): a CMS `SignedData` over an RFC 5958 `AsymmetricKeyPackage`, sealed in a CMS `EnvelopedData`. `opts.key` (with `opts.cert`) opens the key transport and key agreement containers a signature-protected request selects, `opts.password` the password container a MAC-protected request selects. The authority is authorized before any key material is returned: its certificate must chain to an anchor in `opts.trustAnchors` AND assert the `id-kp-cmKGA` extended key usage, since a certificate with no `extendedKeyUsage` permits every purpose and so authorizes nothing; `opts.authorizedBySharedSecret` states §4.1.6's exemption for an entity that authorizes by the secret that protected its request. Returns `{ keys, kga, trusted }`, where each key is a PKCS#8 `PrivateKeyInfo` ready for `pki.key.import`; `keys` is the whole RFC 5958 `AsymmetricKeyPackage` (`SIZE (1..MAX)`), while a session enrollment requires the sequence of one §4.1.6 profiles it to. Both CMS layers are read as a ContentInfo or as the bare structure, told apart by what the SEQUENCE opens with. `session(opts)` drives the whole exchange under `opts.acceptCentralKeyGeneration`: the request omits `certTemplate.publicKey`, carries no proof of possession, and goes out as `cmp2021`; the session opens the delivered container with the credential it enrolled under and surfaces it as `deliveredKey`, having first checked that the issued certificate certifies the delivered key. Without the option, such a request and such a grant are both refused. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `openKeyPackage`, `session` |
276
276
  | `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuer alternative name, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access, which is every extension RFC 5280 §5.2 defines) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The authority key identifier is emitted on every CRL whether or not it is named, as §5.2.1 places it on every conforming issuer, from the issuer certificate's SKI or the issuer key. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, the RFC fixes per-extension criticality, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine, algorithm-confusion and EdDSA low-order gates included; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber, opts)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. A CRL speaks for a span as well as a set, so `opts.time` states the instant the question is asked at and a list that has stopped speaking for it — `nextUpdate` passed, `thisUpdate` later, or no `nextUpdate` stated at all — is refused rather than read as a clean bill of health; without `opts.time` the lookup is structural and an absent serial means only that it is not on that list. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base, performs the §6.3.3 correspondence, and decides currency against the material it fetched. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign`, `verify`, `isRevoked` |
@@ -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 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 Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), 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 Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), 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), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `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>` |
@@ -79,7 +79,14 @@ intrinsic.forEach([
79
79
  ["noRevAvail", "noRevAvail"], ["aaControls", "aaControls"], ["acProxying", "acProxying"],
80
80
  ["authorityKeyIdentifier", "authorityKeyIdentifier"], ["cRLDistributionPoints", "cRLDistributionPoints"],
81
81
  ["authorityInfoAccess", "authorityInfoAccess"],
82
- ], function (r) { EXT_META[r[0]] = { name: r[1], critical: REQUIRED_CRITICALITY[O(r[1])].critical }; });
82
+ /** @internal An extension whose clause fixes a criticality is written with it. One whose clause only
83
+ * permits critical, which is AAControls of sec. 7.4, has no row to read, and is written
84
+ * non-critical: sec. 4.2.9 makes a non-critical extension outside sec. 4.3 conforming and a critical
85
+ * one non-conforming, so the permitted form that conforms is the one to emit. */
86
+ ], function (r) {
87
+ var fixed = REQUIRED_CRITICALITY[O(r[1])];
88
+ EXT_META[r[0]] = { name: r[1], critical: fixed ? fixed.critical : false };
89
+ });
83
90
  var OID_AKI = O("authorityKeyIdentifier"), OID_OCSP = O("ocsp");
84
91
 
85
92
  /** @internal RFC 5755 sets rules on the issuer that its parser reads past and its verifier does
@@ -649,18 +656,32 @@ function _callerTarget(target) {
649
656
  return attrcert.readGeneralName(_b.encodeGeneralName(target), "attrcert/bad-input");
650
657
  }
651
658
 
659
+ /** @internal RFC 5280 sec. 7.5's rule, read from the one place that holds it: the local part matches
660
+ * exactly and the host part without regard to ASCII case. A value that rule cannot separate into the
661
+ * two halves is compared as written, since identical text is one name whatever its shape. */
652
662
  function _mailboxEqual(a, bx) {
653
- var i = a.lastIndexOf("@"), j = bx.lastIndexOf("@");
654
- if (i < 0 || j < 0) return a === bx;
655
- return a.slice(0, i) === bx.slice(0, j) &&
656
- a.slice(i + 1).toLowerCase() === bx.slice(j + 1).toLowerCase();
663
+ var verdict = guard.name.emailEqual(a, bx);
664
+ if (verdict === "match") return true;
665
+ if (verdict === "no-match") return false;
666
+ return a === bx;
657
667
  }
658
668
 
669
+ /** @internal Each form is compared under the RFC 5280 sec. 7 rule for it, which sec. 4.3.2's
670
+ * "matches one of the GeneralNames" leaves to that section. A dNSName is case-insensitive throughout
671
+ * (sec. 7.2). A URI is not: sec. 7.4 prepares both values, lowercasing the scheme and host alone, and
672
+ * then calls for an exact match, so two URIs written with the same scheme in different case name one
673
+ * target where comparing the values as written made them two. */
659
674
  function _generalNameMatches(entry, want) {
660
675
  if (!entry || entry.tagNumber !== want.tagNumber) return false;
661
676
  switch (entry.tagNumber) {
662
677
  case 2: return String(entry.value).toLowerCase() === String(want.value).toLowerCase();
663
678
  case 1: return _mailboxEqual(String(entry.value), String(want.value));
679
+ case 6:
680
+ if (typeof entry.value !== "string" || typeof want.value !== "string") return null;
681
+ var uri = guard.name.uriEqual(entry.value, want.value);
682
+ if (uri === "match") return true;
683
+ if (uri === "no-match") return false;
684
+ return entry.value === want.value;
664
685
  case 4:
665
686
  return !!(entry.value && want.value) &&
666
687
  guard.name.dnEqual(entry.value.rdns, want.value.rdns, _err, "attrcert/bad-input", "the target name");
package/lib/cmp-verify.js CHANGED
@@ -247,11 +247,17 @@ function _mailboxSplit(s) {
247
247
  return sep;
248
248
  }
249
249
 
250
+ /** @internal The split above decides whether this sender name is one this module will compare at all,
251
+ * and RFC 5280 sec. 7.5's rule for two it will decides the verdict, read from the one place that
252
+ * holds that rule. A name the split refuses is compared as written, which is the stricter answer and
253
+ * the one a sender identity wants. */
250
254
  function _rfc822Equal(a, b) {
251
255
  var ai = _mailboxSplit(a), bi = _mailboxSplit(b);
252
256
  if (ai < 0 || bi < 0) return a === b;
253
- return _strSlice(a, 0, ai) === _strSlice(b, 0, bi) &&
254
- _lowerAsciiDomain(_strSlice(a, ai + 1)) === _lowerAsciiDomain(_strSlice(b, bi + 1));
257
+ var verdict = guard.name.emailEqual(a, b);
258
+ if (verdict === "match") return true;
259
+ if (verdict === "no-match") return false;
260
+ return a === b;
255
261
  }
256
262
 
257
263
  var _lowerAsciiDomain = guard.name.lowerAscii;
@@ -347,9 +353,17 @@ function _normalizeUri(u) {
347
353
  if (!hostOk) return null;
348
354
  return scheme + "://" + userinfo + _toLowerCase(host) + port + tail;
349
355
  }
356
+ /** @internal The normalization above decides whether this module will compare the two URIs at all: it
357
+ * refuses one whose host is neither a DNS name nor an IPv6 literal, and one of those is compared as
358
+ * written. For two it accepts, RFC 5280 sec. 7.4's five preparation steps and the exact match it then
359
+ * calls for decide the verdict, read from the one place that holds them, so a sender naming itself
360
+ * with a default port or a dot segment is the sender the certificate carries. */
350
361
  function _uriEqual(a, b) {
351
362
  var na = _normalizeUri(a), nb = _normalizeUri(b);
352
363
  if (na === null || nb === null) return a === b;
364
+ var verdict = guard.name.uriEqual(a, b);
365
+ if (verdict === "match") return true;
366
+ if (verdict === "no-match") return false;
353
367
  return na === nb;
354
368
  }
355
369
 
@@ -493,10 +493,16 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
493
493
  }
494
494
  }
495
495
 
496
+ /** @internal RFC 4055 sec. 4.1 gives every RSAES-OAEP-params field a default: `hashFunc` sha1,
497
+ * `maskGenFunc` mgf1SHA1, `pSourceFunc` pSpecifiedEmpty. An omitted field therefore NAMES a value
498
+ * rather than leaving the question open, so each default is resolved here before the fields are
499
+ * compared. Reading an absent `maskGenFunc` as "whatever hashFunc said" would enforce the equality
500
+ * below on a parameter set that spells the mask generator out and skip it on the same set written with
501
+ * the field omitted. The engine takes one hash for both, so a set naming two is one it cannot perform. */
496
502
  function _oaepHashFromParams(paramsBytes) {
497
503
  if (paramsBytes == null) return "SHA-1";
498
504
  var node = asn1.decode(paramsBytes);
499
- var hashName = "SHA-1", mgfHash = null, label = null;
505
+ var hashName = "SHA-1", mgfHash = "SHA-1", label = null;
500
506
  (node.children || []).forEach(function (ch) {
501
507
  if (ch.tagClass !== "context") return;
502
508
  if (ch.tagNumber === 0) { hashName = _hashW3c(asn1.read.oid(ch.children[0].children[0])); }
@@ -506,7 +512,7 @@ function _oaepHashFromParams(paramsBytes) {
506
512
  mgfHash = _hashW3c(asn1.read.oid(mg.children[1].children[0]));
507
513
  } else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); }
508
514
  });
509
- if (mgfHash != null && mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
515
+ if (mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
510
516
  if (label != null && label.length > 0) throw _err("cms/unsupported-algorithm", "a non-empty OAEP label is not supported");
511
517
  return hashName;
512
518
  }
package/lib/cms-sign.js CHANGED
@@ -347,6 +347,16 @@ function _assembleSignedData(built, eContentType, opts, contentBuf) {
347
347
  return opts.pem ? pkix.pemEncode(contentInfo, "CMS", frameworkError.PemError) : contentInfo;
348
348
  }
349
349
 
350
+ /** @internal The OID and parameters of a digest AlgorithmIdentifier TLV, in the shape the schema
351
+ * module's RFC 5754 sec. 2 comparison reads a parsed one. */
352
+ function _digestAlgIdentifier(der) {
353
+ var n = asn1.decode(der);
354
+ return {
355
+ oid: asn1.read.oid(n.children[0]),
356
+ parameters: n.children.length > 1 ? n.children[1].bytes : null,
357
+ };
358
+ }
359
+
350
360
  function _dedupe(ders) {
351
361
  var seen = intrinsic.create(null), out = [];
352
362
  intrinsic.forEach(ders, function (d) { var k = intrinsic.bufToString(d, "hex"); if (!seen[k]) { seen[k] = 1; intrinsic.push(out, d); } });
@@ -445,7 +455,7 @@ function _buildCountersignature(targetSigOctets, countersigner, opts) {
445
455
  var fields = [b.integer(BigInt(sidv.version)), sidv.sid, scheme.digestAlgId];
446
456
  if (attrs) fields.push(attrs.wire);
447
457
  fields.push(scheme.sigAlgId, b.octetString(sig));
448
- return { value: b.sequence(fields), certDer: certDer };
458
+ return { value: b.sequence(fields), certDer: certDer, digestAlgId: scheme.digestAlgId };
449
459
  });
450
460
  });
451
461
  });
@@ -542,8 +552,13 @@ function _countersign(cmsInput, signers, opts) {
542
552
  list.forEach(function (cs) { jobs.push({ t: t, p: _buildCountersignature(preimage, cs, opts) }); });
543
553
  });
544
554
  return Promise.all(jobs.map(function (j) { return j.p; })).then(function (built) {
545
- var byTarget = intrinsic.create(null), certDers = [];
546
- built.forEach(function (res, i) { (byTarget[jobs[i].t] = byTarget[jobs[i].t] || []).push(res.value); certDers.push(res.certDer); });
555
+ var byTarget = intrinsic.create(null), certDers = [], csDigestAlgs = [];
556
+ intrinsic.forEach(built, function (res, i) {
557
+ var slot = byTarget[jobs[i].t] || (byTarget[jobs[i].t] = []);
558
+ intrinsic.push(slot, res.value);
559
+ intrinsic.push(certDers, res.certDer);
560
+ intrinsic.push(csDigestAlgs, res.digestAlgId);
561
+ });
547
562
 
548
563
  var newSiSet = b.set(siSet.children.map(function (siNode, idx) {
549
564
  if (!byTarget[idx]) return siNode.bytes;
@@ -560,7 +575,29 @@ function _countersign(cmsInput, signers, opts) {
560
575
  if (opts.certificates !== false) certDers.forEach(function (d) { existing.push(d); });
561
576
  var allCerts = _dedupe(existing).sort(Buffer.compare);
562
577
 
563
- var newSdFields = [sdKids[0].bytes, sdKids[1].bytes, sdKids[2].bytes];
578
+ /** @internal RFC 5652 sec. 5.1 has this collection list "the message digest algorithms employed
579
+ * by all of the signers", and sec. 11.4 makes a countersignature a SignerInfo, so sec. 5.3's
580
+ * "the message digest algorithm SHOULD be among those listed in the digestAlgorithms field"
581
+ * reaches a countersigner's digest too. The set is outside every signature in the message, the
582
+ * way the unsigned attribute the countersignature lands in is, so adding to it here leaves each
583
+ * existing signature verifying over the same bytes.
584
+ * Whether an algorithm is ALREADY listed is asked through the schema module's RFC 5754 sec. 2
585
+ * comparison, not by matching bytes: a message from another producer may spell a digest with an
586
+ * explicit NULL where this one omits the parameters, and those are one algorithm, so a byte
587
+ * comparison would list it a second time. */
588
+ var listed = [];
589
+ intrinsic.forEach(intrinsic.isArray(parsed.digestAlgorithms) ? parsed.digestAlgorithms : [],
590
+ function (a) { intrinsic.push(listed, a); });
591
+ var digestAlgs = intrinsic.map(sdKids[1].children, function (d) { return d.bytes; });
592
+ intrinsic.forEach(csDigestAlgs, function (der) {
593
+ var info = _digestAlgIdentifier(der);
594
+ var already = false;
595
+ intrinsic.forEach(listed, function (alg) { if (cms.sameDigestAlgorithm(alg, info)) already = true; });
596
+ if (already) return;
597
+ intrinsic.push(listed, info);
598
+ intrinsic.push(digestAlgs, der);
599
+ });
600
+ var newSdFields = [sdKids[0].bytes, b.set(digestAlgs), sdKids[2].bytes];
564
601
  if (allCerts.length) newSdFields.push(b.contextConstructed(0, Buffer.concat(allCerts)));
565
602
  if (crlsNode) newSdFields.push(crlsNode.bytes);
566
603
  newSdFields.push(newSiSet);
package/lib/constants.js CHANGED
@@ -160,6 +160,7 @@ var LIMITS = Object.assign(Object.create(null), {
160
160
  PKCS12_MAX_DH_PAIRS: 8,
161
161
  PKCS12_MAX_REDECODES: 64,
162
162
  PKCS12_MAX_BAG_DEPTH: 16,
163
+ CMS_MAX_COUNTERSIGNATURE_DEPTH: 16,
163
164
  BER_MAX_STRING_NESTING: 8,
164
165
  ATTRIBUTE_MAX_VALUES: 256,
165
166
  JSON_MAX_BYTES: BYTES.mib(1),
package/lib/guard-name.js CHANGED
@@ -290,8 +290,7 @@ function escapeDnValue(v) {
290
290
  return out;
291
291
  }
292
292
 
293
- // @enforced-by guard-shape-reinlined
294
- // @guard-shape lastIndexOf\("@"\)
293
+ // @enforced-by behavioral -- a shape naming the separator cannot fire, since the walk strips string literals before applying one, and the split has no other form that separates it from the authority scans in this tree; the vectors are the guard, where a local part differing in case alone is a different mailbox and a host part that does is the same one
295
294
  function emailEqual(a, b) {
296
295
  if (typeof a !== "string" || typeof b !== "string") return "not-comparable";
297
296
  var ai = _lastIndexOf(a, "@"), bi = _lastIndexOf(b, "@");
@@ -447,7 +446,9 @@ var _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV
447
446
  * that needs a host decides what to do with that rather than being handed a guess. `host` is null
448
447
  * when the authority carries none, and `bracketed` says the host arrived inside `[]`, which is the
449
448
  * only place an IPv6 literal may appear. Percent-escapes are left as written, so a caller comparing
450
- * the host decides whether to accept one. */
449
+ * the host decides whether to accept one. `authStart` and `authEnd` bound the authority as written,
450
+ * userinfo and port included, and are equal on a URI that carries none, so a caller reading the rest
451
+ * of the URI reads it from the same walk rather than scanning again. */
451
452
  // @enforced-by behavioral -- a URI walk has no rename-proof shape that separates it from any other character scan; the vectors reading a scheme, refusing two userinfo separators and stripping a port are the guard
452
453
  function uriParts(uri) {
453
454
  if (typeof uri !== "string") return null;
@@ -460,7 +461,8 @@ function uriParts(uri) {
460
461
  if (_charCodeAt(uri, p) !== 0x3a) return null;
461
462
  var scheme = _strSlice(uri, 0, p);
462
463
  if (_charCodeAt(uri, p + 1) !== 0x2f || _charCodeAt(uri, p + 2) !== 0x2f) {
463
- return { scheme: scheme, hasAuthority: false, host: null, bracketed: false };
464
+ return { scheme: scheme, hasAuthority: false, host: null, bracketed: false,
465
+ authStart: p + 1, authEnd: p + 1 };
464
466
  }
465
467
  p += 3;
466
468
  var authStart = p;
@@ -474,12 +476,17 @@ function uriParts(uri) {
474
476
  if (firstAt !== _lastIndexOf(authority, "@")) return null;
475
477
  if (firstAt !== -1) authority = _strSlice(authority, firstAt + 1);
476
478
  var host = _strSlice(authority, 0, _portStripIndex(authority));
477
- if (host === "") return { scheme: scheme, hasAuthority: true, host: null, bracketed: false };
479
+ if (host === "") {
480
+ return { scheme: scheme, hasAuthority: true, host: null, bracketed: false,
481
+ authStart: authStart, authEnd: p };
482
+ }
478
483
  var hn = host.length;
479
484
  if (hn >= 2 && _charCodeAt(host, 0) === 0x5b && _charCodeAt(host, hn - 1) === 0x5d) {
480
- return { scheme: scheme, hasAuthority: true, host: _strSlice(host, 1, hn - 1), bracketed: true };
485
+ return { scheme: scheme, hasAuthority: true, host: _strSlice(host, 1, hn - 1), bracketed: true,
486
+ authStart: authStart, authEnd: p };
481
487
  }
482
- return { scheme: scheme, hasAuthority: true, host: host, bracketed: false };
488
+ return { scheme: scheme, hasAuthority: true, host: host, bracketed: false,
489
+ authStart: authStart, authEnd: p };
483
490
  }
484
491
 
485
492
  /** @internal Where a `:port` suffix starts, or the length when there is none. RFC 3986 sec. 3.2.3
@@ -497,6 +504,138 @@ function _portStripIndex(s) {
497
504
  return (i > 0 && _charCodeAt(s, i - 1) === 0x3a) ? i - 1 : s.length;
498
505
  }
499
506
 
507
+ var _UNRESERVED_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~");
508
+ var _HEXUP = "0123456789ABCDEF";
509
+
510
+ /** @internal Percent-encoding normalization (RFC 3986 sec. 6.2.2.2, which RFC 3987 sec. 5.3.2.3
511
+ * carries): a triplet encoding an unreserved character is decoded to that character, and the hex
512
+ * digits of every triplet that remains are written in uppercase. A `%` that does not introduce two
513
+ * hex digits is left as written, since a value this cannot read is one the comparison must not
514
+ * rewrite. */
515
+ function _pctNormalize(s) {
516
+ var out = "", i = 0, n = s.length;
517
+ while (i < n) {
518
+ var c = _charCodeAt(s, i);
519
+ if (c !== 0x25 || i + 2 >= n) { out += _charAt(s, i); i += 1; continue; }
520
+ var hi = _hexNibble(_charCodeAt(s, i + 1)), lo = _hexNibble(_charCodeAt(s, i + 2));
521
+ if (hi < 0 || lo < 0) { out += _charAt(s, i); i += 1; continue; }
522
+ var v = hi * 16 + lo;
523
+ out += _UNRESERVED_TABLE[v] ? _fromCharCode(v)
524
+ : "%" + _charAt(_HEXUP, hi) + _charAt(_HEXUP, lo);
525
+ i += 3;
526
+ }
527
+ return out;
528
+ }
529
+
530
+ /** @internal Path segment normalization (RFC 3986 sec. 5.2.4's remove_dot_segments, which RFC 3987
531
+ * sec. 5.3.2.4 carries). The document writes the algorithm as repeated removal from the front of the
532
+ * input and appending to an output buffer; both are done here with an index and a list of the segments
533
+ * kept, because rewriting the remaining input on each step costs a copy of it per segment, and a path
534
+ * of many `a/../` segments then costs time in the square of its length. A certificate can carry a URI
535
+ * of any length the encoding allows, and this runs on one while an operator waits, so the walk reads
536
+ * each character once.
537
+ * A segment is read with the `/` that introduces it, so the kept list holds whole `/segment` units and
538
+ * removing "the last segment" is removing the last of them. A `.` or `..` that opens a relative path
539
+ * takes its following `/` with it, which is what sec. 5.2.4 step 2A does. */
540
+ function _removeDotSegments(path) {
541
+ var kept = [], i = 0, n = path.length;
542
+ while (i < n) {
543
+ var lead = _charCodeAt(path, i) === 0x2f;
544
+ if (lead) i += 1;
545
+ var segStart = i;
546
+ while (i < n && _charCodeAt(path, i) !== 0x2f) i += 1;
547
+ var seg = _strSlice(path, segStart, i);
548
+ if (seg === "." || seg === "..") {
549
+ if (!lead) {
550
+ if (i < n) i += 1;
551
+ continue;
552
+ }
553
+ if (seg === ".." && kept.length) kept.length -= 1;
554
+ /** @internal The last segment of the input being `.` or `..` leaves the path ending in a
555
+ * separator, which steps 2B and 2C write as replacing it with `/`. */
556
+ if (i >= n) _append(kept, "/");
557
+ continue;
558
+ }
559
+ _append(kept, lead ? "/" + seg : seg);
560
+ }
561
+ return intrinsic.join(kept, "");
562
+ }
563
+
564
+ /** @internal The port each scheme RFC 5280 sec. 7.4 names leaves out when it is the default one
565
+ * (RFC 3986 sec. 6.2.3). The clause requires scheme-based normalization for exactly these four and
566
+ * says to omit step 5 for any other scheme, so a scheme absent from this table keeps its port. */
567
+ var _SCHEME_DEFAULT_PORT = intrinsic.assign(_create(null),
568
+ { http: "80", https: "443", ftp: "21", ldap: "389" });
569
+
570
+ /** @internal A digit string with its leading zeros removed, and "0" for one that is all zeros, so a
571
+ * port written with them is read as the number RFC 3986 sec. 3.2.3 says it is. */
572
+ function _dropLeadingZeros(digits) {
573
+ var i = 0;
574
+ while (i < digits.length - 1 && _charCodeAt(digits, i) === 0x30) i += 1;
575
+ return _strSlice(digits, i);
576
+ }
577
+
578
+ /** @internal A URI prepared for comparison as RFC 5280 sec. 7.4 requires, or null for a value that
579
+ * is not a URI. The clause's step 1 converts an internationalized name to ASCII Compatible Encoding;
580
+ * a `uniformResourceIdentifier` is an IA5String, so a value read out of a certificate is already
581
+ * ASCII and that step has nothing to do, and a caller handing this a non-ASCII string gets it back
582
+ * unconverted rather than guessed at. Step 2 lowercases the scheme and the host, step 3 normalizes
583
+ * percent-encoding, step 4 normalizes path segments, and step 5 drops a default port and writes an
584
+ * empty path as `/` for the four schemes the clause names. Everything else keeps its case, which is
585
+ * what makes the exact match the clause then calls for answer correctly. The steps run in the order
586
+ * the clause numbers them, so a host is lowercased before its percent-encoding is read and a triplet
587
+ * standing for an unreserved character contributes that character's own case. */
588
+ function _uriPrepare(uri) {
589
+ var p = uriParts(uri);
590
+ if (p === null) return null;
591
+ var scheme = lowerAscii(p.scheme);
592
+ var rest = _pctNormalize(_strSlice(uri, p.authEnd));
593
+ var authority = "";
594
+ if (p.hasAuthority) {
595
+ var auth = _strSlice(uri, p.authStart, p.authEnd);
596
+ var at = _lastIndexOf(auth, "@");
597
+ var userinfo = at === -1 ? "" : _pctNormalize(_strSlice(auth, 0, at + 1));
598
+ var hostPort = _strSlice(auth, at + 1);
599
+ var cut = _portStripIndex(hostPort);
600
+ var host = _pctNormalize(lowerAscii(_strSlice(hostPort, 0, cut)));
601
+ var port = _strSlice(hostPort, cut);
602
+ /** @internal RFC 3986 sec. 6.2.3, which step 5 runs for the four schemes the clause names: an
603
+ * explicit `:port` whose port is empty, or is that scheme's default, is the same URI as one
604
+ * written without it. sec. 3.2.3 writes `port = *DIGIT`, so the port is a number and `:080` on
605
+ * http is the default one; the digits are read as a number rather than compared as text, which is
606
+ * what the clause's own purpose, reducing false negatives, asks for. A scheme the clause does not
607
+ * name keeps the port as written, since step 5 is omitted for it, and no step removes a trailing
608
+ * dot from a host, so one is kept too. */
609
+ if (_hasOwn(_SCHEME_DEFAULT_PORT, scheme) &&
610
+ (port === ":" || _dropLeadingZeros(_strSlice(port, 1)) === _SCHEME_DEFAULT_PORT[scheme])) {
611
+ port = "";
612
+ }
613
+ authority = "//" + userinfo + host + port;
614
+ }
615
+ var pathEnd = rest.length;
616
+ for (var i = 0; i < rest.length; i++) {
617
+ var c = _charCodeAt(rest, i);
618
+ if (c === 0x3f || c === 0x23) { pathEnd = i; break; }
619
+ }
620
+ var path = _removeDotSegments(_strSlice(rest, 0, pathEnd));
621
+ if (path === "" && p.hasAuthority && _hasOwn(_SCHEME_DEFAULT_PORT, scheme)) path = "/";
622
+ return scheme + ":" + authority + path + _strSlice(rest, pathEnd);
623
+ }
624
+
625
+ /** @internal Whether two URIs name the same resource, under the preparation RFC 5280 sec. 7.4
626
+ * requires before the "case-sensitive exact match" it then calls for. Folding case over the whole
627
+ * value instead reads two URIs differing only in a path or a userinfo as one name, and comparing the
628
+ * values as written reads two differing only in the case of a scheme or a host as two. Returns
629
+ * "not-comparable" for a value that is not a URI, which leaves the caller to say that rather than
630
+ * report a verdict this did not reach. */
631
+ // @enforced-by behavioral -- the preparation is five character scans with no shape that separates it from any other; the vectors driving each step, and the pair that differs only in a path, are the guard
632
+ function uriEqual(a, b) {
633
+ if (typeof a !== "string" || typeof b !== "string") return "not-comparable";
634
+ var pa = _uriPrepare(a), pb = _uriPrepare(b);
635
+ if (pa === null || pb === null) return "not-comparable";
636
+ return pa === pb ? "match" : "no-match";
637
+ }
638
+
500
639
  var _ATEXT_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-/=?^_`{|}~");
501
640
 
502
641
  /** @internal A quoted local part (RFC 5321 sec. 4.1.2 Quoted-string): printable ASCII between
@@ -773,6 +912,7 @@ module.exports = intrinsic.freeze({
773
912
  lowerAscii: lowerAscii, uriSchemeIs: uriSchemeIs,
774
913
  stripTrailingDot: stripTrailingDot, isFqdnHost: isFqdnHost, splitMailbox: splitMailbox,
775
914
  isHostName: isHostName, hostLabels: hostLabels, isXnLabel: isXnLabel, uriParts: uriParts,
915
+ uriEqual: uriEqual,
776
916
  constraintBaseRefusal: constraintBaseRefusal,
777
917
  constraintBaseUnreadable: constraintBaseUnreadable,
778
918
  });