@blamejs/pki 0.8.50 → 0.8.51
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 +15 -0
- package/README.md +3 -3
- package/lib/acme.js +33 -13
- package/lib/cmp-build.js +9 -3
- package/lib/constants.js +3 -2
- package/lib/est.js +31 -14
- package/lib/guard-intrinsic.js +4 -0
- package/lib/http-digest.js +7 -2
- package/lib/inspect.js +3 -2
- package/lib/lint.js +105 -2
- package/lib/path-validate.js +89 -8
- package/lib/pki-build.js +40 -0
- package/lib/revocation-fetch.js +11 -5
- package/lib/scep.js +26 -12
- package/lib/x509-sign.js +22 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ 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.51 — 2026-10-05
|
|
8
|
+
|
|
9
|
+
Issue and request the TLS Feature extension, hold a chain to the features its issuer advertises, and read a parsed destination through accessors taken at load.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `extensions.tlsFeature` on `pki.x509.sign`, `pki.csr.sign` and `pki.x509.extension` writes the RFC 7633 TLS Feature extension. The value is a list of TLS ExtensionType values, each a number, a bigint, or one of the registry names this build knows, `status_request` and `status_request_v2`. It is emitted non-critical unless `tlsFeatureCritical` is set: section 4 says the extension SHOULD NOT be marked critical and then names the case where the critical form is the intent, so the knob exists rather than the critical form being refused. A request carries it in the certification-request extension attribute, which is where OpenSSL's `req -addext "tlsfeature=status_request"` puts it, and the bytes the two produce are identical. An empty list is refused, because the extension it would emit advertises nothing; one that arrives on the wire still decodes, as it did before.
|
|
14
|
+
- `pki.lint.certificate(der, { profile: "rfc7633" })` reads the extension's values. RFC 7633 states no range, no ordering and no prohibition on repeats, so none of those is refused by the encoder and all are reported here: a feature outside the 16-bit width of the IANA registry that section 4.1 points at, and a repeated feature, which names the same set it would without the repeat. Two rows come from elsewhere. RFC 9162 section 7.2 asks a certification authority not to issue a certificate naming the transparency_info feature, because a TLS server need not support it to take part in certificate transparency. And a certificate naming status_request_v2 asks of a TLS 1.3 server something that version does not define: RFC 9846 obsoletes RFC 6961, which defined that extension, and leaves it out of the ExtensionType enum in its section 4.3. The IANA registry still lists it as recommended and marks it neither obsolete nor deprecated, so that row is a notice and says only what those documents say. A critical TLS Feature extension is reported by the default rule set and needs no profile.
|
|
15
|
+
- `pki.path.validate` enforces RFC 7633 section 4.2.2 at every issuer-to-subject boundary of a path. A certificate signed by a signing certificate that asserts keyCertSign and carries the extension must carry one offering the same set or a superset, or the path fails with `path/tls-feature-not-offered`; a subject carrying no extension at all under such an issuer is the same failure, because the clause requires it to carry one. The comparison is of sets, so neither order nor a repeat on either side changes it. An issuer that carries no extension constrains nothing. A trust anchor's own extensions are not read, so the rule applies from the first certificate in the path downward, and a boundary that was not checked reports no result rather than a passing one.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- A parsed request destination is read through accessors the package captured at load, in `pki.acme`, `pki.est`, `pki.scep`, `pki.cmp`, the revocation fetcher, the certificate-path builder's authority-information-access fetch and the HTTP transport's Digest authentication. Capturing the `URL` constructor, which v0.8.50 did, does not capture the prototype the value it returns reads its parts off: redefining `href` there answered every read of a destination, so a caller's code running mid-request could still move it. Each part is now read once into a local and every decision made from that local, which also closes the case of a part that answers two reads differently: the scheme that decides whether a request is allowed is the scheme the port defaults from, the host the address filter is applied to is the host the request is sent to, and the request target a Digest credential is computed over is the target it is sent to.
|
|
20
|
+
- The citation on the TLS Feature renderer named RFC 7633 section 6 for the ExtensionType values. Section 6 is IANA Considerations and registers the extension's own object identifier; section 4.1 is the clause that points at the IANA registry for the values.
|
|
21
|
+
|
|
7
22
|
## v0.8.50 — 2026-10-05
|
|
8
23
|
|
|
9
24
|
Decide where a certificate-transparency request goes and which trust anchors it is made under before reading anything else the caller passed, and keep a copied byte argument out of the caller's reach.
|
package/README.md
CHANGED
|
@@ -266,9 +266,9 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
266
266
|
| `pki.cmc` | Build and interpret CMC messages (RFC 5272, obsoleted by RFC 10002). `build(spec, signer)` assembles a Full PKI Request across all three request arms (PKCS#10, CRMF, other) and carries it either way RFC 5272 §3.2 allows: `{ cert, key }` signs it, while `{ mac: { identifier, secret } }` authenticates it under a shared secret for a client that holds no key yet, using the PasswordRecipientInfo that section requires and deriving the key from the identifier followed by the secret. A key that cannot sign for itself proves possession another way: `spec.popChallenge` answers the authority's encrypted challenge (RFC 5272 §6.7), decrypting the proof value, confirming it hashes to the witness the challenge carries, and returning the MAC the authority asked for, with `cmc/pop-failed` when the value is not the one issued. A signed request goes through `pki.cms.sign` under `id-cct-PKIData`. A PKCS#10 (`tcr`) request is verified against its own proof-of-possession before signing, so a request whose self-signature does not verify under its subject public key is refused rather than signed into a message a CA would reject. Body-part identifiers are unique across the whole message and never the reserved 0; a caller's clash is refused rather than renumbered, since a control may already reference it. An Identity Proof V2 witness is computed over the `reqSequence` bytes exactly as emitted (§6.2.1 step 1) rather than a re-serialization, a POP Link Witness is emitted only alongside the POP Link Random control §6.3.1.1 requires beside it, and a renewal carries neither Identification nor Identity Proof in either version. `verify(response, sent)` takes what the CA returned plus the state the client retained and reduces it to one terminal outcome: `issued`, `pending`, `confirm-required`, `pop-required`, or `rejected`. It binds the exchange first — Transaction Identifier, the Sender and Recipient Nonce echo compared in constant time and by full value so a truncation cannot match, and the Data Return echo — with each check applying only if the client sent that half, and, once sent, an absent or differing echo being a refusal, which is the replay defense. `bodyPartIDs` extends the same rule to what the response is about: a status reporting on a body part the request never sent is refused, which the transaction and nonce cannot catch, because a server can echo both correctly while answering about a different message. Several status controls are permitted and the worst governs, so a failure cannot hide behind an earlier success; the absence of any status control is success, per §6.1.2. The carrier's signature must verify (§3.2.1.3.4): a conforming SignedData carries its own signer certificate, so the ordinary build-then-verify flow needs nothing extra and the verdict reports `signatureVerified: true`. Where the signer is found nowhere the posture is fail-closed with a named opt-out — supply `certs` with the responder's certificate, or `allowUnverified: true`, in which case the verdict reports `signatureVerified: false`. Doing neither is refused, the opt-out never excuses a signature that is present and wrong, and a carrier with no signer at all is refused outright. The response's own `cmsSequence` and `otherMsgs` come back raw, since a request whose only arm was the other-message form has no certificate to return and §4.1 puts its answer there. Nothing is trusted: issued certificates are read from the CMS certificate bag (§4.2) and surfaced raw for `pki.path.validate`, and a Publish Trust Anchors control is surfaced with `trusted: false` rather than added to a store — `build`, `verify` |
|
|
267
267
|
| `pki.schema.cmc` | Decode CMC messages (RFC 5272 as updated by RFC 6402, the pair obsoleted by RFC 10002 / 10003 / 10004): a Full PKI Request (`PKIData`) or Full PKI Response (`PKIResponse`) riding inside a CMS SignedData, reached by the encapsulated content type (`id-cct-PKIData` / `id-cct-PKIResponse`). Controls are surfaced in wire order with their values raw, so an unrecognized one is data rather than a fault. Tagged requests decode across all three arms. The status verdicts (`CMCStatusInfo` v1 and `CMCStatusInfoV2`) are collected as an ordered list, and the RFC 6402 two-module `OtherStatusInfo` ambiguity — `pendInfo` and `extendedFailInfo` are both untagged SEQUENCEs in the 1988 module, told apart only by their first element — is resolved by inspection and refused when it cannot be told apart. Body-part identity is unique across the whole message rather than per sequence, 0 is reserved as the reference to the enclosing PKIData, and the `reqSequence` bytes are surfaced exactly as they appeared so an Identity Proof witness is computed over the wire bytes. A companion decoder for CMS content rather than an auto-routed format — `parse`, `parsePkiData`, `parsePkiResponse` |
|
|
268
268
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
|
|
269
|
-
| `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
|
|
269
|
+
| `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, the RFC 7633 §4.2.2 constraint that a certificate issued under a signing certificate advertising TLS features must offer the same set or a superset, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
|
|
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
|
-
| `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,
|
|
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, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, 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`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. 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
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` |
|
|
@@ -296,7 +296,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
296
296
|
the checkpoint read through `pki.tlog` and verified under the key the entry names in `logId.keyId`, so a signed tree root from one pinned log cannot satisfy an entry claiming another; the size and root the proof folds against come from that verified checkpoint rather than from the `inclusionProof` fields no signature covers, 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. A Rekor v2 `hashedrekord` v0.0.2 entry is read on either arm: Rekor v2 supports no DSSE entry type, so a DSSE signature is logged as a `hashedrekord` over the hash of its signing preimage, and the entry is bound by the signature it records, the certificate that made it, and that digest. Where the entry states `keyDetails`, that statement is held to the leaf key rather than read in its place. Rekor v2 issues no signed entry timestamp and writes `integratedTime` as zero, so it is ignored and `opts.tsaRoots` pins the timestamp-authority anchors for an RFC 3161 token over the signature bytes; with neither that nor `opts.time` the verdict is `sigstore/no-attested-time`, because a certificate that lives about ten minutes cannot be validated against an unattested instant. `timestampSource` names which of the three dated it, beside `logOrigin`, `treeSize` and `checkpointKeyId` from the verified checkpoint. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
|
|
297
297
|
| `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. Every format `pki.schema.all()` detects has a report and its own named verb, so a PKCS#8 key, a PKCS#12 store, a CRMF request, a CMP message, an EST CSR-attributes response, an RFC 5914 trust anchor list, an OCSP request or response, an RFC 3161 timestamp response and an RFC 5755 attribute certificate each read the same way a certificate does. No report prints private key material: a PKCS#8 and a PKCS#12 name the algorithm and say a private key is present, never its bytes, and a PKCS#12's encrypted safes are named rather than decrypted since these verbs take no password. `asn1(input)` is the structural dump below the reports, shaped after `openssl asn1parse`: one line per value carrying the byte offset, the nesting depth, the header and content lengths, the form, the tag name, and a primitive's rendered content, with an OBJECT IDENTIFIER named from the registry and a time rendered as the bytes on the wire. A conforming input is read by the same strict decoder the parsers use; when that decode refuses the bytes, the report names the refusal and a header walk renders the prefix it can read and the offset where it stopped, so a truncated, trailing-byte, indefinite-length or non-minimal-length file still reads. It withholds no value: where the other verbs here name a key and print none of it, this one prints bytes, private key material included, so a report that must not print a key is `any` or `pkcs8` — `certificate`, `crl`, `csr`, `cms`, `pkcs8`, `pkcs12`, `crmf`, `cmp`, `csrattrs`, `trustanchor`, `ocspRequest`, `ocspResponse`, `tsp`, `attrcert`, `asn1`, `any` |
|
|
298
298
|
| `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` |
|
|
299
|
-
| `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 two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, 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` |
|
|
299
|
+
| `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 two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; `rfc7633` reads the values of a TLS Feature extension, reporting a feature outside the 16-bit width of the IANA registry RFC 7633 §4.1 points at, a repeated feature, the transparency_info feature RFC 9162 §7.2 asks a CA not to issue, and the status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum, while a critical TLS Feature extension is reported by the default set; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, 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` |
|
|
300
300
|
| `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
|
|
301
301
|
| `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 |
|
|
302
302
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <file> [--asn1]`, `pki keygen --out <key>`, `pki csr --key <k> --subject <dn>`, `pki issue (--key <k> --subject <dn> \| --csr <req>)`, `pki fetch <https-url>`, `pki lint <file>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>`. `pki <verb> --help` prints that verb's usage on stdout and exits 0. Issuing from a request answers what the request asks for: `--copy-requested-san` writes the requested subjectAltName into the certificate, `--ignore-requested-extensions` issues without it, and a request asking for anything neither accounts for is refused rather than certified short |
|
package/lib/acme.js
CHANGED
|
@@ -63,6 +63,18 @@ var _Promise = intrinsic.Promise;
|
|
|
63
63
|
* origin with it. */
|
|
64
64
|
var _URL = intrinsic.URL;
|
|
65
65
|
var _urlCanParse = intrinsic.urlCanParse;
|
|
66
|
+
/** @internal And the accessors its result is read through, because capturing a constructor does not
|
|
67
|
+
* capture the prototype the value it returns reads its parts off: MEASURED on another module, a
|
|
68
|
+
* redefined `URL.prototype.href` answered every read of a destination and the request went to the
|
|
69
|
+
* host that getter named. Each part of a parsed URL here decides something, the scheme whether the
|
|
70
|
+
* request is allowed at all, the host whether a link relation is on the directory's origin. */
|
|
71
|
+
var _urlHref = intrinsic.urlHref;
|
|
72
|
+
var _urlOrigin = intrinsic.urlOrigin;
|
|
73
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
74
|
+
var _urlHostname = intrinsic.urlHostname;
|
|
75
|
+
var _urlPathname = intrinsic.urlPathname;
|
|
76
|
+
var _urlSearch = intrinsic.urlSearch;
|
|
77
|
+
var _urlSetPathname = intrinsic.urlSetPathname;
|
|
66
78
|
/** @internal A refusal returned as a REJECTED promise is a verdict too: read from the global binding, a
|
|
67
79
|
* replacement whose `reject` resolves instead turns the refusal into a pass. */
|
|
68
80
|
var _reject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
@@ -205,7 +217,8 @@ function _isUrl(v) {
|
|
|
205
217
|
var u;
|
|
206
218
|
try { u = new _URL(v); }
|
|
207
219
|
catch (_e) { return false; }
|
|
208
|
-
|
|
220
|
+
var scheme = _urlProtocol(u);
|
|
221
|
+
return (scheme === "http:" || scheme === "https:") && _urlHostname(u).length > 0;
|
|
209
222
|
}
|
|
210
223
|
function _isUriString(v) { return _isString(v) && _isUriShape(v); }
|
|
211
224
|
|
|
@@ -1448,7 +1461,14 @@ function _clientUrl(urlStr) {
|
|
|
1448
1461
|
var url;
|
|
1449
1462
|
try { url = new _URL(s); }
|
|
1450
1463
|
catch (e) { throw E("acme/bad-url", "the ACME URL did not parse: " + s, e); }
|
|
1451
|
-
|
|
1464
|
+
/** @internal Each part read ONCE through the captured accessor, and every check below made from the
|
|
1465
|
+
* local: the canonical-form checks compare the raw text against what the parser reports, so a part
|
|
1466
|
+
* that answered two reads differently would let one of those checks approve text the other saw. */
|
|
1467
|
+
var urlScheme = _urlProtocol(url);
|
|
1468
|
+
var urlHost = _urlHostname(url);
|
|
1469
|
+
var urlPath = _urlPathname(url);
|
|
1470
|
+
var urlQuery = _urlSearch(url);
|
|
1471
|
+
if (urlScheme !== "https:") throw E("acme/insecure-url", "ACME requires https (RFC 8555 sec. 6.1), got " + urlScheme + " for " + s);
|
|
1452
1472
|
var _uAuth = _uriAuthority(s, true);
|
|
1453
1473
|
if (intrinsic.stringIndexOf(_uAuth ? _uAuth.authority : "", "@") !== -1) throw E("acme/bad-url", "an ACME URL must not contain userinfo: " + JSON.stringify(s));
|
|
1454
1474
|
if (_hasWsOrBackslash(s)) throw E("acme/bad-url", "an ACME URL must not contain whitespace or a backslash: " + JSON.stringify(s));
|
|
@@ -1457,14 +1477,14 @@ function _clientUrl(urlStr) {
|
|
|
1457
1477
|
if (_auth) {
|
|
1458
1478
|
var _hp = _auth.authority.slice(_auth.authority.lastIndexOf("@") + 1);
|
|
1459
1479
|
var _rawHost = _hp.charAt(0) === "[" ? _hp.slice(0, _hp.indexOf("]") + 1) : _hp.split(":")[0];
|
|
1460
|
-
if (_rawHost.toLowerCase() !==
|
|
1480
|
+
if (_rawHost.toLowerCase() !== urlHost) throw E("acme/bad-url", "an ACME URL host is not canonical (the transport would rewrite it): " + JSON.stringify(s));
|
|
1461
1481
|
}
|
|
1462
1482
|
var _pathStart = _rawPathStart(s);
|
|
1463
1483
|
var rawPath = _strSlice(s, _pathStart, _firstDelim(s, _pathStart, 0x3f, 0x23, -1));
|
|
1464
|
-
if (rawPath !==
|
|
1484
|
+
if (rawPath !== urlPath) throw E("acme/bad-url", "an ACME URL path is not canonical (the transport would normalize it): " + JSON.stringify(s));
|
|
1465
1485
|
var qi = s.indexOf("?");
|
|
1466
1486
|
var rawQuery = qi === -1 ? "" : s.slice(qi).split("#")[0];
|
|
1467
|
-
if (rawQuery !==
|
|
1487
|
+
if (rawQuery !== urlQuery) throw E("acme/bad-url", "an ACME URL query is not canonical (the transport would normalize it): " + JSON.stringify(s));
|
|
1468
1488
|
if (s.indexOf("#") !== -1) throw E("acme/bad-url", "an ACME URL must not contain a fragment: " + JSON.stringify(s));
|
|
1469
1489
|
return s;
|
|
1470
1490
|
}
|
|
@@ -1477,11 +1497,11 @@ function _resolveLocation(loc, base) {
|
|
|
1477
1497
|
try { u = new _URL(loc, base); }
|
|
1478
1498
|
catch (e) { throw E("acme/bad-url", "the Location header did not resolve to a valid URL: " + JSON.stringify(loc), e); }
|
|
1479
1499
|
if (_queryRepaired(loc, u)) throw E("acme/bad-url", "a relative Link/Location query is not canonical (resolution re-encoded it): " + JSON.stringify(loc));
|
|
1480
|
-
return _clientUrl(u
|
|
1500
|
+
return _clientUrl(_urlHref(u));
|
|
1481
1501
|
}
|
|
1482
1502
|
function _queryRepaired(rawRef, resolvedUrl) {
|
|
1483
1503
|
var qi = rawRef.indexOf("?");
|
|
1484
|
-
return qi !== -1 && rawRef.slice(qi).split("#")[0] !== resolvedUrl
|
|
1504
|
+
return qi !== -1 && rawRef.slice(qi).split("#")[0] !== _urlSearch(resolvedUrl);
|
|
1485
1505
|
}
|
|
1486
1506
|
var _defaultSleep = require("./sleep").sleep;
|
|
1487
1507
|
/** @internal The settings this client forwards to its transport. */
|
|
@@ -1763,7 +1783,7 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
1763
1783
|
if (hNames[hi].toLowerCase() === "link") { raw = headers[hNames[hi]]; break; }
|
|
1764
1784
|
}
|
|
1765
1785
|
if (raw == null) return [];
|
|
1766
|
-
var baseUrl = new _URL(base), baseOrigin = baseUrl
|
|
1786
|
+
var baseUrl = new _URL(base), baseOrigin = _urlOrigin(baseUrl), baseHref = _urlHref(baseUrl);
|
|
1767
1787
|
var baseKey = _dedupKey(baseHref);
|
|
1768
1788
|
var fields = Array.isArray(raw) ? raw : [raw], out = [], seen = Object.create(null), distinctCount = 0, totalBytes = 0;
|
|
1769
1789
|
for (var fi = 0; fi < fields.length; fi++) {
|
|
@@ -1786,7 +1806,7 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
1786
1806
|
try { au = new _URL(lv.anchor, base); }
|
|
1787
1807
|
catch (_ae) { continue; }
|
|
1788
1808
|
if (_queryRepaired(lv.anchor, au)) continue;
|
|
1789
|
-
if (au
|
|
1809
|
+
if (_urlHref(au) !== baseHref) continue;
|
|
1790
1810
|
}
|
|
1791
1811
|
var resolved;
|
|
1792
1812
|
try { resolved = _resolveLocation(lv.uri, base); }
|
|
@@ -1796,8 +1816,8 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
1796
1816
|
continue;
|
|
1797
1817
|
}
|
|
1798
1818
|
var parsed = new _URL(resolved);
|
|
1799
|
-
if (parsed
|
|
1800
|
-
var key = _dedupKey(parsed
|
|
1819
|
+
if (_urlOrigin(parsed) !== baseOrigin) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not on the request URL's origin (SSRF guard): " + JSON.stringify(lv.uri));
|
|
1820
|
+
var key = _dedupKey(_urlHref(parsed));
|
|
1801
1821
|
if (seen[key]) continue;
|
|
1802
1822
|
seen[key] = true;
|
|
1803
1823
|
distinctCount += 1;
|
|
@@ -2371,8 +2391,8 @@ function client(directoryUrl, opts) {
|
|
|
2371
2391
|
// allow:nan-date-comparison-unguarded -- notAfterMs is a codec-parsed cert date; clk() is validated finite.
|
|
2372
2392
|
if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate expired before the RenewalInfo request could be issued; a client MUST NOT check RenewalInfo after expiry (RFC 9773 sec. 4.3)");
|
|
2373
2393
|
var u = new _URL(base);
|
|
2374
|
-
u
|
|
2375
|
-
var url = _clientUrl(u
|
|
2394
|
+
_urlSetPathname(u, _stripTrailingSlash(_urlPathname(u)) + "/" + certId);
|
|
2395
|
+
var url = _clientUrl(_urlHref(u));
|
|
2376
2396
|
return _send("GET", url, { accept: "application/json" }, null).then(function (res) {
|
|
2377
2397
|
if (res.status !== 200) throw _serverProblem(res);
|
|
2378
2398
|
var obj = validateRenewalInfo(_json(res));
|
package/lib/cmp-build.js
CHANGED
|
@@ -38,6 +38,11 @@ var _Promise = intrinsic.Promise;
|
|
|
38
38
|
* conversion it is handed is taken with it, since a replacement answers with a host of its own. */
|
|
39
39
|
var _URL = intrinsic.URL;
|
|
40
40
|
var _stringOf = intrinsic.String;
|
|
41
|
+
/** @internal And the accessors its result is read through: the constructor's capture does not cover
|
|
42
|
+
* the prototype the value reads its parts off. */
|
|
43
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
44
|
+
var _urlSearch = intrinsic.urlSearch;
|
|
45
|
+
var _urlHash = intrinsic.urlHash;
|
|
41
46
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
42
47
|
var _hasOwn = intrinsic.hasOwn;
|
|
43
48
|
var _isArray = intrinsic.isArray;
|
|
@@ -1127,8 +1132,9 @@ function wellKnownUrl(base, opts) {
|
|
|
1127
1132
|
var u;
|
|
1128
1133
|
try { u = new _URL(_stringOf(base)); }
|
|
1129
1134
|
catch (e) { throw _err("cmp/bad-url", "the CMP base URL did not parse: " + guard.text.showValue(base), e); }
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1135
|
+
var baseScheme = _urlProtocol(u);
|
|
1136
|
+
if (baseScheme !== "https:" && baseScheme !== "http:") throw _err("cmp/bad-url", "the CMP base URL must be http or https (a well-known URI is authority-rooted over HTTP), got " + baseScheme + ": " + JSON.stringify(String(base)));
|
|
1137
|
+
if (_urlSearch(u) || _urlHash(u)) throw _err("cmp/bad-url", "the CMP base URL must not carry a query or fragment component (RFC 9811 sec. 3.4)");
|
|
1132
1138
|
if (_strIndexOf(String(base), "\\") !== -1) throw _err("cmp/bad-url", "the CMP base URL must not contain a backslash (WHATWG rewrites it to a path separator): " + JSON.stringify(String(base)));
|
|
1133
1139
|
var rawBasePath = _beforeQueryOrFragment(_stripSchemeAuthority(String(base)));
|
|
1134
1140
|
if (rawBasePath !== "" && rawBasePath !== "/") throw _err("cmp/bad-url", "an RFC 8615 well-known URI is authority-rooted; the CMP base URL must have no path component (pass a full operation URL to transfer instead): " + JSON.stringify(String(base)));
|
|
@@ -1138,7 +1144,7 @@ function wellKnownUrl(base, opts) {
|
|
|
1138
1144
|
guard.list.append(segs, _wellKnownSeg(opts.label, "label"));
|
|
1139
1145
|
}
|
|
1140
1146
|
if (opts.operation != null) guard.list.append(segs, _wellKnownSeg(opts.operation, "operation"));
|
|
1141
|
-
return u
|
|
1147
|
+
return intrinsic.urlOrigin(u) + "/" + intrinsic.join(segs, "/");
|
|
1142
1148
|
}
|
|
1143
1149
|
|
|
1144
1150
|
/**
|
package/lib/constants.js
CHANGED
|
@@ -273,8 +273,9 @@ var NAMES = Object.assign(Object.create(null), {
|
|
|
273
273
|
"8": "removeFromCRL", "9": "privilegeWithdrawn", "10": "aACompromise",
|
|
274
274
|
},
|
|
275
275
|
OCSP_STATUS: { "0": "successful", "1": "malformedRequest", "2": "internalError", "3": "tryLater", "5": "sigRequired", "6": "unauthorized" },
|
|
276
|
-
/** @internal The TLS ExtensionType values
|
|
277
|
-
*
|
|
276
|
+
/** @internal The TLS ExtensionType values this build names for the TLS feature extension. RFC 7633
|
|
277
|
+
* sec. 4 declares the syntax and sec. 4.1 points at the IANA TLS Extensions registry for the values
|
|
278
|
+
* themselves, so neither clause lists one. A value outside this table is printed as its number. */
|
|
278
279
|
TLS_FEATURE: { "5": "status_request", "17": "status_request_v2" },
|
|
279
280
|
/** @internal The IANA Address Family Numbers RFC 3779 sec. 2.2.3.3 names. */
|
|
280
281
|
ADDRESS_FAMILY: { "1": "IPv4", "2": "IPv6" },
|
package/lib/est.js
CHANGED
|
@@ -85,6 +85,15 @@ var _Promise = intrinsic.Promise;
|
|
|
85
85
|
* is handed is taken with it, since a replacement answers with a host of its own. */
|
|
86
86
|
var _URL = intrinsic.URL;
|
|
87
87
|
var _stringOf = intrinsic.String;
|
|
88
|
+
/** @internal And the accessors its result is read through: capturing the constructor does not capture
|
|
89
|
+
* the prototype the value reads its parts off, and each part here decides something, the scheme
|
|
90
|
+
* whether the request is allowed, the origin whether credentials may still be sent after a redirect. */
|
|
91
|
+
var _urlHref = intrinsic.urlHref;
|
|
92
|
+
var _urlOrigin = intrinsic.urlOrigin;
|
|
93
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
94
|
+
var _urlPathname = intrinsic.urlPathname;
|
|
95
|
+
var _urlSearch = intrinsic.urlSearch;
|
|
96
|
+
var _urlHash = intrinsic.urlHash;
|
|
88
97
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
89
98
|
var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
90
99
|
var pkix = require("./schema-pkix");
|
|
@@ -810,7 +819,8 @@ function _parseUrl(urlStr) {
|
|
|
810
819
|
var url;
|
|
811
820
|
try { url = new _URL(_stringOf(urlStr)); }
|
|
812
821
|
catch (e) { throw E("est/bad-url", "the EST server URL did not parse: " + String(urlStr), e); }
|
|
813
|
-
|
|
822
|
+
var scheme = _urlProtocol(url);
|
|
823
|
+
if (scheme !== "https:") throw E("est/insecure-url", "EST requires https (RFC 7030 sec. 3.3), got " + scheme + " for " + urlStr);
|
|
814
824
|
return url;
|
|
815
825
|
}
|
|
816
826
|
|
|
@@ -823,11 +833,12 @@ function _tlsForRequest(opts) {
|
|
|
823
833
|
function _redirectTarget(current, location, method, opts) {
|
|
824
834
|
if (location == null || String(location).trim() === "") throw E("est/http-error", "a redirect response carried no Location header");
|
|
825
835
|
var resolved;
|
|
826
|
-
try { resolved = new _URL(_stringOf(location), current
|
|
836
|
+
try { resolved = new _URL(_stringOf(location), _urlHref(current)); }
|
|
827
837
|
catch (e) { throw E("est/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
828
|
-
|
|
838
|
+
var resolvedScheme = _urlProtocol(resolved);
|
|
839
|
+
if (resolvedScheme !== "https:") throw E("est/insecure-redirect", "a redirect to a non-https URL is refused (RFC 7030 sec. 3.2.1): " + resolvedScheme);
|
|
829
840
|
var safeMethod = method === "GET" || method === "HEAD";
|
|
830
|
-
if (resolved
|
|
841
|
+
if (_urlOrigin(resolved) !== _urlOrigin(current) && !safeMethod && !opts.allowCrossOriginRedirect) {
|
|
831
842
|
throw E("est/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect (RFC 7030 sec. 3.2.1)");
|
|
832
843
|
}
|
|
833
844
|
return resolved;
|
|
@@ -890,10 +901,10 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
890
901
|
var staleRetries = 0;
|
|
891
902
|
var authSpaces = 0;
|
|
892
903
|
var lastDigestNonce = null;
|
|
893
|
-
var initialOrigin = url
|
|
904
|
+
var initialOrigin = _urlOrigin(url);
|
|
894
905
|
function _tlsFor(u) {
|
|
895
906
|
var t = budgets.tls;
|
|
896
|
-
if (t && u
|
|
907
|
+
if (t && _urlOrigin(u) !== initialOrigin) {
|
|
897
908
|
t = Object.assign({}, t);
|
|
898
909
|
t.cert = null; t.key = null; t.servername = null;
|
|
899
910
|
}
|
|
@@ -905,20 +916,26 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
905
916
|
var digestSent = false;
|
|
906
917
|
function _headersFor(u) {
|
|
907
918
|
digestSent = false;
|
|
908
|
-
|
|
919
|
+
/** @internal The origin and the request target are each read ONCE and both the protection-space
|
|
920
|
+
* test and the answer are built from those locals: the credential is bound to the target it is
|
|
921
|
+
* computed over, so a part answering two reads differently would answer for one target and be
|
|
922
|
+
* sent to another. */
|
|
923
|
+
var uOrigin = _urlOrigin(u);
|
|
924
|
+
var uTarget = _urlPathname(u) + _urlSearch(u);
|
|
925
|
+
if (uOrigin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
|
|
909
926
|
var hh = guard.identifier.ownOptions(headers);
|
|
910
927
|
if (digestChallenge) {
|
|
911
|
-
if (!httpDigest.inProtectionSpace(digestChallenge,
|
|
928
|
+
if (!httpDigest.inProtectionSpace(digestChallenge, uOrigin, uTarget)) return headers;
|
|
912
929
|
var nkey = digestChallenge.nonce;
|
|
913
930
|
digestNcByNonce[nkey] = (digestNcByNonce[nkey] || 0) + 1;
|
|
914
|
-
hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri:
|
|
931
|
+
hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri: uTarget, username: _authUser(opts), password: _authPass(opts), body: body, nc: digestNcByNonce[nkey], policy: _digestPolicy(opts) }, E);
|
|
915
932
|
digestSent = true;
|
|
916
933
|
} else { hh.authorization = authValue; }
|
|
917
934
|
return hh;
|
|
918
935
|
}
|
|
919
936
|
function step() {
|
|
920
937
|
var hopBody = (typeof body === "function" && budgets.onAttempt) ? budgets.onAttempt() : body;
|
|
921
|
-
return guard.async.invoked(transport, [{ method: method, url: url
|
|
938
|
+
return guard.async.invoked(transport, [{ method: method, url: _urlHref(url), headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }], E, "est/bad-input", "opts.transport").then(function (res) {
|
|
922
939
|
res = res || {};
|
|
923
940
|
var blen = _bodyLen(res);
|
|
924
941
|
if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
|
|
@@ -939,7 +956,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
939
956
|
return step();
|
|
940
957
|
}
|
|
941
958
|
if (status === 401) {
|
|
942
|
-
if (url
|
|
959
|
+
if (_urlOrigin(url) !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
|
|
943
960
|
var www = String(h["www-authenticate"] || "");
|
|
944
961
|
if (www.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E("est/auth-required", "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap (RFC 7030 sec. 3.2.3)");
|
|
945
962
|
var scheme = _authScheme(opts);
|
|
@@ -951,8 +968,8 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
951
968
|
selPol.preferStale = digestSent;
|
|
952
969
|
selPol.priorNonce = lastDigestNonce;
|
|
953
970
|
selPol.priorRealm = digestChallenge && digestChallenge.realm;
|
|
954
|
-
selPol.requestOrigin = url
|
|
955
|
-
selPol.requestTarget = url
|
|
971
|
+
selPol.requestOrigin = _urlOrigin(url);
|
|
972
|
+
selPol.requestTarget = _urlPathname(url) + _urlSearch(url);
|
|
956
973
|
var ch = httpDigest.parseChallenge(www, E, "est/digest-bad-challenge", selPol);
|
|
957
974
|
if (!ch) throw E("est/auth-required", "the server offered no usable Digest challenge: " + www);
|
|
958
975
|
if (digestSent && digestChallenge && ch.realm === digestChallenge.realm) {
|
|
@@ -988,7 +1005,7 @@ function _client(op, method, baseUrl, body, headers, opts, proxySnap, onAttempt,
|
|
|
988
1005
|
var base;
|
|
989
1006
|
try { base = new _URL(_stringOf(baseUrl)); }
|
|
990
1007
|
catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + guard.text.showValue(baseUrl), e); }
|
|
991
|
-
if (base
|
|
1008
|
+
if (_urlSearch(base) || _urlHash(base)) throw E("est/bad-url", "the EST base URL must not carry a query or fragment component (RFC 7030 sec. 3.2.2)");
|
|
992
1009
|
var url = _parseUrl(paths(baseUrl, { label: opts.label })[op]);
|
|
993
1010
|
var transport = checked !== undefined ? checked : _assertTransport(opts);
|
|
994
1011
|
if (!transport) {
|
package/lib/guard-intrinsic.js
CHANGED
|
@@ -85,6 +85,7 @@ var _urlOrigin = getter(URL.prototype, "origin", "URL.prototype");
|
|
|
85
85
|
var _urlProtocol = getter(URL.prototype, "protocol", "URL.prototype");
|
|
86
86
|
var _urlPathname = getter(URL.prototype, "pathname", "URL.prototype");
|
|
87
87
|
var _urlSearch = getter(URL.prototype, "search", "URL.prototype");
|
|
88
|
+
var _urlHash = getter(URL.prototype, "hash", "URL.prototype");
|
|
88
89
|
var _urlHostname = getter(URL.prototype, "hostname", "URL.prototype");
|
|
89
90
|
var _urlPort = getter(URL.prototype, "port", "URL.prototype");
|
|
90
91
|
var _urlUsername = getter(URL.prototype, "username", "URL.prototype");
|
|
@@ -92,6 +93,7 @@ var _urlPassword = getter(URL.prototype, "password", "URL.prototype");
|
|
|
92
93
|
var _urlSearchParams = getter(URL.prototype, "searchParams", "URL.prototype");
|
|
93
94
|
var _urlSetPathname = setter(URL.prototype, "pathname", "URL.prototype");
|
|
94
95
|
var _urlSetSearch = setter(URL.prototype, "search", "URL.prototype");
|
|
96
|
+
var _urlSetHash = setter(URL.prototype, "hash", "URL.prototype");
|
|
95
97
|
var _asyncIterator = Symbol.asyncIterator;
|
|
96
98
|
|
|
97
99
|
var _arrForEach = uncurry(Array.prototype.forEach);
|
|
@@ -275,6 +277,7 @@ module.exports = Object.freeze({
|
|
|
275
277
|
urlProtocol: _urlProtocol,
|
|
276
278
|
urlPathname: _urlPathname,
|
|
277
279
|
urlSearch: _urlSearch,
|
|
280
|
+
urlHash: _urlHash,
|
|
278
281
|
urlHostname: _urlHostname,
|
|
279
282
|
urlPort: _urlPort,
|
|
280
283
|
urlUsername: _urlUsername,
|
|
@@ -282,6 +285,7 @@ module.exports = Object.freeze({
|
|
|
282
285
|
urlSearchParams: _urlSearchParams,
|
|
283
286
|
urlSetPathname: _urlSetPathname,
|
|
284
287
|
urlSetSearch: _urlSetSearch,
|
|
288
|
+
urlSetHash: _urlSetHash,
|
|
285
289
|
asyncIterator: _asyncIterator,
|
|
286
290
|
forEach: _arrForEach,
|
|
287
291
|
map: _arrMap,
|
package/lib/http-digest.js
CHANGED
|
@@ -7,8 +7,13 @@ var crypto = require("crypto");
|
|
|
7
7
|
var constants = require("./constants");
|
|
8
8
|
var intrinsic = require("./guard-intrinsic");
|
|
9
9
|
var _hasOwn = intrinsic.hasOwn;
|
|
10
|
-
/** @internal The URL parser, taken at load: the origin it reports is what a challenge is bound to.
|
|
10
|
+
/** @internal The URL parser, taken at load: the origin it reports is what a challenge is bound to.
|
|
11
|
+
* Its accessors are taken with it, since the constructor's capture does not cover the prototype the
|
|
12
|
+
* value reads its parts off and this origin is what a credential is scoped by. */
|
|
11
13
|
var _URL = intrinsic.URL;
|
|
14
|
+
var _urlOrigin = intrinsic.urlOrigin;
|
|
15
|
+
var _urlPathname = intrinsic.urlPathname;
|
|
16
|
+
var _urlSearch = intrinsic.urlSearch;
|
|
12
17
|
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
13
18
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
14
19
|
var _fromCharCode = intrinsic.fromCharCode;
|
|
@@ -295,7 +300,7 @@ function answer(challenge, params, E) {
|
|
|
295
300
|
function _domainEntry(d) {
|
|
296
301
|
d = String(d == null ? "" : d);
|
|
297
302
|
if (d.charAt(0) === "/") return { origin: null, full: d };
|
|
298
|
-
try { var u = new _URL(d); return { origin: u.
|
|
303
|
+
try { var u = new _URL(d); return { origin: _urlOrigin(u).toLowerCase(), full: (_urlPathname(u) || "/") + (_urlSearch(u) || "") }; }
|
|
299
304
|
catch (_e) { return null; }
|
|
300
305
|
}
|
|
301
306
|
|
package/lib/inspect.js
CHANGED
|
@@ -449,8 +449,9 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
|
|
|
449
449
|
}
|
|
450
450
|
return akiLines.length ? akiLines.join("\n") : inner + "keyid:(none)";
|
|
451
451
|
},
|
|
452
|
-
/** @internal RFC 7633 sec.
|
|
453
|
-
* where the toolkit knows it and printed as its number where it does not.
|
|
452
|
+
/** @internal RFC 7633 sec. 4.1 binds these values to the IANA TLS Extensions registry, so each is
|
|
453
|
+
* named from the registry where the toolkit knows it and printed as its number where it does not.
|
|
454
|
+
* sec. 6 is IANA Considerations and registers the extension's own OID; it carries no value. */
|
|
454
455
|
tlsFeature: function (decoded, inner) {
|
|
455
456
|
return (decoded.features || []).map(function (f) {
|
|
456
457
|
var text = String(f);
|
package/lib/lint.js
CHANGED
|
@@ -303,9 +303,16 @@ var OID_CPS = oid.byName("cps");
|
|
|
303
303
|
/** @internal The extensions RFC 5280 says a conforming CA SHOULD mark non-critical, keyed by OID
|
|
304
304
|
* with the clause; the MUSTs live in the shared CERT_FIXED_CRITICALITY table. The subjectAltName
|
|
305
305
|
* and extKeyUsage recommendations are conditional and have their own rules. */
|
|
306
|
+
/** @internal The per-extension citation travels with the finding, so a row here may come from a
|
|
307
|
+
* document other than RFC 5280: RFC 7633 sec. 4 states the same shape of recommendation for the TLS
|
|
308
|
+
* feature extension, and its clause closes by naming the case where critical is what the caller
|
|
309
|
+
* wants, so this grades it rather than refusing it. Adding a row here grades the extension in the
|
|
310
|
+
* DEFAULT set, which a named profile would not; it does not feed `_isUnknownExtension`, so the
|
|
311
|
+
* unknown-critical verdict a critical one draws is unaffected. */
|
|
306
312
|
var CERT_RECOMMENDED_NON_CRITICAL = Object.create(null);
|
|
307
313
|
CERT_RECOMMENDED_NON_CRITICAL[oid.byName("issuerAltName")] = "RFC 5280 sec. 4.2.1.7";
|
|
308
314
|
CERT_RECOMMENDED_NON_CRITICAL[oid.byName("cRLDistributionPoints")] = "RFC 5280 sec. 4.2.1.13";
|
|
315
|
+
CERT_RECOMMENDED_NON_CRITICAL[oid.byName("tlsFeature")] = "RFC 7633 sec. 4";
|
|
309
316
|
var _T_BMP = asn1.TAGS.BMP_STRING, _T_VISIBLE = asn1.TAGS.VISIBLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
|
|
310
317
|
function _hasControlChar(str) {
|
|
311
318
|
for (var i = 0; i < str.length; i++) {
|
|
@@ -488,7 +495,7 @@ var RFC5280_RULES = [
|
|
|
488
495
|
_criticalityRule("keyUsage", "lint/rfc5280/key-usage-not-critical", "warn",
|
|
489
496
|
"RFC 5280 4.2.1.3", "the keyUsage extension should be marked critical"),
|
|
490
497
|
{
|
|
491
|
-
id: "lint/rfc5280/recommended-criticality", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.7 / 4.2.1.13",
|
|
498
|
+
id: "lint/rfc5280/recommended-criticality", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.7 / 4.2.1.13, RFC 7633 sec. 4",
|
|
492
499
|
message: "an extension a conforming CA should mark non-critical is marked critical",
|
|
493
500
|
check: function (cert) {
|
|
494
501
|
var rows = [];
|
|
@@ -4056,6 +4063,96 @@ var RFC9763_RULES = [
|
|
|
4056
4063
|
},
|
|
4057
4064
|
];
|
|
4058
4065
|
|
|
4066
|
+
/** @internal RFC 7633. The criticality recommendation lives in the always-on table above, because
|
|
4067
|
+
* sec. 4 states it for every certificate carrying the extension. These rows read the VALUE, which
|
|
4068
|
+
* RFC 7633 leaves open in three ways and another document speaks to in a fourth:
|
|
4069
|
+
*
|
|
4070
|
+
* sec. 4 writes `Features ::= SEQUENCE OF INTEGER` with no range, no ordering rule and no prohibition
|
|
4071
|
+
* on repeats, so none of those is a conformance failure and none is refused by the encoder. What
|
|
4072
|
+
* sec. 4.1 does say is that the values are "TLS extension identifiers ... as specified in the IANA
|
|
4073
|
+
* Transport Layer Security (TLS) Extensions registry", and that registry is 16 bits wide, so a value
|
|
4074
|
+
* outside 0..65535 names nothing in it. That is a `warn`, not an `error`: the finding rests on a
|
|
4075
|
+
* reference to a registry rather than on a syntax rule, and at `error` in a named profile it would
|
|
4076
|
+
* make `pki.x509.sign` refuse to emit the certificate.
|
|
4077
|
+
*
|
|
4078
|
+
* The transparency_info row is RFC 9162 sec. 7.2, read directly: "A certification authority SHOULD
|
|
4079
|
+
* NOT issue any certificate that identifies the transparency_info TLS extension in a TLS feature
|
|
4080
|
+
* extension [RFC7633], because TLS servers are not required to support the transparency_info TLS
|
|
4081
|
+
* extension in order to participate in CT". That is the only clause outside RFC 7633 that constrains
|
|
4082
|
+
* this field.
|
|
4083
|
+
*
|
|
4084
|
+
* There is NO deprecation row for status_request_v2, and the absence is deliberate. The IANA registry
|
|
4085
|
+
* still lists `17 status_request_v2 [RFC 6961]` as Recommended and marks it neither obsolete nor
|
|
4086
|
+
* deprecated; RFC 9846 contains no occurrence of "status_request_v2" and none of "7633", so no
|
|
4087
|
+
* document states a deprecation to cite. What is true is narrower: RFC 9846 obsoletes RFC 6961, which
|
|
4088
|
+
* defined the extension, and the ExtensionType enum at its sec. 4.3 does not list 17 at all, so a
|
|
4089
|
+
* certificate naming feature 17 asks a TLS 1.3 server for something that version does not define
|
|
4090
|
+
* while a TLS 1.2 deployment can still satisfy it. That is reported at `notice`, with the clause that
|
|
4091
|
+
* supports it and nothing more. */
|
|
4092
|
+
var RFC7633_RULES = [
|
|
4093
|
+
{
|
|
4094
|
+
id: "lint/rfc7633/feature-outside-registry-width", severity: "warn", source: "rfc7633", citation: "RFC 7633 4.1",
|
|
4095
|
+
message: "a TLS feature value lies outside the 16-bit width of the IANA TLS ExtensionType registry sec. 4.1 points at, so it names no extension in it",
|
|
4096
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
|
|
4097
|
+
check: function (cert, ctx) {
|
|
4098
|
+
var d = ctx.decode("tlsFeature");
|
|
4099
|
+
var features = (d && d.value && d.value.features) || [];
|
|
4100
|
+
var rows = [];
|
|
4101
|
+
for (var i = 0; i < features.length; i++) {
|
|
4102
|
+
var f = features[i];
|
|
4103
|
+
if (f < 0n || f > 65535n) rows.push({ context: { feature: String(f), width: "0..65535" } });
|
|
4104
|
+
}
|
|
4105
|
+
return rows.length ? rows : null;
|
|
4106
|
+
},
|
|
4107
|
+
},
|
|
4108
|
+
{
|
|
4109
|
+
id: "lint/rfc7633/feature-repeated", severity: "notice", source: "rfc7633", citation: "RFC 7633 4",
|
|
4110
|
+
message: "a TLS feature value appears more than once; the syntax permits it and the set it names is the same, so the repeat carries no meaning",
|
|
4111
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
|
|
4112
|
+
check: function (cert, ctx) {
|
|
4113
|
+
var d = ctx.decode("tlsFeature");
|
|
4114
|
+
var features = (d && d.value && d.value.features) || [];
|
|
4115
|
+
/** @internal One pass with a set of what has been seen, not a rescan of every preceding value.
|
|
4116
|
+
* This rule is in the default set and a certificate reaching it is unauthenticated, so the
|
|
4117
|
+
* pairwise form is work the input chooses: MEASURED, 20000 distinct features in an 80 KiB
|
|
4118
|
+
* certificate took 355 ms against 5 ms for 50 of them. A set compares the bigint values. */
|
|
4119
|
+
var seen = new Set();
|
|
4120
|
+
var rows = [];
|
|
4121
|
+
for (var i = 0; i < features.length; i++) {
|
|
4122
|
+
if (seen.has(features[i])) rows.push({ context: { feature: String(features[i]) } });
|
|
4123
|
+
else seen.add(features[i]);
|
|
4124
|
+
}
|
|
4125
|
+
return rows.length ? rows : null;
|
|
4126
|
+
},
|
|
4127
|
+
},
|
|
4128
|
+
{
|
|
4129
|
+
id: "lint/rfc7633/transparency-info-feature", severity: "warn", source: "rfc7633", citation: "RFC 9162 7.2",
|
|
4130
|
+
message: "a certification authority should not issue a certificate naming the transparency_info TLS extension (52) in a TLS feature extension, because a TLS server need not support it to participate in certificate transparency",
|
|
4131
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
|
|
4132
|
+
check: function (cert, ctx) {
|
|
4133
|
+
var d = ctx.decode("tlsFeature");
|
|
4134
|
+
var features = (d && d.value && d.value.features) || [];
|
|
4135
|
+
for (var i = 0; i < features.length; i++) {
|
|
4136
|
+
if (features[i] === 52n) return { context: { feature: "52", name: "transparency_info" } };
|
|
4137
|
+
}
|
|
4138
|
+
return null;
|
|
4139
|
+
},
|
|
4140
|
+
},
|
|
4141
|
+
{
|
|
4142
|
+
id: "lint/rfc7633/feature-undefined-in-tls13", severity: "notice", source: "rfc7633", citation: "RFC 9846 4.3",
|
|
4143
|
+
message: "a TLS feature value names status_request_v2 (17), which RFC 9846 does not list in its ExtensionType enum and whose defining document RFC 6961 it obsoletes, so a TLS 1.3 server cannot satisfy it",
|
|
4144
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
|
|
4145
|
+
check: function (cert, ctx) {
|
|
4146
|
+
var d = ctx.decode("tlsFeature");
|
|
4147
|
+
var features = (d && d.value && d.value.features) || [];
|
|
4148
|
+
for (var i = 0; i < features.length; i++) {
|
|
4149
|
+
if (features[i] === 17n) return { context: { feature: "17", name: "status_request_v2", obsoletedBy: "RFC 9846" } };
|
|
4150
|
+
}
|
|
4151
|
+
return null;
|
|
4152
|
+
},
|
|
4153
|
+
},
|
|
4154
|
+
];
|
|
4155
|
+
|
|
4059
4156
|
/** @internal RFC 5280 sec. 5 states the criticality of each CRL extension it profiles, and sec. 5.3
|
|
4060
4157
|
* does the same for the entry extensions. certificateIssuer is critical (sec. 5.3.3) because a
|
|
4061
4158
|
* consumer that ignores it attributes an entry to the wrong issuer. */
|
|
@@ -6413,6 +6510,7 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
6413
6510
|
"rfc9909": RFC9909_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6414
6511
|
"rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6415
6512
|
"rfc9763": RFC9763_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6513
|
+
"rfc7633": RFC7633_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6416
6514
|
"x509-altsig": X509_ALTSIG_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6417
6515
|
"cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6418
6516
|
"cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
@@ -6436,7 +6534,7 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
6436
6534
|
.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6437
6535
|
});
|
|
6438
6536
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
6439
|
-
.concat(RFC9763_RULES).concat(X509_ALTSIG_RULES)
|
|
6537
|
+
.concat(RFC9763_RULES).concat(RFC7633_RULES).concat(X509_ALTSIG_RULES)
|
|
6440
6538
|
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
|
|
6441
6539
|
/** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
|
|
6442
6540
|
* inert under the default set because each one asks whether the caller named the profile. */
|
|
@@ -6725,6 +6823,11 @@ function _applyThreshold(report, severity) {
|
|
|
6725
6823
|
* id-kp-serverAuth and is not a CA. `"nist-pqc-transition"` and `"cnsa-2.0"` are
|
|
6726
6824
|
* named the same way: which algorithm policy a certificate is held to is the
|
|
6727
6825
|
* caller's assertion, so neither is detected and both are inert until named.
|
|
6826
|
+
* `"rfc7633"` adds the rows that read a TLS Feature extension's VALUES: a feature
|
|
6827
|
+
* outside the 16-bit width of the registry sec. 4.1 points at, a repeated feature,
|
|
6828
|
+
* the transparency_info feature RFC 9162 sec. 7.2 asks a CA not to issue, and the
|
|
6829
|
+
* status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum. The
|
|
6830
|
+
* criticality of that extension is graded by default and needs no profile.
|
|
6728
6831
|
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
|
|
6729
6832
|
* `worst` always reflect the complete, unfiltered result.
|
|
6730
6833
|
* @example
|
package/lib/path-validate.js
CHANGED
|
@@ -68,8 +68,14 @@ var _reject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
|
68
68
|
* not a refusal, and this module's verdicts are what a caller acts on. */
|
|
69
69
|
var _Promise = intrinsic.Promise;
|
|
70
70
|
/** @internal The URL parser, taken at load: the host it reports from an access descriptor is the host a
|
|
71
|
-
* revocation fetch would be directed to.
|
|
71
|
+
* revocation fetch would be directed to. Its accessors are taken with it, because the constructor's
|
|
72
|
+
* capture does not cover the prototype the value reads its parts off, and the host read here is the
|
|
73
|
+
* one the address filter is applied to. */
|
|
72
74
|
var _URL = intrinsic.URL;
|
|
75
|
+
var _urlHref = intrinsic.urlHref;
|
|
76
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
77
|
+
var _urlHostname = intrinsic.urlHostname;
|
|
78
|
+
var _urlSetHash = intrinsic.urlSetHash;
|
|
73
79
|
var constants = require("./constants");
|
|
74
80
|
var validator = require("./validator-all");
|
|
75
81
|
var cms = require("./schema-cms");
|
|
@@ -125,6 +131,7 @@ var OID = intrinsic.assign(intrinsic.create(null), {
|
|
|
125
131
|
anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
|
|
126
132
|
cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
|
|
127
133
|
subjectKeyIdentifier: oid.byName("subjectKeyIdentifier"),
|
|
134
|
+
tlsFeature: oid.byName("tlsFeature"),
|
|
128
135
|
authorityKeyIdentifier: oid.byName("authorityKeyIdentifier"),
|
|
129
136
|
authorityInfoAccess: oid.byName("authorityInfoAccess"),
|
|
130
137
|
caIssuers: oid.byName("caIssuers"),
|
|
@@ -300,6 +307,66 @@ function _isCaCert(cert) {
|
|
|
300
307
|
return !!(bc && bc.value && bc.value.cA === true);
|
|
301
308
|
}
|
|
302
309
|
|
|
310
|
+
/** @internal RFC 7633 sec. 4.2.2, the one requirement in that document decidable from certificate
|
|
311
|
+
* bytes: "a certificate that is signed by a Certificate Signing Certificate that contains a TLS
|
|
312
|
+
* feature extension MUST contain a TLS feature extension that offers the same set or a superset of
|
|
313
|
+
* the features advertised in the signing certificate." The clause's own gate is the issuer's key
|
|
314
|
+
* usage, "Certification Authority certificate with the key usage extension value set to
|
|
315
|
+
* keyCertSign", which is a different question from its basicConstraints cA bit, so this asks
|
|
316
|
+
* keyUsage. A subject carrying NO extension under an issuer that advertises fails, because the
|
|
317
|
+
* clause says the subject MUST contain one; an issuer carrying none constrains nothing, so the
|
|
318
|
+
* constraint is one-directional.
|
|
319
|
+
*
|
|
320
|
+
* Set semantics. SEQUENCE OF fixes no order and forbids no repeat, so neither side's order nor its
|
|
321
|
+
* multiplicity may be read as meaning, and the comparison cannot stop at a first match.
|
|
322
|
+
*
|
|
323
|
+
* Returns null when there is nothing to check, so a boundary that was not checked reports no row
|
|
324
|
+
* rather than a passing one.
|
|
325
|
+
*
|
|
326
|
+
* The anchor is not read. `anchorFromCert` keeps the name, key and algorithm and drops the
|
|
327
|
+
* certificate's extensions, so a trust anchor's own advertised features cannot reach this and the
|
|
328
|
+
* rule is applied from the first certificate in the path downward. An anchor that advertises
|
|
329
|
+
* features constrains nothing here, which is stated rather than implied. */
|
|
330
|
+
function tlsFeatureChainCheck(issuerCert, cert) {
|
|
331
|
+
if (!issuerCert) return null;
|
|
332
|
+
var issuerKu;
|
|
333
|
+
try { issuerKu = decodeExt(issuerCert, OID.keyUsage); }
|
|
334
|
+
// allow:swallow-unverified a keyUsage that cannot be read is refused by its own check on that certificate
|
|
335
|
+
catch (_e) { return null; }
|
|
336
|
+
if (!(issuerKu && issuerKu.value && issuerKu.value.keyCertSign === true)) return null;
|
|
337
|
+
var issuerFeatures;
|
|
338
|
+
try { issuerFeatures = decodeExt(issuerCert, OID.tlsFeature); }
|
|
339
|
+
catch (e) { return guard.verdict.of({ ok: false, code: pathCode(e, "path/bad-tls-feature") }); }
|
|
340
|
+
if (!issuerFeatures) return null;
|
|
341
|
+
/** @internal The subject's extension is read BEFORE the issuer's set is looked at, because the
|
|
342
|
+
* clause is two requirements and the first one does not mention the set: a certificate signed by a
|
|
343
|
+
* signing certificate that CONTAINS the extension MUST CONTAIN one, and what it contains must then
|
|
344
|
+
* cover the issuer's features. Testing the set first read an issuer whose Features list is EMPTY as
|
|
345
|
+
* nothing to require and returned success for a subject that carried no extension at all, which the
|
|
346
|
+
* MUST CONTAIN half forbids; it also meant a malformed subject value under such an issuer was never
|
|
347
|
+
* decoded. An empty SEQUENCE is a conforming Features, the syntax carrying no SIZE constraint. */
|
|
348
|
+
var offeredExt;
|
|
349
|
+
try { offeredExt = decodeExt(cert, OID.tlsFeature); }
|
|
350
|
+
catch (e) { return guard.verdict.of({ ok: false, code: pathCode(e, "path/bad-tls-feature") }); }
|
|
351
|
+
if (!offeredExt) return guard.verdict.of({ ok: false, code: "path/tls-feature-not-offered" });
|
|
352
|
+
var required = (issuerFeatures.value && issuerFeatures.value.features) || [];
|
|
353
|
+
if (!required.length) return guard.verdict.of({ ok: true });
|
|
354
|
+
var offered = (offeredExt.value && offeredExt.value.features) || [];
|
|
355
|
+
/** @internal Membership through a set rather than a scan of the offered list per required value.
|
|
356
|
+
* Both lists come off untrusted certificates and the syntax bounds neither, so the pairwise form is
|
|
357
|
+
* work an issuer and a subject choose together: MEASURED, 20000 values on each side is 4 x 10^8
|
|
358
|
+
* comparisons, synchronous, on a path whose signatures may already have failed. A set makes it
|
|
359
|
+
* linear in the two lengths, and the values are bigints, which a set compares by value. */
|
|
360
|
+
var offeredSet = new intrinsic.Set();
|
|
361
|
+
for (var j = 0; j < offered.length; j++) intrinsic.setAdd(offeredSet, offered[j]);
|
|
362
|
+
for (var i = 0; i < required.length; i++) {
|
|
363
|
+
if (!intrinsic.setHas(offeredSet, required[i])) {
|
|
364
|
+
return guard.verdict.of({ ok: false, code: "path/tls-feature-not-offered" });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return guard.verdict.of({ ok: true });
|
|
368
|
+
}
|
|
369
|
+
|
|
303
370
|
function statefulHbsKeyUsageCheck(cert, isCa) {
|
|
304
371
|
var ku;
|
|
305
372
|
try { ku = decodeExt(cert, OID.keyUsage); }
|
|
@@ -1552,6 +1619,16 @@ async function _validate(path, opts, run, scope) {
|
|
|
1552
1619
|
guard.list.append(checks,{ name: "nameChaining", ok: chainOk === true, code: chainOk === true ? undefined : "path/name-chaining" });
|
|
1553
1620
|
if (chainOk !== true) failed = true;
|
|
1554
1621
|
|
|
1622
|
+
/** @internal In the loop body, so it runs at EVERY issuer-to-subject boundary rather than only at
|
|
1623
|
+
* the target: RFC 7633 sec. 4.2.2 constrains each certificate against the one that signed it. The
|
|
1624
|
+
* issuer is the previous entry, which is `null` for the first certificate because the anchor's
|
|
1625
|
+
* extensions are not part of the path. */
|
|
1626
|
+
var tfRes = tlsFeatureChainCheck(idx > 0 ? certs[idx - 1] : null, cert);
|
|
1627
|
+
if (tfRes) {
|
|
1628
|
+
guard.list.append(checks, { name: "tlsFeatureChain", ok: tfRes.ok, code: tfRes.ok ? undefined : tfRes.code });
|
|
1629
|
+
if (!tfRes.ok) failed = true;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1555
1632
|
if (!(selfIssued(cert) && i !== n)) {
|
|
1556
1633
|
var ncRes;
|
|
1557
1634
|
try { ncRes = checkNameConstraints(state, cert); }
|
|
@@ -3151,14 +3228,18 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3151
3228
|
var u;
|
|
3152
3229
|
try { u = new _URL(ad.accessLocation.value); }
|
|
3153
3230
|
catch (_e) { continue; }
|
|
3154
|
-
if (u
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
if (
|
|
3231
|
+
if (_urlProtocol(u) !== "https:") continue;
|
|
3232
|
+
/** @internal The host read ONCE and both address tests applied to that one string: the blocklist
|
|
3233
|
+
* and the literal-versus-name question have to be answered about the same host. */
|
|
3234
|
+
var uHost = _urlHostname(u);
|
|
3235
|
+
if (_isBlockedAiaHost(uHost)) continue;
|
|
3236
|
+
if (net.isIP(_stripBrackets(uHost)) === 0 && !aia.transportGuardsAddresses) continue;
|
|
3237
|
+
_urlSetHash(u, "");
|
|
3238
|
+
var uHref = _urlHref(u);
|
|
3239
|
+
if (intrinsic.setHas(aia.fetchedUrls, uHref) || intrinsic.setHas(seenThisCert, uHref)) continue;
|
|
3159
3240
|
if (seenThisCert.size >= aia.maxPerCert) break;
|
|
3160
|
-
intrinsic.setAdd(seenThisCert,
|
|
3161
|
-
guard.list.append(uris,
|
|
3241
|
+
intrinsic.setAdd(seenThisCert, uHref);
|
|
3242
|
+
guard.list.append(uris, uHref);
|
|
3162
3243
|
}
|
|
3163
3244
|
var out = [];
|
|
3164
3245
|
for (var k = 0; k < uris.length; k++) {
|
package/lib/pki-build.js
CHANGED
|
@@ -851,6 +851,39 @@ function makeBuilder(ctx) {
|
|
|
851
851
|
}
|
|
852
852
|
/** @internal InhibitAnyPolicy (RFC 5280 sec. 4.2.1.14): a bare SkipCerts INTEGER. */
|
|
853
853
|
function extInhibitAnyPolicy(v) { return b.integer(skipCount(v, "inhibitAnyPolicy")); }
|
|
854
|
+
/** @internal Features (RFC 7633 sec. 4): `SEQUENCE OF INTEGER`, each a TLS ExtensionType value.
|
|
855
|
+
* sec. 4.1 binds those values to the IANA TLS Extensions registry, so the registry names this
|
|
856
|
+
* build knows are accepted beside the numbers, resolved through the one table `C.NAMES.TLS_FEATURE`
|
|
857
|
+
* that `pki.inspect` prints them from rather than a second copy of it. The type states no range and
|
|
858
|
+
* no ordering, so a value outside the registry's 16-bit width and a repeat both encode, and the
|
|
859
|
+
* linter is what grades them: an encoder that dropped or sorted them would hide what it emitted
|
|
860
|
+
* from the rule that reads it. An EMPTY list is refused here although the decoder accepts one on
|
|
861
|
+
* the wire, because an empty Features advertises nothing and makes the extension inert, which is a
|
|
862
|
+
* caller's typo rather than a document to read. */
|
|
863
|
+
function extTlsFeature(list) {
|
|
864
|
+
if (!_isArray(list) || !list.length) {
|
|
865
|
+
throw E("bad-input", "tlsFeature must name at least one TLS ExtensionType value (RFC 7633 sec. 4.1); " +
|
|
866
|
+
"an empty Features advertises nothing, so the extension it would emit is inert");
|
|
867
|
+
}
|
|
868
|
+
var dense = reqDenseArray(list, "tlsFeature");
|
|
869
|
+
return b.sequence(_mapIntrinsic(dense, function (f, i) {
|
|
870
|
+
return b.integer(tlsFeatureValue(f, "tlsFeature entry " + i));
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
/** @internal A registry name resolved off the printing table, so the two directions cannot drift. */
|
|
874
|
+
function tlsFeatureValue(f, who) {
|
|
875
|
+
if (typeof f !== "string") {
|
|
876
|
+
return guard.range.authoredInteger(f, rawErr, ctx.prefix + "/bad-input", who);
|
|
877
|
+
}
|
|
878
|
+
var table = C.NAMES.TLS_FEATURE;
|
|
879
|
+
var keys = intrinsic.keys(table);
|
|
880
|
+
for (var i = 0; i < keys.length; i++) {
|
|
881
|
+
if (table[keys[i]] === f) return intrinsic.BigInt(keys[i]);
|
|
882
|
+
}
|
|
883
|
+
throw E("bad-input", who + " names no TLS ExtensionType this build knows: " + _stringify(f) +
|
|
884
|
+
" (accepted names: " + intrinsic.join(_mapIntrinsic(keys, function (k) { return table[k]; }), ", ") +
|
|
885
|
+
"; any other value is given as its number)");
|
|
886
|
+
}
|
|
854
887
|
/** @internal PolicyMappings (RFC 5280 sec. 4.2.1.5): SEQUENCE SIZE (1..MAX) OF
|
|
855
888
|
* { issuerDomainPolicy, subjectDomainPolicy }. The clause forbids mapping either to or from
|
|
856
889
|
* anyPolicy, so neither side may name it. */
|
|
@@ -1282,6 +1315,7 @@ function makeBuilder(ctx) {
|
|
|
1282
1315
|
nameConstraints: 1, authorityInfoAccess: 1, cRLDistributionPoints: 1, freshestCRL: 1,
|
|
1283
1316
|
policyConstraints: 1, inhibitAnyPolicy: 1, policyMappings: 1, policyMappingsCritical: 1,
|
|
1284
1317
|
issuerAltName: 1, qcStatements: 1, qcStatementsCritical: 1,
|
|
1318
|
+
tlsFeature: 1, tlsFeatureCritical: 1,
|
|
1285
1319
|
msCertificateTemplate: 1, msEnrollCertType: 1, msApplicationPolicies: 1,
|
|
1286
1320
|
subjectInfoAccess: 1, subjectDirectoryAttributes: 1,
|
|
1287
1321
|
};
|
|
@@ -1336,6 +1370,11 @@ function makeBuilder(ctx) {
|
|
|
1336
1370
|
if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), !!extSpec.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
|
|
1337
1371
|
if (extSpec.issuerAltName != null) guard.list.append(out, ext(O("issuerAltName"), false, encodeGeneralNames(reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
|
|
1338
1372
|
if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), !!extSpec.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
|
|
1373
|
+
/** @internal RFC 7633 sec. 4.2.1 lets a request state the features the server supports, and
|
|
1374
|
+
* sec. 4.3.2 asks a server to generate it, so a feature declaration is the requester's wish and
|
|
1375
|
+
* belongs in this table. The criticality knob is here too, because a request carries the
|
|
1376
|
+
* Extension shape whole and the CA decides what it issues. */
|
|
1377
|
+
if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
|
|
1339
1378
|
if (extSpec.msCertificateTemplate != null) guard.list.append(out, ext(O("msCertificateTemplate"), false, extMsCertificateTemplate(extSpec.msCertificateTemplate)));
|
|
1340
1379
|
if (extSpec.msEnrollCertType != null) guard.list.append(out, ext(O("msEnrollCertType"), false, extMsEnrollCertType(extSpec.msEnrollCertType)));
|
|
1341
1380
|
if (extSpec.msApplicationPolicies != null) guard.list.append(out, ext(O("msApplicationPolicies"), false, extCertPolicies(extSpec.msApplicationPolicies)));
|
|
@@ -1395,6 +1434,7 @@ function makeBuilder(ctx) {
|
|
|
1395
1434
|
extNameConstraints: extNameConstraints, extAuthorityInfoAccess: extAuthorityInfoAccess,
|
|
1396
1435
|
extCrlDistributionPoints: extCrlDistributionPoints, extPolicyConstraints: extPolicyConstraints,
|
|
1397
1436
|
extInhibitAnyPolicy: extInhibitAnyPolicy, extPolicyMappings: extPolicyMappings,
|
|
1437
|
+
extTlsFeature: extTlsFeature,
|
|
1398
1438
|
extQcStatements: extQcStatements,
|
|
1399
1439
|
encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
|
|
1400
1440
|
requestedExtensions: requestedExtensions,
|
package/lib/revocation-fetch.js
CHANGED
|
@@ -32,8 +32,13 @@ var OID_OCSP_NONCE = oid.byName("ocspNonce");
|
|
|
32
32
|
|
|
33
33
|
var _append = guard.list.append;
|
|
34
34
|
/** @internal The URL parser, taken at load: the host it reports is the distribution point a fetch is
|
|
35
|
-
* directed to.
|
|
35
|
+
* directed to. Its accessors are taken with it, since the constructor's capture does not cover the
|
|
36
|
+
* prototype the value reads its parts off and the host read here is the one the filter applies to. */
|
|
36
37
|
var _URL = intrinsic.URL;
|
|
38
|
+
var _urlHref = intrinsic.urlHref;
|
|
39
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
40
|
+
var _urlHostname = intrinsic.urlHostname;
|
|
41
|
+
var _urlSetHash = intrinsic.urlSetHash;
|
|
37
42
|
var LIMITS = constants.LIMITS;
|
|
38
43
|
|
|
39
44
|
var CHECKER_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
@@ -167,16 +172,17 @@ function _acceptableUrl(value, net_) {
|
|
|
167
172
|
var u;
|
|
168
173
|
try { u = new _URL(value); }
|
|
169
174
|
catch (_e) { return _no("is not one this client fetches"); } // allow:swallow-unverified a value that is not a URL names no location
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
var scheme = _urlProtocol(u);
|
|
176
|
+
if (scheme !== "https:" && !(net_.allowHttp && scheme === "http:")) return _no("is not one this client fetches");
|
|
177
|
+
var host = _urlHostname(u);
|
|
172
178
|
if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1);
|
|
173
179
|
if (net.isIP(host) !== 0) {
|
|
174
180
|
if (httpTransport.isBlockedIp(host)) return _no("is not one this client fetches");
|
|
175
181
|
} else if (!net_.guardsAddresses) {
|
|
176
182
|
return _no("names a host whose resolved address opts.transport does not declare it filters");
|
|
177
183
|
}
|
|
178
|
-
u
|
|
179
|
-
return { url: u
|
|
184
|
+
_urlSetHash(u, "");
|
|
185
|
+
return { url: _urlHref(u), why: null };
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
function _no(why) { return { url: null, why: why }; }
|
package/lib/scep.js
CHANGED
|
@@ -43,6 +43,15 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
43
43
|
* handed is taken with it, since a replacement answers with a host of its own. */
|
|
44
44
|
var _URL = intrinsic.URL;
|
|
45
45
|
var _stringOf = intrinsic.String;
|
|
46
|
+
/** @internal And the accessors its result is read through: capturing the constructor does not capture
|
|
47
|
+
* the prototype the value reads its parts off, and the scheme here decides whether a redirect may
|
|
48
|
+
* be followed while the origin decides whether it is a cross-origin one. */
|
|
49
|
+
var _urlHref = intrinsic.urlHref;
|
|
50
|
+
var _urlOrigin = intrinsic.urlOrigin;
|
|
51
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
52
|
+
var _urlSearchParams = intrinsic.urlSearchParams;
|
|
53
|
+
var _urlHash = intrinsic.urlHash;
|
|
54
|
+
var _searchSet = intrinsic.uncurry(URLSearchParams.prototype.set);
|
|
46
55
|
var oid = require("./oid");
|
|
47
56
|
var asn1 = require("./asn1-der");
|
|
48
57
|
var b = asn1.build;
|
|
@@ -675,21 +684,26 @@ function _tlsForRequest(opts) {
|
|
|
675
684
|
function _redirectTarget(current, location, method, allowCrossOrigin) {
|
|
676
685
|
if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
|
|
677
686
|
var resolved;
|
|
678
|
-
try { resolved = new _URL(_stringOf(location), current
|
|
687
|
+
try { resolved = new _URL(_stringOf(location), _urlHref(current)); }
|
|
679
688
|
catch (e) { throw _err("scep/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
680
|
-
|
|
681
|
-
|
|
689
|
+
/** @internal Each scheme read ONCE: the first test decides whether the target is HTTP at all and
|
|
690
|
+
* the second whether it is a downgrade, and those two have to be answered about one value. */
|
|
691
|
+
var toScheme = _urlProtocol(resolved);
|
|
692
|
+
var fromScheme = _urlProtocol(current);
|
|
693
|
+
if (toScheme !== "http:" && toScheme !== "https:") throw _err("scep/bad-url", "a redirect to a non-HTTP scheme is refused: " + toScheme);
|
|
694
|
+
if (fromScheme === "https:" && toScheme !== "https:") throw _err("scep/insecure-redirect", "a redirect from https to " + toScheme + " is refused");
|
|
682
695
|
var safe = method === "GET" || method === "HEAD";
|
|
683
|
-
if (resolved
|
|
696
|
+
if (_urlOrigin(resolved) !== _urlOrigin(current) && !safe && !allowCrossOrigin) throw _err("scep/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect");
|
|
684
697
|
return resolved;
|
|
685
698
|
}
|
|
686
699
|
function _scepUrl(baseUrl, operation) {
|
|
687
700
|
var url;
|
|
688
701
|
try { url = new _URL(_stringOf(baseUrl)); }
|
|
689
702
|
catch (e) { throw _err("scep/bad-url", "the SCEP base URL did not parse: " + _showValue(baseUrl), e); }
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
url
|
|
703
|
+
var baseScheme = _urlProtocol(url);
|
|
704
|
+
if (baseScheme !== "http:" && baseScheme !== "https:") throw _err("scep/bad-url", "the SCEP base URL must be http or https, got " + baseScheme);
|
|
705
|
+
if (_urlHash(url)) throw _err("scep/bad-url", "the SCEP base URL must not carry a fragment");
|
|
706
|
+
_searchSet(_urlSearchParams(url), "operation", operation);
|
|
693
707
|
return url;
|
|
694
708
|
}
|
|
695
709
|
function _pkiMethod(opts) {
|
|
@@ -700,27 +714,27 @@ function _pkiMethod(opts) {
|
|
|
700
714
|
}
|
|
701
715
|
function _pkiRequest(method, opUrl, reqMsg, clientOpts) {
|
|
702
716
|
if (method === "GET") {
|
|
703
|
-
var getUrl = new _URL(opUrl
|
|
704
|
-
getUrl
|
|
717
|
+
var getUrl = new _URL(_urlHref(opUrl));
|
|
718
|
+
_searchSet(_urlSearchParams(getUrl), "message", Buffer.from(reqMsg).toString("base64"));
|
|
705
719
|
return _client("GET", getUrl, null, {}, clientOpts);
|
|
706
720
|
}
|
|
707
721
|
return _client("POST", opUrl, reqMsg, { "content-type": CT_PKI_MESSAGE }, clientOpts);
|
|
708
722
|
}
|
|
709
723
|
function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
710
|
-
var redirects = 0, curMethod = method, curBody = body, initialOrigin = url
|
|
724
|
+
var redirects = 0, curMethod = method, curBody = body, initialOrigin = _urlOrigin(url);
|
|
711
725
|
var proxySnap = _snapshotProxy(opts.proxy);
|
|
712
726
|
var tlsBase = _tlsForRequest(opts);
|
|
713
727
|
var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
|
|
714
728
|
function _hopTls(current) {
|
|
715
729
|
var t = tlsBase;
|
|
716
|
-
if (current
|
|
730
|
+
if (_urlOrigin(current) !== initialOrigin) {
|
|
717
731
|
t = Object.assign({}, t);
|
|
718
732
|
t.cert = null; t.key = null; t.servername = null;
|
|
719
733
|
}
|
|
720
734
|
return t;
|
|
721
735
|
}
|
|
722
736
|
function step(current) {
|
|
723
|
-
var req = { method: curMethod, url: current
|
|
737
|
+
var req = { method: curMethod, url: _urlHref(current), headers: headers, tls: _hopTls(current), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes };
|
|
724
738
|
if (proxySnap != null) req.proxy = proxySnap;
|
|
725
739
|
if (curBody != null) req.body = curBody;
|
|
726
740
|
return guard.async.invoked(transport, [req], _err, "scep/bad-input", "opts.transport").then(function (res) {
|
package/lib/x509-sign.js
CHANGED
|
@@ -71,6 +71,7 @@ var KNOWN_EXT_KEYS = intrinsic.assign(intrinsic.create(null), {
|
|
|
71
71
|
authorityInfoAccess: 1, cRLDistributionPoints: 1, freshestCRL: 1,
|
|
72
72
|
policyConstraints: 1, inhibitAnyPolicy: 1, policyMappings: 1, policyMappingsCritical: 1,
|
|
73
73
|
issuerAltName: 1, qcStatements: 1, qcStatementsCritical: 1,
|
|
74
|
+
tlsFeature: 1, tlsFeatureCritical: 1,
|
|
74
75
|
precertificatePoison: 1, signedCertificateTimestampList: 1,
|
|
75
76
|
msCertificateTemplate: 1, msEnrollCertType: 1, msCaVersion: 1, msPreviousCertHash: 1,
|
|
76
77
|
msApplicationPolicies: 1,
|
|
@@ -279,6 +280,9 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
279
280
|
* unrecognized critical extension; the default is the form that validates and the knob reaches
|
|
280
281
|
* the other, as policyMappings and certificatePolicies do. */
|
|
281
282
|
if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), !!extSpec.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
|
|
283
|
+
/** @internal RFC 7633 sec. 4: SHOULD NOT be critical, and the clause names the case where critical
|
|
284
|
+
* is the desired behavior, so the knob exists and the default is the compatible form. */
|
|
285
|
+
if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
|
|
282
286
|
if (extSpec.nameConstraints != null) {
|
|
283
287
|
if (!caTrue) throw _err("x509/bad-input", "nameConstraints appears only in a CA certificate, so it requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.10)");
|
|
284
288
|
guard.list.append(out, _ext(O("nameConstraints"), true, _extNameConstraints(extSpec.nameConstraints)));
|
|
@@ -628,6 +632,18 @@ function _hasCriticalSan(extSpec) {
|
|
|
628
632
|
* know an unknown syntax. `pki.path.validate` does not process this extension, so it is emitted
|
|
629
633
|
* non-critical unless `qcStatementsCritical` is set.
|
|
630
634
|
*
|
|
635
|
+
* `extensions.tlsFeature` builds the RFC 7633 TLS Feature extension, the "must-staple" declaration,
|
|
636
|
+
* from a list of TLS ExtensionType values. Each is a number, a bigint, or one of the registry names
|
|
637
|
+
* this build knows, `status_request` and `status_request_v2`. The values come from the IANA TLS
|
|
638
|
+
* Extensions registry that RFC 7633 sec. 4.1 points at, and the type states no range, so a value
|
|
639
|
+
* outside that registry's width encodes and `pki.lint.certificate(der, { profile: "rfc7633" })`
|
|
640
|
+
* reports it rather than the encoder refusing it; the same holds for a repeated value, which the
|
|
641
|
+
* syntax permits. An EMPTY list is refused, because the extension it would emit advertises nothing.
|
|
642
|
+
* Emitted non-critical unless `tlsFeatureCritical` is set: sec. 4 says it SHOULD NOT be critical and
|
|
643
|
+
* names the case where critical is the caller's intent. `pki.path.validate` enforces sec. 4.2.2 over
|
|
644
|
+
* a built path, so a certificate issued under a CA that advertises features must offer the same set
|
|
645
|
+
* or a superset.
|
|
646
|
+
*
|
|
631
647
|
* `extensions.subjectInfoAccess` takes the same `{ accessMethod, accessLocation }` list
|
|
632
648
|
* `authorityInfoAccess` takes, since RFC 5280 sec. 4.2.2.2 gives it sec. 4.2.2.1's syntax; the two
|
|
633
649
|
* access methods that section defines are the registered names `id-ad-caRepository` and
|
|
@@ -910,6 +926,10 @@ var EXTENSION_ROWS = [
|
|
|
910
926
|
["cRLDistributionPoints", "cRLDistributionPoints", { fixed: false, cite: "RFC 5280 sec. 4.2.1.13" }, function (v) { return _extCrlDp(v, "cRLDistributionPoints"); }],
|
|
911
927
|
["freshestCRL", "freshestCRL", { fixed: false, cite: "RFC 5280 sec. 4.2.1.15" }, function (v) { return _extCrlDp(v, "freshestCRL"); }],
|
|
912
928
|
["qcStatements", "qcStatements", { knob: true, dflt: false }, function (v) { return _extQcStatements(v); }],
|
|
929
|
+
/** @internal RFC 7633 sec. 4 reads "SHOULD NOT be marked critical" and then names the case where
|
|
930
|
+
* the critical form is what the caller wants, so this takes a knob defaulting to the compatible
|
|
931
|
+
* form rather than refusing one: `fixed: false` would promote that SHOULD NOT into a refusal. */
|
|
932
|
+
["tlsFeature", "tlsFeature", { knob: true, dflt: false }, function (v) { return _b.extTlsFeature(v); }],
|
|
913
933
|
["subjectDirectoryAttributes", "subjectDirectoryAttributes", { fixed: false, cite: "RFC 5280 sec. 4.2.1.8" }, function (v) { return _b.extSubjectDirectoryAttributes(v); }],
|
|
914
934
|
["msCertificateTemplate", "msCertificateTemplate", { fixed: false, cite: "the toolkit issues the form its readers accept" }, function (v) { return _b.extMsCertificateTemplate(v); }],
|
|
915
935
|
["msEnrollCertType", "msEnrollCertType", { fixed: false, cite: "the toolkit issues the form its readers accept" }, function (v) { return _b.extMsEnrollCertType(v); }],
|
|
@@ -971,8 +991,8 @@ function _extensionCritical(row, opts) {
|
|
|
971
991
|
* (the key identifiers, the constraint and policy-constraint extensions, the access and
|
|
972
992
|
* distribution extensions), or the toolkit issues one form only, `opts.critical` may only restate
|
|
973
993
|
* that value. `keyUsage` (default critical), `extendedKeyUsage`, `certificatePolicies`,
|
|
974
|
-
* `policyMappings`, `qcStatements` (default non-critical), and `subjectAltName`
|
|
975
|
-
* when the subject is empty, which the caller knows) take `opts.critical`; `basicConstraints`
|
|
994
|
+
* `policyMappings`, `qcStatements` and `tlsFeature` (default non-critical), and `subjectAltName`
|
|
995
|
+
* (critical exactly when the subject is empty, which the caller knows) take `opts.critical`; `basicConstraints`
|
|
976
996
|
* carries `critical` inside its value. An unregistered OID takes `opts.critical` as stated.
|
|
977
997
|
*
|
|
978
998
|
* @opts
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:3a1ddfe7-c71b-4cb4-8bd0-256c3b6075ba",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-05T13:17:14.754Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.51",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.51",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.51",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.8.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.51",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|