@blamejs/pki 0.8.27 → 0.8.28
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 +21 -0
- package/README.md +1 -1
- package/index.js +14 -1
- package/lib/crl-sign.js +22 -3
- package/lib/csr-sign.js +17 -2
- package/lib/inspect.js +3 -2
- package/lib/lint.js +130 -2
- package/lib/schema-crl.js +12 -7
- package/lib/x509-sign.js +20 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ 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.28 — 2026-09-28
|
|
8
|
+
|
|
9
|
+
The three signing verbs lint what they are about to emit, and refuse it before the key is used.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.x509.sign` refuses a spec whose certificate violates an error-severity rule of the RFC 5280 profile, with `x509/profile-violation` naming the rule and its clause. `pki.csr.sign` does the same against RFC 2986 with `csr/profile-violation`, and `pki.crl.sign` against the RFC 5280 section 5 profile with `crl/profile-violation`. The check runs on the assembled artifact rather than on the spec, so it reports what would have reached the wire.
|
|
14
|
+
- The refusal precedes the signature. A spec that violates a rule never reaches the signing key, which matters where that key is a hardware token, a remote signer, or anything that counts or logs a use.
|
|
15
|
+
- `opts.profile` on all three verbs names a further profile from `pki.lint.profiles()` whose error-severity rules run beside the always-on set, so a certificate can be held to `cabf-tls` or `cnsa-2.0` at the moment it is built rather than after. A name that verb does not run is refused with its own `bad-input` code and the sentence `pki.lint.rules` gives for the same name.
|
|
16
|
+
- `opts.profile: "none"` runs no rules and emits what the spec describes. A toolkit that cannot emit a non-conforming artifact cannot produce a test corpus, reproduce a real misissuance, or build the fixtures a linter is exercised against, so the escape is a name the caller writes rather than a default: the gate is closed unless somebody asked for it, and every refusal names it.
|
|
17
|
+
- Two kinds of finding are passed over. A rule reading the signature value cannot apply, because at the moment of the check the signature does not exist. `lint/rfc5280/unknown-critical-extension` does not apply either: RFC 5280 section 4.2 places that requirement on a certificate-using system, and the same section states that conforming CAs may support extensions the specification does not identify, so a private critical extension signs and the relying party still decides what to do with it.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- A spec that describes a non-conforming artifact is now refused rather than signed. What changes in practice: a CRL spec with no `crlNumber` or no `nextUpdate` is refused, since RFC 5280 section 5.2.3 and section 5.1.2.5 require both of a conforming CRL issuer and neither can be derived, a CRL number being issuer state; a certificate whose basicConstraints sets cA without a keyUsage asserting keyCertSign is refused, per section 4.2.1.3; a request with an empty subject that does not ask for a subjectAltName is refused, since it names nothing to certify; and a certificate or CRL built through the pre-encoded extension array is refused when that array carries no key identifier the artifact needs, because that form emits only what is written into it. The object form still derives what its RFC places on the issuer: a subjectKeyIdentifier on every certificate, an authorityKeyIdentifier on every certificate that is not self-signed and on every CRL. To upgrade: add the missing field, write the identifier into the array, or pass `{ profile: "none" }` where the artifact is meant to be non-conforming. The refusal message names the rule and the clause in every case.
|
|
22
|
+
- `pki.lint` is assembled from its public verbs rather than passed through as a module, so the namespace holds `certificate`, `csr`, `crl`, `ocsp`, `cms`, `tsp`, `attrcert`, `rules` and `profiles` and nothing else. No name was removed.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- The documented examples for `pki.crl.verify`, `pki.crl.isRevoked`, `pki.inspect.crl`, `pki.schema.crl.parse`, `pki.schema.crl.pemDecode`, `pki.schema.crl.pemEncode`, `pki.schema.crl.decodeExtension` and `pki.schema.crl.decodeExtensions` built artifacts this toolkit's linter grades an error, and an operator copying one got a CRL with no nextUpdate or a CA certificate asserting no keyCertSign. Each now carries what its RFC requires.
|
|
27
|
+
|
|
7
28
|
## v0.8.27 — 2026-09-28
|
|
8
29
|
|
|
9
30
|
Two algorithm-policy profiles, and a two-digit year read against the clock rather than a constant.
|
package/README.md
CHANGED
|
@@ -268,7 +268,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
268
268
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
|
|
269
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` |
|
|
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, and the RFC 6960 OCSP no-check responder marker, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
|
|
271
|
+
| `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, and the RFC 6960 OCSP no-check responder marker, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. 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` |
|
package/index.js
CHANGED
|
@@ -224,7 +224,20 @@ module.exports = {
|
|
|
224
224
|
// OID registry, naming extension/algorithm OIDs OpenSSL shows only as raw bytes.
|
|
225
225
|
// Pure, no OpenSSL dependency; best-effort (a bad extension falls back to hex).
|
|
226
226
|
inspect: inspect,
|
|
227
|
-
lint
|
|
227
|
+
// `lint` is named member by member rather than passed through, because the module also exports the
|
|
228
|
+
// build-time gate the authoring verbs call and the two readers its conformance vectors use. Those
|
|
229
|
+
// are internal plumbing, so they stay off the public namespace.
|
|
230
|
+
lint: {
|
|
231
|
+
certificate: lint.certificate,
|
|
232
|
+
csr: lint.csr,
|
|
233
|
+
crl: lint.crl,
|
|
234
|
+
ocsp: lint.ocsp,
|
|
235
|
+
cms: lint.cms,
|
|
236
|
+
tsp: lint.tsp,
|
|
237
|
+
attrcert: lint.attrcert,
|
|
238
|
+
rules: lint.rules,
|
|
239
|
+
profiles: lint.profiles,
|
|
240
|
+
},
|
|
228
241
|
// `webauthn` verifies a W3C WebAuthn / passkey attestation -- pki.webauthn.verify
|
|
229
242
|
// checks the attestation-statement signature + each format's structural bindings
|
|
230
243
|
// (packed / tpm / android-key / apple / fido-u2f / none) and surfaces the x5c chain
|
package/lib/crl-sign.js
CHANGED
|
@@ -32,6 +32,7 @@ var pkiBuild = require("./pki-build");
|
|
|
32
32
|
var constants = require("./constants");
|
|
33
33
|
require("./path-validate");
|
|
34
34
|
var crlVerify = require("./crl-verify");
|
|
35
|
+
var lint = require("./lint");
|
|
35
36
|
|
|
36
37
|
var CrlError = frameworkError.CrlError;
|
|
37
38
|
var NS = pkix.makeNS("crl", CrlError, oid);
|
|
@@ -85,7 +86,11 @@ var KNOWN_SPEC_KEYS_ISSUER_NAMED = intrinsic.assign(intrinsic.create(null), {
|
|
|
85
86
|
});
|
|
86
87
|
var KNOWN_ISSUER_CERT_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, cert: 1 });
|
|
87
88
|
var KNOWN_ISSUER_EXPLICIT_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, name: 1, publicKey: 1 });
|
|
88
|
-
var KNOWN_SIGN_OPTS = intrinsic.assign(intrinsic.create(null), { digestAlgorithm: 1, pem: 1, pss: 1 });
|
|
89
|
+
var KNOWN_SIGN_OPTS = intrinsic.assign(intrinsic.create(null), { digestAlgorithm: 1, pem: 1, pss: 1, profile: 1 });
|
|
90
|
+
|
|
91
|
+
/** @internal The signature the build-time gate lints against, standing in for one that does not exist
|
|
92
|
+
* until the key is used. It is non-empty so the gate is not answering a rule about its own placeholder. */
|
|
93
|
+
var GATE_PLACEHOLDER_SIG = intrinsic.bufferAlloc(64, 0xff);
|
|
89
94
|
|
|
90
95
|
var KNOWN_IDP_KEYS = intrinsic.assign(intrinsic.create(null), { fullName: 1, onlyContainsUserCerts: 1, onlyContainsCACerts: 1, onlyContainsAttributeCerts: 1, indirectCRL: 1 });
|
|
91
96
|
|
|
@@ -384,6 +389,7 @@ function _sign(spec, issuer, opts) {
|
|
|
384
389
|
: "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
|
|
385
390
|
});
|
|
386
391
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "crl/bad-input", "pki.crl.sign has an unknown option ");
|
|
392
|
+
lint.assertKnownBuildProfile("crl", opts.profile, _err, "crl/bad-input");
|
|
387
393
|
if (issuer.key == null) throw _err("crl/bad-input", "a signing key (issuer.key, a WebCrypto CryptoKey or a PKCS#8 private key DER/PEM) is required");
|
|
388
394
|
|
|
389
395
|
var issuerDer, issuerSpki, issuerCert = null;
|
|
@@ -426,6 +432,9 @@ function _sign(spec, issuer, opts) {
|
|
|
426
432
|
if (revoked.entries.length) tbsChildren.push(b.sequence(revoked.entries));
|
|
427
433
|
if (crlExts.length) tbsChildren.push(b.explicit(0, b.sequence(crlExts)));
|
|
428
434
|
var tbsDer = b.sequence(tbsChildren);
|
|
435
|
+
lint.assertBuildClean("crl",
|
|
436
|
+
b.sequence([tbsDer, scheme.sigAlgId, b.bitString(GATE_PLACEHOLDER_SIG, 0)]),
|
|
437
|
+
opts.profile, _err, "crl/profile-violation");
|
|
429
438
|
|
|
430
439
|
return signScheme.signOverTbs(scheme, issuer.key, tbsDer, _signE).then(function (sig) {
|
|
431
440
|
return Promise.resolve(_assertSignatureVerifies(tbsDer, sig, issuerSpki, scheme)).then(function () {
|
|
@@ -474,10 +483,18 @@ function _sign(spec, issuer, opts) {
|
|
|
474
483
|
* key before return. A violation throws a typed `CrlError`; where the spec carries raw DER (an issuer
|
|
475
484
|
* `Name` Buffer or a pre-encoded `Extension`), a malformed leaf inside those bytes throws `Asn1Error`.
|
|
476
485
|
*
|
|
486
|
+
* Before the key is used, the CRL the spec describes is linted against the RFC 5280 section 5 profile
|
|
487
|
+
* and refused with `crl/profile-violation` if any rule grades it `error`, naming the rule and its
|
|
488
|
+
* clause. A `crlNumber` and a `nextUpdate` are what a spec most often lacks, and sec. 5.2.3 and
|
|
489
|
+
* sec. 5.1.2.5 require both of a conforming CRL issuer.
|
|
490
|
+
*
|
|
477
491
|
* @opts
|
|
478
492
|
* - `pem` (boolean) -- return a PEM `X509 CRL` string instead of DER.
|
|
479
493
|
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
|
|
480
494
|
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
495
|
+
* - `profile` (string) -- a second lint profile to hold the CRL to beside RFC 5280 section 5, named
|
|
496
|
+
* from `pki.lint.profiles()`. `"none"` runs no rules at all and emits what the spec describes,
|
|
497
|
+
* which is how a deliberately non-conforming CRL is produced.
|
|
481
498
|
* @example
|
|
482
499
|
* var pair = await pki.key.generate("Ed25519");
|
|
483
500
|
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
@@ -556,7 +573,8 @@ function _issuerMaySign(parsed, cert) {
|
|
|
556
573
|
* var signerCertDer = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: signerSpki,
|
|
557
574
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
558
575
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: signerKeyPkcs8 });
|
|
559
|
-
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
576
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
577
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
|
|
560
578
|
* { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
561
579
|
* var res = await pki.crl.verify(crlDer, { publicKey: signerSpki }); // { valid, signatureValid, issuerMaySign, issuer }
|
|
562
580
|
*/
|
|
@@ -640,7 +658,8 @@ function _serialHexOf(serial) {
|
|
|
640
658
|
* var signerCertDer = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
641
659
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
642
660
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: signerKeyPkcs8 });
|
|
643
|
-
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
661
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
662
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n,
|
|
644
663
|
* revoked: [{ serialNumber: 0x1234n, revocationDate: new Date("2026-01-15T00:00:00Z") }] },
|
|
645
664
|
* { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
646
665
|
* pki.crl.isRevoked(crlDer, 0x1234n) ? "revoked" : "not listed";
|
package/lib/csr-sign.js
CHANGED
|
@@ -27,13 +27,18 @@ var frameworkError = require("./framework-error");
|
|
|
27
27
|
var guard = require("./guard-all");
|
|
28
28
|
require("./path-validate");
|
|
29
29
|
var csrVerify = require("./csr-verify");
|
|
30
|
+
var lint = require("./lint");
|
|
30
31
|
|
|
31
32
|
var CsrError = frameworkError.CsrError;
|
|
32
33
|
var KNOWN_SPEC_KEYS = Object.assign(Object.create(null), { subject: 1, subjectPublicKey: 1, extensionRequest: 1, challengePassword: 1, attributes: 1 });
|
|
33
|
-
/** @internal The
|
|
34
|
+
/** @internal The four this verb reads, which are the four `pki.x509.sign` reads. A name outside
|
|
34
35
|
* them was accepted and dropped, so asking for PEM and misspelling the option returned DER and
|
|
35
36
|
* reported nothing. */
|
|
36
|
-
var KNOWN_SIGN_OPTS = Object.assign(Object.create(null), { digestAlgorithm: 1, pem: 1, pss: 1 });
|
|
37
|
+
var KNOWN_SIGN_OPTS = Object.assign(Object.create(null), { digestAlgorithm: 1, pem: 1, pss: 1, profile: 1 });
|
|
38
|
+
|
|
39
|
+
/** @internal The signature the build-time gate lints against, standing in for one that does not exist
|
|
40
|
+
* until the key is used. It is non-empty so the gate is not answering a rule about its own placeholder. */
|
|
41
|
+
var GATE_PLACEHOLDER_SIG = Buffer.alloc(64, 0xff);
|
|
37
42
|
var KNOWN_KEY_WRAPPER_KEYS = Object.assign(Object.create(null), { key: 1 });
|
|
38
43
|
var b = asn1.build;
|
|
39
44
|
function _err(code, message, cause) { return new CsrError(code, message, cause); }
|
|
@@ -86,10 +91,16 @@ function _challengePassword(pw) {
|
|
|
86
91
|
* Buffer, a pre-encoded requested `Extension` or `Attribute`) a malformed leaf inside those bytes
|
|
87
92
|
* throws `Asn1Error` instead. Certificate-request parsing is `pki.schema.csr.parse`.
|
|
88
93
|
*
|
|
94
|
+
* Before the key is used, the request the spec describes is linted against the RFC 2986 profile and
|
|
95
|
+
* refused with `csr/profile-violation` if any rule grades it `error`, naming the rule and its clause.
|
|
96
|
+
*
|
|
89
97
|
* @opts
|
|
90
98
|
* - `pem` (boolean) -- return a PEM `CERTIFICATE REQUEST` string instead of DER.
|
|
91
99
|
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
|
|
92
100
|
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
101
|
+
* - `profile` (string) -- a second lint profile to hold the request to beside RFC 2986, named from
|
|
102
|
+
* `pki.lint.profiles()`. `"none"` runs no rules at all and emits what the spec describes, which is
|
|
103
|
+
* how a deliberately non-conforming request is produced.
|
|
93
104
|
* @example
|
|
94
105
|
* var pair = await pki.key.generate("Ed25519");
|
|
95
106
|
* var signerSpki = await pki.key.export(pair.publicKey);
|
|
@@ -111,6 +122,7 @@ function _sign(spec, key, opts) {
|
|
|
111
122
|
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("csr/bad-input", "the certification-request spec must be an object");
|
|
112
123
|
guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "csr/bad-input", "unknown spec field ");
|
|
113
124
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "csr/bad-input", "pki.csr.sign has an unknown option ");
|
|
125
|
+
lint.assertKnownBuildProfile("csr", opts.profile, _err, "csr/bad-input");
|
|
114
126
|
var wrapped = key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key;
|
|
115
127
|
if (wrapped) guard.identifier.assertKnownKeys(key, KNOWN_KEY_WRAPPER_KEYS, _err, "csr/bad-input", "unknown field in the { key } wrapper (a signing option belongs in the third argument): ");
|
|
116
128
|
var signingKey = wrapped ? key.key : key;
|
|
@@ -151,6 +163,9 @@ function _sign(spec, key, opts) {
|
|
|
151
163
|
var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(subjectSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
|
|
152
164
|
|
|
153
165
|
var criDer = b.sequence([b.integer(0n), subjectDer, b.raw(subjectSpki), _implicitSetOf0(attrs)]);
|
|
166
|
+
lint.assertBuildClean("csr",
|
|
167
|
+
b.sequence([criDer, scheme.sigAlgId, b.bitString(GATE_PLACEHOLDER_SIG, 0)]),
|
|
168
|
+
opts.profile, _err, "csr/profile-violation");
|
|
154
169
|
|
|
155
170
|
return signScheme.signOverTbs(scheme, signingKey, criDer, _signE).then(function (sig) {
|
|
156
171
|
return Promise.resolve(_b.assertSignatureVerifies(criDer, sig, subjectSpki, scheme)).then(function () {
|
package/lib/inspect.js
CHANGED
|
@@ -648,8 +648,9 @@ function _crlExtension(ext, pad) {
|
|
|
648
648
|
* var key = await pki.key.export(pair.privateKey);
|
|
649
649
|
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
650
650
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
651
|
-
* extensions: { basicConstraints: { cA: true }, keyUsage: ["cRLSign"] } }, { key: key });
|
|
652
|
-
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
651
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
|
|
652
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
653
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
|
|
653
654
|
* { cert: caCert, key: key });
|
|
654
655
|
* pki.inspect.crl(crlDer).split("\n")[0]; // "Certificate Revocation List (CRL):"
|
|
655
656
|
*/
|
package/lib/lint.js
CHANGED
|
@@ -363,6 +363,7 @@ function _criticalityRule(name, id, severity, citation, message) {
|
|
|
363
363
|
var RFC5280_RULES = [
|
|
364
364
|
{
|
|
365
365
|
id: "lint/rfc5280/signature-empty", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.1.3",
|
|
366
|
+
readsSignatureValue: true,
|
|
366
367
|
/** @internal The strict parser admits an empty BIT STRING, since one is well-formed DER; a
|
|
367
368
|
* signatureValue that contains no signature is not the field section 4.1.1.3 defines. */
|
|
368
369
|
message: "the signatureValue contains no signature",
|
|
@@ -3949,7 +3950,7 @@ var RFC9881_RULES = [
|
|
|
3949
3950
|
},
|
|
3950
3951
|
{
|
|
3951
3952
|
id: "lint/rfc9881/mldsa-signature-length", severity: "error", source: "rfc9881",
|
|
3952
|
-
citation: "RFC 9881 4 / FIPS 204",
|
|
3953
|
+
citation: "RFC 9881 4 / FIPS 204", readsSignatureValue: true,
|
|
3953
3954
|
/** @internal The signature is the FIPS 204 value carried directly in the BIT STRING, and FIPS 204
|
|
3954
3955
|
* fixes its length per parameter set as it fixes the public key's. So a signatureValue of any other
|
|
3955
3956
|
* length is not a signature of the algorithm the identifier names, whatever a verifier would later
|
|
@@ -4218,6 +4219,7 @@ function _criticalityRows(list, table, what) {
|
|
|
4218
4219
|
var RFC5280_CRL_RULES = [
|
|
4219
4220
|
{
|
|
4220
4221
|
id: "lint/rfc5280-crl/signature-empty", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.1.3",
|
|
4222
|
+
readsSignatureValue: true,
|
|
4221
4223
|
message: "the signatureValue contains no signature",
|
|
4222
4224
|
check: function (crl) { return crl.signatureValue.bytes.length === 0 ? true : null; },
|
|
4223
4225
|
},
|
|
@@ -4702,7 +4704,7 @@ function _cnsaSignatureRow(id, citation, reader) {
|
|
|
4702
4704
|
* registry passes returns the length and whether it is octet-aligned. */
|
|
4703
4705
|
function _cnsaSignatureLengthRow(id, citation, algReader, lengthReader) {
|
|
4704
4706
|
return {
|
|
4705
|
-
id: id, severity: "error", source: "cnsa-2.0", citation: citation,
|
|
4707
|
+
id: id, severity: "error", source: "cnsa-2.0", citation: citation, readsSignatureValue: true,
|
|
4706
4708
|
message: "the signature is not the length FIPS 204 fixes for its algorithm",
|
|
4707
4709
|
appliesTo: function (artifact, ctx) {
|
|
4708
4710
|
var sig = algReader(artifact);
|
|
@@ -5260,6 +5262,7 @@ function _placementRow(name, placed, where) {
|
|
|
5260
5262
|
var RFC6960_OCSP_RULES = [
|
|
5261
5263
|
{
|
|
5262
5264
|
id: "lint/rfc6960/signature-empty", severity: "error", source: "rfc6960", citation: "RFC 6960 2.2",
|
|
5265
|
+
readsSignatureValue: true,
|
|
5263
5266
|
message: "a definitive response shall be digitally signed; the signature BIT STRING is empty",
|
|
5264
5267
|
check: function (resp) { return _ocspBasic(resp).signature.length === 0 ? true : null; },
|
|
5265
5268
|
},
|
|
@@ -7270,6 +7273,126 @@ function profiles() {
|
|
|
7270
7273
|
return out;
|
|
7271
7274
|
}
|
|
7272
7275
|
|
|
7276
|
+
/** @internal The rules the build-time gate does not refuse on because the SPEC does not place the
|
|
7277
|
+
* requirement on the issuer, keyed by the last segment of the rule id so one entry answers for every
|
|
7278
|
+
* profile carrying the rule: a certificate reports lint/rfc5280/unknown-critical-extension and a
|
|
7279
|
+
* revocation list reports lint/rfc5280-crl/unknown-critical-extension, and the two state one
|
|
7280
|
+
* requirement. The value is the reason, read by the gate's conformance vectors rather than restated.
|
|
7281
|
+
*
|
|
7282
|
+
* The other reason a rule cannot apply, that it reads a signature the gate has not produced yet, is
|
|
7283
|
+
* not a name here: those rules declare `readsSignatureValue` where they are defined, and
|
|
7284
|
+
* SIGNATURE_VALUE_RULES below derives the set from the same registries `rules()` walks. A rule that
|
|
7285
|
+
* starts reading the signature value therefore exempts itself, and a vector measures the declared set
|
|
7286
|
+
* against the rules whose verdict actually moves with the signature. */
|
|
7287
|
+
var BUILD_GATE_EXEMPT = Object.assign(Object.create(null), {
|
|
7288
|
+
"unknown-critical-extension": "RFC 5280 sec. 4.2 places this requirement on \"a certificate-using system\", so the rule reports what a relying party owes rather than what an issuer does. It covers two artifacts an issuer may conform in emitting: one carrying a critical extension the specification does not identify, which the same section permits a conforming CA to support, and one carrying an extension the specification does identify but this toolkit's own path validation does not process, such as a critical policyMappings",
|
|
7289
|
+
});
|
|
7290
|
+
|
|
7291
|
+
/** @internal Every rule id whose verdict reads the signature VALUE, derived at load from the rule rows
|
|
7292
|
+
* themselves rather than listed. The gate runs on an artifact carrying a placeholder signature, so one
|
|
7293
|
+
* of these answers about the placeholder and not about what the caller is asking to emit. */
|
|
7294
|
+
var SIGNATURE_VALUE_RULES = (function () {
|
|
7295
|
+
var out = Object.create(null);
|
|
7296
|
+
Object.keys(BY_ARTIFACT).forEach(function (artifact) {
|
|
7297
|
+
Object.keys(BY_ARTIFACT[artifact]).forEach(function (name) {
|
|
7298
|
+
BY_ARTIFACT[artifact][name].forEach(function (r) {
|
|
7299
|
+
if (r.readsSignatureValue === true) out[r.id] = true;
|
|
7300
|
+
});
|
|
7301
|
+
});
|
|
7302
|
+
});
|
|
7303
|
+
return out;
|
|
7304
|
+
})();
|
|
7305
|
+
|
|
7306
|
+
/** @internal The profile whose rows an artifact is held to whatever the caller names. RFC 5280 states
|
|
7307
|
+
* the contract a certificate and a revocation list are written to and RFC 2986 the one a request is,
|
|
7308
|
+
* so these run on every authoring call and `opts.profile` names rows beside them. */
|
|
7309
|
+
var BUILD_GATE_ALWAYS = Object.assign(Object.create(null), {
|
|
7310
|
+
certificate: "rfc5280", csr: "rfc2986", crl: "rfc5280-crl",
|
|
7311
|
+
});
|
|
7312
|
+
|
|
7313
|
+
var BUILD_GATE_VERB = Object.assign(Object.create(null), {
|
|
7314
|
+
certificate: certificate, csr: csr, crl: crl,
|
|
7315
|
+
});
|
|
7316
|
+
|
|
7317
|
+
/** @internal The profile name that runs no rules. A toolkit that cannot emit a non-conforming artifact
|
|
7318
|
+
* cannot build the corpus its own linter is tested against, and an operator reproducing a real
|
|
7319
|
+
* misissuance needs the same. The escape is a name the caller writes rather than an absence, so a
|
|
7320
|
+
* non-conforming artifact is one somebody asked for: the gate is closed by default and every refusal
|
|
7321
|
+
* names this among the profiles it would accept. */
|
|
7322
|
+
var BUILD_GATE_NONE = "none";
|
|
7323
|
+
|
|
7324
|
+
/** @internal The exemption table, copied so a caller cannot edit the gate's own line. The gate's
|
|
7325
|
+
* vectors read the reasons from here, which is why it is reachable at all. */
|
|
7326
|
+
function buildGateExemptions() {
|
|
7327
|
+
return Object.assign(Object.create(null), BUILD_GATE_EXEMPT);
|
|
7328
|
+
}
|
|
7329
|
+
|
|
7330
|
+
/** @internal The rule ids that declared they read the signature value, for the vector that measures
|
|
7331
|
+
* this set against the rules whose verdict actually moves when the signature does. */
|
|
7332
|
+
function buildGateSignatureRules() {
|
|
7333
|
+
return Object.assign(Object.create(null), SIGNATURE_VALUE_RULES);
|
|
7334
|
+
}
|
|
7335
|
+
|
|
7336
|
+
/** @internal The profile a build-time gate always runs for this artifact, for a vector that derives
|
|
7337
|
+
* the expected rule set from the same name the gate uses. */
|
|
7338
|
+
function buildGateProfile(artifact) {
|
|
7339
|
+
return BUILD_GATE_ALWAYS[artifact] || null;
|
|
7340
|
+
}
|
|
7341
|
+
|
|
7342
|
+
function _buildGateTail(id) {
|
|
7343
|
+
var cut = id.lastIndexOf("/");
|
|
7344
|
+
return cut === -1 ? id : id.slice(cut + 1);
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
/**
|
|
7348
|
+
* @internal Refuse a profile name an authoring verb cannot run, under the CALLER's code, so
|
|
7349
|
+
* `pki.x509.sign` reports an unknown profile in its own domain as it reports every other bad option.
|
|
7350
|
+
* The sentence is the one `pki.lint.rules` gives for the same name, so asking the linter where a
|
|
7351
|
+
* profile lives and asking a builder to hold an artifact to it are answered alike.
|
|
7352
|
+
*/
|
|
7353
|
+
function assertKnownBuildProfile(artifact, profile, E, code) {
|
|
7354
|
+
if (profile == null || profile === BUILD_GATE_NONE) return;
|
|
7355
|
+
if (typeof profile !== "string") {
|
|
7356
|
+
throw E(code, "profile must be the name of a lint profile (known: " +
|
|
7357
|
+
Object.keys(BY_ARTIFACT[artifact]).join(", ") + ", " + BUILD_GATE_NONE + ")");
|
|
7358
|
+
}
|
|
7359
|
+
if (!Object.prototype.hasOwnProperty.call(BY_ARTIFACT[artifact], profile)) {
|
|
7360
|
+
throw E(code, _profileRefusal(profile, artifact).message + ", or \"" + BUILD_GATE_NONE +
|
|
7361
|
+
"\" to emit what the spec describes without holding it to a profile");
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
|
|
7365
|
+
/**
|
|
7366
|
+
* @internal Refuse to emit an artifact this toolkit's own linter grades `error` or worse. The caller
|
|
7367
|
+
* passes the artifact it is about to sign, carrying a placeholder signature, and the gate runs the
|
|
7368
|
+
* always-on profile for that artifact plus the one the caller named. A finding whose rule id is in
|
|
7369
|
+
* BUILD_GATE_EXEMPT is passed over, and every other one throws under the caller's code naming the
|
|
7370
|
+
* rule and its citation.
|
|
7371
|
+
*
|
|
7372
|
+
* Standards are the contract, so this runs before the signing key is reached: a certificate that was
|
|
7373
|
+
* signed and then discarded is still one a key signed and an audit log recorded. A caller naming the
|
|
7374
|
+
* profile "none" gets no rules and whatever the spec describes.
|
|
7375
|
+
*/
|
|
7376
|
+
function assertBuildClean(artifact, der, profile, E, code) {
|
|
7377
|
+
if (profile === BUILD_GATE_NONE) return;
|
|
7378
|
+
var verb = BUILD_GATE_VERB[artifact];
|
|
7379
|
+
var reports = [verb(der, { profile: BUILD_GATE_ALWAYS[artifact] })];
|
|
7380
|
+
if (profile != null && profile !== BUILD_GATE_ALWAYS[artifact]) {
|
|
7381
|
+
reports.push(verb(der, { profile: profile }));
|
|
7382
|
+
}
|
|
7383
|
+
for (var r = 0; r < reports.length; r++) {
|
|
7384
|
+
var found = reports[r].findings;
|
|
7385
|
+
for (var i = 0; i < found.length; i++) {
|
|
7386
|
+
var f = found[i];
|
|
7387
|
+
if (f.severity !== "error" && f.severity !== "fatal") continue;
|
|
7388
|
+
if (SIGNATURE_VALUE_RULES[f.id]) continue;
|
|
7389
|
+
if (BUILD_GATE_EXEMPT[_buildGateTail(f.id)]) continue;
|
|
7390
|
+
throw E(code, "this spec describes " + _article(ARTIFACT_NOUN[artifact]) + " " +
|
|
7391
|
+
ARTIFACT_NOUN[artifact] + " that violates " + f.id + " (" + f.citation + "): " + f.message);
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
}
|
|
7395
|
+
|
|
7273
7396
|
module.exports = {
|
|
7274
7397
|
certificate: certificate,
|
|
7275
7398
|
csr: csr,
|
|
@@ -7280,4 +7403,9 @@ module.exports = {
|
|
|
7280
7403
|
attrcert: attrcert,
|
|
7281
7404
|
rules: rules,
|
|
7282
7405
|
profiles: profiles,
|
|
7406
|
+
assertBuildClean: assertBuildClean,
|
|
7407
|
+
assertKnownBuildProfile: assertKnownBuildProfile,
|
|
7408
|
+
buildGateExemptions: buildGateExemptions,
|
|
7409
|
+
buildGateSignatureRules: buildGateSignatureRules,
|
|
7410
|
+
buildGateProfile: buildGateProfile,
|
|
7283
7411
|
};
|
package/lib/schema-crl.js
CHANGED
|
@@ -168,7 +168,8 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
|
|
|
168
168
|
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
169
169
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
170
170
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
|
|
171
|
-
* var der = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
171
|
+
* var der = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
172
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n,
|
|
172
173
|
* revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z") }] },
|
|
173
174
|
* { cert: caCert, key: key });
|
|
174
175
|
* var crl = pki.schema.crl.parse(der);
|
|
@@ -197,8 +198,9 @@ var parse = pkix.makeRecordingParser({ pemLabel: "X509 CRL", PemError: PemError,
|
|
|
197
198
|
* var key = await pki.key.export(pair.privateKey);
|
|
198
199
|
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
199
200
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
200
|
-
* extensions: { basicConstraints: { cA: true }, keyUsage: ["cRLSign"] } }, { key: key });
|
|
201
|
-
* var pemText = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
201
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
|
|
202
|
+
* var pemText = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
203
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
|
|
202
204
|
* { cert: caCert, key: key }, { pem: true });
|
|
203
205
|
* var der = pki.schema.crl.pemDecode(pemText);
|
|
204
206
|
*/
|
|
@@ -220,8 +222,9 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "X509 CRL
|
|
|
220
222
|
* var key = await pki.key.export(pair.privateKey);
|
|
221
223
|
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
222
224
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
223
|
-
* extensions: { basicConstraints: { cA: true }, keyUsage: ["cRLSign"] } }, { key: key });
|
|
224
|
-
* var der = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
225
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
|
|
226
|
+
* var der = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
227
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
|
|
225
228
|
* { cert: caCert, key: key });
|
|
226
229
|
* var pem = pki.schema.crl.pemEncode(der);
|
|
227
230
|
*/
|
|
@@ -329,7 +332,8 @@ var DOOR = pkix.extensionDoor({
|
|
|
329
332
|
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
330
333
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
331
334
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
|
|
332
|
-
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
335
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
336
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 7n,
|
|
333
337
|
* revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }] },
|
|
334
338
|
* { cert: caCert, key: key });
|
|
335
339
|
* var rows = pki.schema.crl.decodeExtensions(crlDer);
|
|
@@ -362,7 +366,8 @@ var decodeExtensions = DOOR.decodeExtensions;
|
|
|
362
366
|
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
363
367
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
364
368
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
|
|
365
|
-
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
369
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
|
|
370
|
+
* nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 7n,
|
|
366
371
|
* revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }] },
|
|
367
372
|
* { cert: caCert, key: key });
|
|
368
373
|
* var e = pki.schema.crl.parse(crlDer).revokedCertificates[0].crlEntryExtensions[0];
|
package/lib/x509-sign.js
CHANGED
|
@@ -31,6 +31,7 @@ var schema = require("./schema-engine");
|
|
|
31
31
|
var pkix = require("./schema-pkix");
|
|
32
32
|
var pkiBuild = require("./pki-build");
|
|
33
33
|
var ct = require("./ct");
|
|
34
|
+
var lint = require("./lint");
|
|
34
35
|
|
|
35
36
|
var CertificateError = frameworkError.CertificateError;
|
|
36
37
|
var NS = pkix.makeNS("x509", CertificateError, oid);
|
|
@@ -47,7 +48,12 @@ var KNOWN_SPEC_KEYS = intrinsic.assign(intrinsic.create(null), {
|
|
|
47
48
|
});
|
|
48
49
|
var KNOWN_ISSUER_CERT_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, cert: 1 });
|
|
49
50
|
var KNOWN_ISSUER_EXPLICIT_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, name: 1, publicKey: 1 });
|
|
50
|
-
var KNOWN_SIGN_OPTS = intrinsic.assign(intrinsic.create(null), { digestAlgorithm: 1, pem: 1, pss: 1 });
|
|
51
|
+
var KNOWN_SIGN_OPTS = intrinsic.assign(intrinsic.create(null), { digestAlgorithm: 1, pem: 1, pss: 1, profile: 1 });
|
|
52
|
+
|
|
53
|
+
/** @internal The signature the build-time gate lints against, standing in for one that does not exist
|
|
54
|
+
* until the key is used. It is non-empty so the gate is not answering a rule about its own placeholder,
|
|
55
|
+
* and the rules that read the signature VALUE are exempt in lint.js for the same reason. */
|
|
56
|
+
var GATE_PLACEHOLDER_SIG = intrinsic.bufferAlloc(64, 0xff);
|
|
51
57
|
|
|
52
58
|
var KNOWN_EXT_KEYS = intrinsic.assign(intrinsic.create(null), {
|
|
53
59
|
subjectKeyIdentifier: 1, authorityKeyIdentifier: 1, keyUsage: 1, keyUsageCritical: 1,
|
|
@@ -493,10 +499,19 @@ function _hasCriticalSan(extSpec) {
|
|
|
493
499
|
* marks it critical. The two name opposite ends of one exchange, so a spec naming both is refused. A
|
|
494
500
|
* client recovers the signed entry from the issued certificate with `pki.ct.x509CertEntry`.
|
|
495
501
|
*
|
|
502
|
+
* Before the key is used, the certificate the spec describes is linted against the RFC 5280 profile
|
|
503
|
+
* and refused with `x509/profile-violation` if any rule grades it `error`, naming the rule and its
|
|
504
|
+
* clause. Two rules are passed over: one reading the not-yet-written signature, and
|
|
505
|
+
* `lint/rfc5280/unknown-critical-extension`, whose requirement RFC 5280 sec. 4.2 places on a
|
|
506
|
+
* certificate-using system rather than on the issuer.
|
|
507
|
+
*
|
|
496
508
|
* @opts
|
|
497
509
|
* - `pem` (boolean) -- return a PEM `CERTIFICATE` string instead of DER.
|
|
498
510
|
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
|
|
499
511
|
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
512
|
+
* - `profile` (string) -- a second lint profile to hold the certificate to beside RFC 5280, named
|
|
513
|
+
* from `pki.lint.profiles()`. `"none"` runs no rules at all and emits what the spec describes,
|
|
514
|
+
* which is how a deliberately non-conforming certificate is produced.
|
|
500
515
|
* @example
|
|
501
516
|
* var pair = await pki.key.generate("Ed25519");
|
|
502
517
|
* var signerSpki = await pki.key.export(pair.publicKey);
|
|
@@ -530,6 +545,7 @@ function _sign(spec, issuer, opts) {
|
|
|
530
545
|
: "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
|
|
531
546
|
});
|
|
532
547
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "x509/bad-input", "pki.x509.sign has an unknown option ");
|
|
548
|
+
lint.assertKnownBuildProfile("certificate", opts.profile, _err, "x509/bad-input");
|
|
533
549
|
if (issuer.key == null) throw _err("x509/bad-input", "a signing key (issuer.key, a WebCrypto CryptoKey or a PKCS#8 private key DER/PEM) is required");
|
|
534
550
|
|
|
535
551
|
var spki = _reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the certified key)");
|
|
@@ -596,6 +612,9 @@ function _sign(spec, issuer, opts) {
|
|
|
596
612
|
tbsChildren.push(b.raw(spki));
|
|
597
613
|
if (exts.length) tbsChildren.push(b.explicit(3, b.sequence(exts)));
|
|
598
614
|
var tbsDer = b.sequence(tbsChildren);
|
|
615
|
+
lint.assertBuildClean("certificate",
|
|
616
|
+
b.sequence([tbsDer, scheme.sigAlgId, b.bitString(GATE_PLACEHOLDER_SIG, 0)]),
|
|
617
|
+
opts.profile, _err, "x509/profile-violation");
|
|
599
618
|
|
|
600
619
|
return signScheme.signOverTbs(scheme, issuer.key, tbsDer, _signE).then(function (sig) {
|
|
601
620
|
return Promise.resolve(_assertCertVerifies(tbsDer, sig, issuerSpki, scheme)).then(function () {
|
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:3eb9063d-93f7-4a44-bdff-52a992c9a1e7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-28T21:19:37.849Z",
|
|
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.28",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.28",
|
|
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.28",
|
|
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.28",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|