@blamejs/pki 0.5.13 → 0.5.15
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 +34 -0
- package/README.md +2 -2
- package/lib/attrcert-sign.js +511 -2
- package/lib/attrcert-verify.js +21 -0
- package/lib/crl-verify.js +11 -20
- package/lib/crmf-sign.js +125 -1
- package/lib/crmf-verify.js +26 -0
- package/lib/csr-sign.js +2 -1
- package/lib/csr-verify.js +10 -27
- package/lib/guard-parsed.js +14 -1
- package/lib/path-validate.js +10 -0
- package/lib/schema-attrcert.js +19 -1
- package/lib/schema-crmf.js +5 -2
- package/lib/verify-seam.js +47 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.5.15 — 2026-08-19
|
|
8
|
+
|
|
9
|
+
`pki.attrcert.verify` checks an attribute certificate against the RFC 5755 validation rules, so a consumer reading its privilege attributes is reading ones an issuer actually granted.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.attrcert.verify(ac, issuer, opts)` performs the checks of RFC 5755 sec. 5 that 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 with its algorithm-confusion and EdDSA low-order-point gates; the AC naming the issuer this verifier trusts; the evaluation instant lying within the validity, where equality with either bound succeeds as the section states; the sec. 4.3.2 targeting rule, where an AC that names targets is refused at a verifier it does not name; 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 an extension parsed but never evaluated is not supported: targeting is processed and an audit identity states no rule that rejects, while `aaControls` and `acProxying` carry constraints this verb does not evaluate, so a critical one is refused, and a critical extension defined in future defaults to refused. Targeting compares each GeneralName form under its own matching rule: a dNSName folds case across the whole name (RFC 5280 sec. 7.2), a mailbox matches its local-part exactly and its host-part case-insensitively (sec. 7.5), and a directoryName goes through the same distinguished-name comparison the rest of the toolkit uses. A URI compares as encoded, since sec. 7.4 makes URI equality a full RFC 3987 normalization that a partial version could get wrong in the accepting direction; a form with no comparison here reports the check as not performed rather than as a pass. The third Target alternative, targetCert, is one sec. 4.3.2 says "MUST NOT be used", so an attribute certificate carrying one is refused before any match is considered, and a matching entry beside it does not rescue it -- the issuer broke a MUST NOT of its own profile, and letting the verdict turn on which entries rode alongside the forbidden one would leave the outcome to whoever assembled the certificate.
|
|
14
|
+
- `issuer` is `{ name, publicKey }` and is required. Section 5 item 4 makes trusting an AC issuer the verifier's own configuration, so it is an argument this verb refuses to infer from the certificate that wants to be trusted. `issuer.name` takes every form `pki.attrcert.sign` accepts for the issuing authority -- a string, an array of RDNs, or raw `Name` DER -- and is compared as a distinguished name under RFC 5280 section 7.1, so an attribute certificate issued under a multi-RDN authority name is verifiable under that same name. An empty distinguished name names no issuer and is refused, whichever form it arrives in.
|
|
15
|
+
- Revocation is RFC 5755 section 6, outside section 5's seven rules, and it gates every pass. This verb implements the section's "never revoke" scheme, which AC users MUST support: it holds no revocation evidence and follows no pointer out of the certificate. The section states what that obliges -- "If only the 'never revoke' scheme is supported, then all ACs that do not contain a noRevAvail extension, MUST be rejected" -- because an issuer that omits `noRevAvail` is stating that revocation status checks are supported, so a verdict that skipped one would grant privileges the issuer expected to be able to withdraw. An attribute certificate carrying `noRevAvail` verifies and reports `revocationChecked: true` with `noRevAvail: true`. One without it is refused unless the caller passes `opts.revocationStatus` (`"notRevoked"` or `"revoked"`), the status they established through the section's "pointer in AC" scheme; the certificate's own `crlDistributionPoints` and `authorityInfoAccess` reach them on the verdict's `extensions` to follow. The two schemes are alternatives, and section 6 closes by saying so -- "An AC MUST NOT contain both a noRevAvail extension and a 'pointer in AC'" -- so an attribute certificate carrying both tells a verifier two different things about its own revocation and is refused however the caller answers.
|
|
16
|
+
- `opts.time` and `opts.target` are read at the call, before signature verification suspends it. A verdict assembled from options re-read after the signature settles would answer for whatever the caller last wrote, so a caller could hand in an expired instant, replace it while the signature was being checked, and be told the attribute certificate was timely. Mutating the `Date` in place has the same effect and is closed the same way: the instant is taken at the call, and the target is decoded there. `issuer.publicKey` is copied at the call for the same reason. The signature engine holds that buffer across the suspension, and a composite arm keeps slices of it and imports them only once its digest settles, so a caller could otherwise hand in an unrelated key of the same length, overwrite it with the real issuer's while the digest ran, and be told the certificate verified under the key they named.
|
|
17
|
+
- The verdict is `{ verified, signatureValid, validityChecked, targetingChecked, revocationChecked, noRevAvail, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }`, its fields re-derived from the signed bytes. The two checks needing certificates this verb is not given -- the holder's own certificate and chain, and the AC issuer's chain and sec. 4.5 profile -- report `false` in their own slots rather than leaving their absence to read as a pass; run those through `pki.path.validate` with the certificates you hold. Omitting `opts.time` leaves the validity question unasked, which reports `validityChecked: false` and never `verified: true`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `pki.attrcert.sign` refuses to issue an attribute certificate carrying both a `noRevAvail` extension and a `crlDistributionPoints` or `authorityInfoAccess` revocation pointer, which RFC 5755 section 6 says an AC MUST NOT do. The rule is held over the extensions the certificate ends up with, so it binds the named-extension form and the pre-encoded DER form alike.
|
|
22
|
+
- `pki.schema.attrcert.parse` now records the bytes it read, joining the certificate, CRL, CMS, certification-request and certificate-request-message parsers, and `pki.attrcert.verify` accepts only a result carrying that record. A parsed attribute certificate presents the signed byte range and the attributes granting the privileges as separate properties, so a rebuilt one can hold a genuine issuer's signed bytes beside substituted attributes. As with those parsers, the raw byte views on a parsed result now read from the parser's own copy of the input; passing DER or PEM is unaffected.
|
|
23
|
+
|
|
24
|
+
## v0.5.14 — 2026-08-19
|
|
25
|
+
|
|
26
|
+
`pki.crmf.verifyPop` checks the proof of possession on an inbound certificate request message, the CRMF counterpart to the PKCS#10 check that shipped in 0.5.13.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `pki.crmf.verifyPop(messages)` verifies the RFC 4211 proof of possession on each `CertReqMsg`, returning one verdict per message plus a top-level `verified` that is true only when every message carried a proof that held. For the `signature` proof the covered bytes are the ones the RFC names: the DER of `poposkInput` when that field is present, and the DER of `certReq` when it is absent (sec. 4.1, and the ASN.1 module, which is where the two readings of that sentence are settled). Verification composes the one path-validation signature engine, with the same algorithm-confusion (RFC 9814 sec. 4) and EdDSA low-order-point gates as the certificate and CRL paths.
|
|
31
|
+
- The proofs that cannot be checked from the message are reported as such. `raVerified` is an RA's assertion that it confirmed possession out of band, so it yields `verified: false` with `method: "raVerified"`, and a caller who trusts that RA opts in by reading `method`. `keyEncipherment` and `keyAgreement` complete over a later protocol exchange, or need the CA's decryption key, so they yield `verified: false` naming the arm. Each verdict carries only what its preimage covers, re-derived from the message's own bytes, so a CA issues from what was checked. `publicKey` is the key possession was proven for. `subject` is the requested name when the signature was over `certReq`, which covers the whole template; a `poposkInput` preimage covers the key and the sender alone, so a subject sitting beside it in the message is unsigned and is withheld with `subjectBound: false` rather than reported next to a passing verdict.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `pki.schema.crmf.parse` now records the bytes it read, joining the certificate, CRL, CMS and certification-request parsers, and `pki.crmf.verifyPop` accepts only a result carrying that record. A parsed message set presents the byte range a proof covers and the template a certificate would be issued from as separate properties, so a rebuilt one can hold a genuine requester's signed range beside a substituted subject. As with those parsers, the raw byte views on a parsed result now read from the parser's own copy of the input; passing DER is unaffected, and a parse result used as-is still works.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- The `pki.csr.sign` example passed `subject: "CN=device-42"`, which asks for a commonName whose value is the string `CN=device-42` and so issues `CN=CN=device-42`. A bare string is the commonName value throughout the toolkit; the example now says so and passes `"device-42"`.
|
|
40
|
+
|
|
7
41
|
## v0.5.13 — 2026-08-19
|
|
8
42
|
|
|
9
43
|
`pki.csr.verify` checks an inbound certification request's proof of possession, so a CA built on this toolkit can tell that the requester holds the key they are asking it to certify.
|
package/README.md
CHANGED
|
@@ -231,8 +231,8 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
231
231
|
| `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-policy tree. 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. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker` |
|
|
232
232
|
| `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. 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), subject alternative names, and certificate policies from the spec, taking any other extension as pre-encoded DER. 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. Parsing stays at `pki.schema.x509.parse` — `sign` |
|
|
233
233
|
| `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, or an array of pre-encoded Extension DER), 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` |
|
|
234
|
-
| `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER) each with its RFC 5755 criticality. 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`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
|
|
235
|
-
| `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. 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`. Parsing stays at `pki.schema.crmf.parse` — `build` |
|
|
234
|
+
| `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER) each with its RFC 5755 criticality. 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` |
|
|
235
|
+
| `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. 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` |
|
|
236
236
|
| `pki.cmp` | CMP message building, transfer, and verification (RFC 9810). `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` and `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata; `message.body` is a single-key object naming the arm — request-side `ir`, `cr`, `kur`, `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, and responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`. Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, or `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a legacy or KEM MAC algorithm, an omitted keyLength, or a SHA-1 PRF, and returns a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signer certificate is fully path-validated (RFC 5280 §6.1 plus the RFC 9483 §3.2 `keyUsage.digitalSignature` gate) at a trusted current time, or an explicit `opts.time` for historical verification and never the message's self-asserted `messageTime`, before it is reported trusted; without one the verdict is crypto-only and the signer certificate is surfaced to anchor. `session(opts)` returns a stateful enrollment session whose `enroll(request)` drives a full `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport, composing `build`, `transfer`, and `verify`. Every response is protection-verified, signer-trusted, and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` and echoed-`recipNonce` chain) before its body is read, with a bounded `pollReq` / `pollRep` loop for a `waiting` status and a `certConf` / `pkiConf` (or implicit) confirmation carrying an explicit `hashAlg` for a signature algorithm that does not convey its hash. It returns a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `session` |
|
|
237
237
|
| `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, the RFC fixes per-extension criticality, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine, algorithm-confusion and EdDSA low-order gates included; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber, opts)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. A CRL speaks for a span as well as a set, so `opts.time` states the instant the question is asked at and a list that has stopped speaking for it — `nextUpdate` passed, `thisUpdate` later, or no `nextUpdate` stated at all — is refused rather than read as a clean bill of health; without `opts.time` the lookup is structural and an absent serial means only that it is not on that list. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base, performs the §6.3.3 correspondence, and decides currency against the material it fetched. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign`, `verify`, `isRevoked` |
|
|
238
238
|
| `pki.key` | Key-material lifecycle (RFC 5958 / RFC 8018). `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 with AES-CBC-Pad), where `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`. The plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo`, re-validated through `pki.schema.pkcs8.parse`: only PBES2 / PBKDF2 / AES-CBC is accepted (PBES1, PBMAC1, and scrypt are refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), and a malformed parameter set or wrong-length IV is a distinct typed error. Because a MAC-less PBES2-CBC decrypt must not become a padding oracle (RFC 8018 §8), a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` and `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo. The key may come from the platform's WebCrypto or from a separately installed copy of this toolkit, and is exported through whichever holds its material; a non-extractable key, or one whose implementation keeps its material out of reach, is refused with that as the reason. Encoding is delegated to WebCrypto, so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters, and an ambiguous RSA or EC import requires `opts.algorithm`. `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards and Montgomery curves, and the FIPS post-quantum ML-DSA and ML-KEM; `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM, with a typed `KeyError` on failure. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt`, `decrypt`, `export`, `import`, `generate`, `publicFromPrivate` |
|
package/lib/attrcert-sign.js
CHANGED
|
@@ -33,6 +33,9 @@ var x509 = require("./schema-x509");
|
|
|
33
33
|
var signScheme = require("./sign-scheme");
|
|
34
34
|
var guard = require("./guard-all");
|
|
35
35
|
var pkix = require("./schema-pkix");
|
|
36
|
+
var schema = require("./schema-engine");
|
|
37
|
+
require("./path-validate"); // side-effect: path-validate injects its signature engine into attrcert-verify at load
|
|
38
|
+
var attrcertVerify = require("./attrcert-verify");
|
|
36
39
|
var pkiBuild = require("./pki-build");
|
|
37
40
|
var frameworkError = require("./framework-error");
|
|
38
41
|
|
|
@@ -364,6 +367,21 @@ function _buildAttributes(attrSpec) {
|
|
|
364
367
|
|
|
365
368
|
// extensions ::= SEQUENCE OF Extension (RFC 5755 sec. 4.2.9): the recognized object form, or an array
|
|
366
369
|
// of pre-encoded Extension DER. At most one instance of a particular extension OID. Returns a universal
|
|
370
|
+
// RFC 5755 sec. 6 defines two revocation schemes and closes by making them exclusive: "An AC MUST
|
|
371
|
+
// NOT contain both a noRevAvail extension and a 'pointer in AC'." Held over the OIDs an AC ends up
|
|
372
|
+
// carrying, so BOTH extension forms meet it -- the object form and the pre-encoded array form, which
|
|
373
|
+
// is the one that can name a pointer at all. `seen` is the OID-keyed set each branch already builds.
|
|
374
|
+
function _assertRevocationSchemesExclusive(seen) {
|
|
375
|
+
if (!Object.prototype.hasOwnProperty.call(seen, O("noRevAvail"))) return;
|
|
376
|
+
var names = Object.keys(REVOCATION_POINTERS);
|
|
377
|
+
for (var i = 0; i < names.length; i++) {
|
|
378
|
+
if (Object.prototype.hasOwnProperty.call(seen, names[i])) {
|
|
379
|
+
throw _err("attrcert/bad-input", "an attribute certificate must not carry both noRevAvail and a " +
|
|
380
|
+
REVOCATION_POINTERS[names[i]] + " revocation pointer (RFC 5755 sec. 6)");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
367
385
|
// SEQUENCE OF Extension, or null when none are requested.
|
|
368
386
|
function _buildExtensions(extSpec, aaSpki) {
|
|
369
387
|
if (extSpec == null) return null;
|
|
@@ -392,6 +410,7 @@ function _buildExtensions(extSpec, aaSpki) {
|
|
|
392
410
|
} catch (err) { if (err instanceof AttrCertError) throw err; throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
|
|
393
411
|
return b.raw(der);
|
|
394
412
|
});
|
|
413
|
+
_assertRevocationSchemesExclusive(seenA);
|
|
395
414
|
return b.sequence(exts);
|
|
396
415
|
}
|
|
397
416
|
if (typeof extSpec !== "object") throw _err("attrcert/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
|
|
@@ -406,6 +425,7 @@ function _buildExtensions(extSpec, aaSpki) {
|
|
|
406
425
|
seen[eOid] = true;
|
|
407
426
|
out.push(_b.ext(eOid, meta.critical, _extensionValue(k, extSpec[k], aaSpki)));
|
|
408
427
|
});
|
|
428
|
+
_assertRevocationSchemesExclusive(seen);
|
|
409
429
|
if (!out.length) return null;
|
|
410
430
|
return b.sequence(out);
|
|
411
431
|
}
|
|
@@ -429,7 +449,10 @@ function _buildExtensions(extSpec, aaSpki) {
|
|
|
429
449
|
* targetInformation / noRevAvail / aaControls / acProxying / authorityKeyIdentifier, or an array of
|
|
430
450
|
* pre-encoded Extension DER). `issuer` is the signing AA: `{ cert, key }` (the AA certificate DER/PEM
|
|
431
451
|
* and its private key) or `{ name, publicKey, key }` (an explicit issuer DN, AA SPKI DER, and key); an
|
|
432
|
-
* attribute certificate is never self-signed.
|
|
452
|
+
* attribute certificate is never self-signed. RFC 5755 section 6 defines two revocation schemes and
|
|
453
|
+
* makes them exclusive -- "An AC MUST NOT contain both a noRevAvail extension and a 'pointer in AC'"
|
|
454
|
+
* -- so `noRevAvail` beside a `crlDistributionPoints` or `authorityInfoAccess` extension is refused,
|
|
455
|
+
* in the named form and the pre-encoded form alike. The signature algorithm is resolved from the AA key (RSA
|
|
433
456
|
* PKCS#1 v1.5 or PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm), and the signature is verified
|
|
434
457
|
* under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE
|
|
435
458
|
* CERTIFICATE` with `opts.pem`. Malformed input throws a typed `AttrCertError`; where the spec carries
|
|
@@ -517,4 +540,490 @@ function _sign(spec, issuer, opts) {
|
|
|
517
540
|
});
|
|
518
541
|
}
|
|
519
542
|
|
|
520
|
-
|
|
543
|
+
var KNOWN_VERIFY_OPTS = { time: 1, target: 1, revocationStatus: 1 };
|
|
544
|
+
// The two answers a caller can give for RFC 5755 sec. 6, spelled out so a typo is refused instead of
|
|
545
|
+
// read as the safe one. Built without a prototype, so an inherited name cannot answer the lookup.
|
|
546
|
+
var REVOCATION_STATUS = Object.create(null);
|
|
547
|
+
REVOCATION_STATUS.notRevoked = 1;
|
|
548
|
+
REVOCATION_STATUS.revoked = 1;
|
|
549
|
+
var _NAME_SCHEMA = pkix.name(NS);
|
|
550
|
+
|
|
551
|
+
// A Name DER, read through the one Name reader, so every distinguished name this verb compares is
|
|
552
|
+
// in the shape guard.name.dnEqual takes.
|
|
553
|
+
function _readName(der, what) {
|
|
554
|
+
try { return schema.walk(_NAME_SCHEMA, asn1.decode(der), NS).result; }
|
|
555
|
+
catch (e) {
|
|
556
|
+
if (e instanceof AttrCertError) throw e;
|
|
557
|
+
throw _err("attrcert/bad-input", what + " is not a readable distinguished name", e);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// The AC issuer's distinguished name. The parser has already established that issuerName holds
|
|
562
|
+
// exactly one non-empty directoryName [4] (RFC 5755 sec. 4.2.3) and surfaces it as a raw node, so
|
|
563
|
+
// the DN itself is one EXPLICIT unwrap away.
|
|
564
|
+
function _acIssuerName(parsed) {
|
|
565
|
+
var node = asn1.decode(parsed.issuer.v2Form.issuerName.names[0].bytes);
|
|
566
|
+
if (!node.children || node.children.length !== 1) {
|
|
567
|
+
throw _err("attrcert/bad-issuer-name", "the AC issuerName directoryName does not wrap exactly one Name");
|
|
568
|
+
}
|
|
569
|
+
return _readName(node.children[0].bytes, "the AC issuer name");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function _coerceAc(input) {
|
|
573
|
+
return guard.parsed.acceptDerived(input, "attributeCertificate", attrcert.parse, _err,
|
|
574
|
+
"attrcert/bad-input", "the attribute certificate");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// The AC issuer this caller directly trusts as an AC issuer (RFC 5755 sec. 5, item 4). That is a
|
|
578
|
+
// configuration decision the certificate cannot make for itself, so it is an argument, and an
|
|
579
|
+
// argument this verb refuses to infer.
|
|
580
|
+
function _resolveIssuer(issuer) {
|
|
581
|
+
if (issuer == null || typeof issuer !== "object" || Buffer.isBuffer(issuer)) {
|
|
582
|
+
throw _err("attrcert/bad-input", "an issuer { name, publicKey } is required -- RFC 5755 sec. 5 item 4 makes trusting an AC issuer the verifier's own configuration");
|
|
583
|
+
}
|
|
584
|
+
guard.identifier.assertKnownKeys(issuer, { name: 1, publicKey: 1 }, _err, "attrcert/bad-input",
|
|
585
|
+
"the issuer has an unknown key ");
|
|
586
|
+
if (issuer.publicKey == null) throw _err("attrcert/bad-input", "issuer.publicKey (the AC issuer's SubjectPublicKeyInfo DER) is required");
|
|
587
|
+
if (issuer.name == null) {
|
|
588
|
+
throw _err("attrcert/bad-input", "issuer.name (the distinguished name this verifier trusts as an AC issuer) is required");
|
|
589
|
+
}
|
|
590
|
+
// Every name form pki.attrcert.sign accepts for the issuing AA is a name this verb has to be able
|
|
591
|
+
// to trust: a string, an array of RDNs, or raw Name DER. The forms come from the one Name encoder
|
|
592
|
+
// both sides use, so an AC issued under a multi-RDN AA name is verifiable under that same name.
|
|
593
|
+
//
|
|
594
|
+
// The trusted name is compared as a distinguished name, never as a string: RFC 5280 sec. 7.1
|
|
595
|
+
// identity is not string equality, and guard.name owns that comparison. Encoding the caller's
|
|
596
|
+
// name and reading it back through the SAME Name reader the parser used puts both sides in the
|
|
597
|
+
// one shape dnEqual takes.
|
|
598
|
+
var trustedDer;
|
|
599
|
+
try { trustedDer = _b.encodeName(issuer.name); }
|
|
600
|
+
catch (e) {
|
|
601
|
+
if (e instanceof AttrCertError) throw e;
|
|
602
|
+
throw _err("attrcert/bad-input", "issuer.name is not a distinguished name", e);
|
|
603
|
+
}
|
|
604
|
+
var rdns = _readName(trustedDer, "issuer.name").rdns;
|
|
605
|
+
// Checked on the decoded name, so every input form is held to it. An empty DN names no issuer, and
|
|
606
|
+
// RFC 5755 sec. 4.2.3 requires the AC's own issuerName to be a non-empty distinguished name, so an
|
|
607
|
+
// empty trusted name could never match a conformant AC and would only mask the caller's mistake.
|
|
608
|
+
if (!rdns.length) {
|
|
609
|
+
throw _err("attrcert/bad-input", "issuer.name is an empty distinguished name, which names no AC issuer (RFC 5755 sec. 4.2.3)");
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
rdns: rdns,
|
|
613
|
+
// Copied, never viewed. Signature verification suspends this call, and the signature engine
|
|
614
|
+
// holds this buffer across that suspension -- a composite arm keeps slices of it and imports
|
|
615
|
+
// them only after its digest settles. A view would leave the trust key writable for that whole
|
|
616
|
+
// window, so a caller could hand in an unrelated key of the same length, overwrite it with the
|
|
617
|
+
// real issuer's while the digest ran, and be told the certificate verified under the key they
|
|
618
|
+
// named. The verdict answers for the key the call was made with.
|
|
619
|
+
spki: guard.bytes.snapshot(issuer.publicKey, AttrCertError, "attrcert/bad-input", "issuer.publicKey"),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// RFC 5755 sec. 4.3.2: a targeted AC is valid only at the servers it names, and "servers not named
|
|
624
|
+
// MUST reject the AC". An absent extension means untargeted, so there is nothing to check.
|
|
625
|
+
//
|
|
626
|
+
// Only the decoded targetName and targetGroup alternatives are compared. Searching the extension's
|
|
627
|
+
// raw bytes for the caller's encoded GeneralName would also match a name nested inside a targetCert,
|
|
628
|
+
// where it identifies the TARGET CERTIFICATE'S ISSUER rather than a target, and would then accept an
|
|
629
|
+
// AC at a server it never named. A match against a targetGroup does count: sec. 4.3.2 targets "named
|
|
630
|
+
// targets or groups", so a verifier presenting its group name is naming itself among them.
|
|
631
|
+
//
|
|
632
|
+
// The caller's GeneralName goes through the shared builder, then back through the SAME GeneralName
|
|
633
|
+
// reader the parser used, so both sides of every comparison are the one decoded shape. Reading it
|
|
634
|
+
// back rather than dereferencing a context node's `content` also keeps this off a field that is
|
|
635
|
+
// null whenever the node is constructed.
|
|
636
|
+
function _callerTarget(target) {
|
|
637
|
+
return attrcert.readGeneralName(_b.encodeGeneralName(target), "attrcert/bad-input");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// RFC 5280 sec. 7.5: two mailboxes match when the local-part is an exact match AND the host-part
|
|
641
|
+
// matches case-insensitively. The split is on the LAST "@", since a quoted local-part may contain
|
|
642
|
+
// one; a value with no "@" has no host-part to fold and compares exactly.
|
|
643
|
+
function _mailboxEqual(a, bx) {
|
|
644
|
+
var i = a.lastIndexOf("@"), j = bx.lastIndexOf("@");
|
|
645
|
+
if (i < 0 || j < 0) return a === bx;
|
|
646
|
+
return a.slice(0, i) === bx.slice(0, j) &&
|
|
647
|
+
a.slice(i + 1).toLowerCase() === bx.slice(j + 1).toLowerCase();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Whether a target the AC names is this verifier, under the matching rule each GeneralName form
|
|
651
|
+
// defines. A form whose rule this build does not implement compares exactly, which can refuse a
|
|
652
|
+
// name a fuller comparison would accept and can never accept one it would refuse.
|
|
653
|
+
function _generalNameMatches(entry, want) {
|
|
654
|
+
if (!entry || entry.tagNumber !== want.tagNumber) return false;
|
|
655
|
+
switch (entry.tagNumber) {
|
|
656
|
+
// RFC 5280 sec. 7.2: "When comparing DNS names for equality, conforming implementations MUST
|
|
657
|
+
// perform a case-insensitive exact match on the entire DNS name."
|
|
658
|
+
case 2: return String(entry.value).toLowerCase() === String(want.value).toLowerCase();
|
|
659
|
+
// sec. 7.5, as above.
|
|
660
|
+
case 1: return _mailboxEqual(String(entry.value), String(want.value));
|
|
661
|
+
// A directoryName is a distinguished name, and sec. 7.1 identity is not string equality, so it
|
|
662
|
+
// goes through the one DN comparator rather than comparing the printed form.
|
|
663
|
+
case 4:
|
|
664
|
+
return !!(entry.value && want.value) &&
|
|
665
|
+
guard.name.dnEqual(entry.value.rdns, want.value.rdns, _err, "attrcert/bad-input", "the target name");
|
|
666
|
+
// sec. 7.4 makes a URI comparison a full RFC 3987 normalization: percent-encoding, path
|
|
667
|
+
// segments and scheme-based rules, not merely lowercasing the scheme and host. A partial
|
|
668
|
+
// normalization would be a guess that could match a URI it should not, so the encoded value is
|
|
669
|
+
// compared as it stands. Re-open condition: an RFC 3987 normalizer in the toolkit.
|
|
670
|
+
default:
|
|
671
|
+
if (Buffer.isBuffer(entry.value) && Buffer.isBuffer(want.value)) return entry.value.equals(want.value);
|
|
672
|
+
if (typeof entry.value === "string" && typeof want.value === "string") return entry.value === want.value;
|
|
673
|
+
// A form whose decoded value is neither (otherName, ediPartyName, x400Address) has no
|
|
674
|
+
// comparison here; the caller is told so rather than given a silent mismatch.
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// `want` is the caller's target already decoded, so this runs against the target the call was made
|
|
680
|
+
// with. See _fixVerifyOptions.
|
|
681
|
+
function _checkTargeting(ac, want) {
|
|
682
|
+
var ext = null;
|
|
683
|
+
for (var i = 0; i < ac.extensions.length; i++) {
|
|
684
|
+
if (ac.extensions[i].name === "targetInformation") ext = ac.extensions[i];
|
|
685
|
+
}
|
|
686
|
+
if (!ext) return { checked: true, ok: true };
|
|
687
|
+
if (want === undefined) {
|
|
688
|
+
return { checked: false, ok: false,
|
|
689
|
+
reason: "this attribute certificate is targeted, and no opts.target names this verifier (RFC 5755 sec. 4.3.2)" };
|
|
690
|
+
}
|
|
691
|
+
// The extension's syntax is a SEQUENCE OF Targets, and its groups are FLATTENED rather than
|
|
692
|
+
// required one by one. RFC 5755 sec. 4.3.2: "If more than one Targets element is found in an AC,
|
|
693
|
+
// the extension MUST be treated as if all Target elements had been found within one Targets
|
|
694
|
+
// element." An issuer conforming to the same paragraph produces only one group anyway, so the
|
|
695
|
+
// nesting carries no meaning to honor; requiring a match in every group would refuse ACs the
|
|
696
|
+
// paragraph says to accept.
|
|
697
|
+
var groups = ext.decoded || [];
|
|
698
|
+
// sec. 4.3.2 defines the check exactly: it "passes if the current server (recipient) is one of the
|
|
699
|
+
// targetName fields in the Targets SEQUENCE, or if the current server is a member of one of the
|
|
700
|
+
// targetGroup fields". The third arm is excluded by the same section -- "The targetCert CHOICE
|
|
701
|
+
// within the Target structure is only present to allow future compatibility with [X.509-2000] and
|
|
702
|
+
// MUST NOT be used" -- and the names inside it identify a certificate, not a target.
|
|
703
|
+
//
|
|
704
|
+
// An AC using it is refused before any match is considered, the same way sec. 6's "An AC MUST NOT
|
|
705
|
+
// contain both a noRevAvail extension and a 'pointer in AC'" is enforced here. Both are MUST NOTs
|
|
706
|
+
// the issuer broke, and a certificate that breaks its own profile is refused rather than partly
|
|
707
|
+
// honored: passing it because some OTHER entry happened to match would resolve the violation in
|
|
708
|
+
// the accepting direction, and would leave the verdict depending on which entries an attacker
|
|
709
|
+
// chose to include alongside the forbidden one. The targets check itself never runs, so its slot
|
|
710
|
+
// says so.
|
|
711
|
+
for (var g0 = 0; g0 < groups.length; g0++) {
|
|
712
|
+
for (var t0 = 0; t0 < groups[g0].length; t0++) {
|
|
713
|
+
if (groups[g0][t0].kind !== "targetCert") continue;
|
|
714
|
+
return { checked: false, ok: false,
|
|
715
|
+
reason: "this attribute certificate carries a targetCert target, which RFC 5755 sec. 4.3.2 says MUST NOT be used, so the targeting check did not run" };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
var uncomparable = null;
|
|
720
|
+
for (var g = 0; g < groups.length; g++) {
|
|
721
|
+
for (var t = 0; t < groups[g].length; t++) {
|
|
722
|
+
var entry = groups[g][t];
|
|
723
|
+
if (entry.kind !== "targetName" && entry.kind !== "targetGroup") continue;
|
|
724
|
+
var m = _generalNameMatches(entry.name, want);
|
|
725
|
+
if (m === true) return { checked: true, ok: true };
|
|
726
|
+
if (m === null) uncomparable = entry.name && entry.name.tagNumber;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// A target this build cannot compare leaves the check unperformed, which cannot read as a pass.
|
|
730
|
+
if (uncomparable !== null) {
|
|
731
|
+
return { checked: false, ok: false,
|
|
732
|
+
reason: "this attribute certificate names a target in a GeneralName form ([" + uncomparable +
|
|
733
|
+
"]) this verifier does not compare, so the RFC 5755 sec. 4.3.2 check did not run" };
|
|
734
|
+
}
|
|
735
|
+
return { checked: true, ok: false,
|
|
736
|
+
reason: "this attribute certificate does not name this verifier among its targets (RFC 5755 sec. 4.3.2)" };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// The critical extensions this verb SUPPORTS, by OID. RFC 5755 sec. 5 defines support in two parts:
|
|
740
|
+
// the verifier must be able to parse the value, AND "where the extension value causes the AC to be
|
|
741
|
+
// rejected, the AC verifier MUST reject the AC". Parsing alone satisfies only the first, so an
|
|
742
|
+
// extension whose constraints this verb never evaluates cannot be called supported however cleanly
|
|
743
|
+
// it decodes.
|
|
744
|
+
//
|
|
745
|
+
// targetInformation -- evaluated, by the sec. 4.3.2 check below.
|
|
746
|
+
// acAuditIdentity -- sec. 4.3.1 is an audit/logging identity and states no rule that rejects an
|
|
747
|
+
// AC, so there is no second part to satisfy. It rides out on the verdict's
|
|
748
|
+
// `extensions` for the logging the section asks for.
|
|
749
|
+
//
|
|
750
|
+
// Everything else critical is refused, including extensions this build parses but does not act on:
|
|
751
|
+
// aaControls (sec. 7.4) constrains what an AA may delegate and acProxying (sec. 7.2) constrains
|
|
752
|
+
// proxy use, and an unevaluated constraint is exactly what item 7 exists to stop. A critical
|
|
753
|
+
// extension defined in future defaults to refused, which is the safe direction.
|
|
754
|
+
var SUPPORTED_CRITICAL = {};
|
|
755
|
+
SUPPORTED_CRITICAL[O("targetInformation")] = 1;
|
|
756
|
+
SUPPORTED_CRITICAL[O("acAuditIdentity")] = 1;
|
|
757
|
+
|
|
758
|
+
function _unsupportedCritical(ac) {
|
|
759
|
+
for (var i = 0; i < ac.extensions.length; i++) {
|
|
760
|
+
var e = ac.extensions[i];
|
|
761
|
+
if (e.critical !== true) continue;
|
|
762
|
+
// Own-property membership, so an extension OID that happens to name something on Object.prototype
|
|
763
|
+
// cannot read as supported.
|
|
764
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_CRITICAL, e.oid)) return e.oid;
|
|
765
|
+
// A supported OID whose value this build could not interpret is still unusable.
|
|
766
|
+
if (e.decoded === null || e.decoded === undefined || e.decoded.opaque === true) return e.oid;
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* @primitive pki.attrcert.verify
|
|
773
|
+
* @signature pki.attrcert.verify(ac, issuer, opts) -> Promise<{ verified, signatureValid, validityChecked, targetingChecked, revocationChecked, noRevAvail, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }>
|
|
774
|
+
* @since 0.5.15
|
|
775
|
+
* @status stable
|
|
776
|
+
* @spec RFC 5755 sec. 5
|
|
777
|
+
* @defends attribute-certificate-forgery (CWE-347)
|
|
778
|
+
* @related pki.attrcert.sign, pki.path.validate, pki.schema.attrcert.parse
|
|
779
|
+
*
|
|
780
|
+
* Verify an RFC 5755 attribute certificate. An AC carries privilege attributes -- a role, a group, a
|
|
781
|
+
* clearance -- so a consumer that reads them without this grants privileges on unauthenticated
|
|
782
|
+
* input. `ac` is a DER `Buffer`, a PEM string, or a parsed attribute certificate; `issuer` is
|
|
783
|
+
* `{ name, publicKey }`, the AC issuer this verifier trusts as an AC issuer, which section 5 item 4
|
|
784
|
+
* makes the verifier's own configuration and this verb therefore refuses to infer from the AC.
|
|
785
|
+
* `issuer.name` takes every form `pki.attrcert.sign` accepts for the issuing AA -- a string, an
|
|
786
|
+
* array of RDNs, or raw `Name` DER -- and is compared as a distinguished name (RFC 5280 section
|
|
787
|
+
* 7.1), so an AC issued under a multi-RDN authority name is verifiable under that same name.
|
|
788
|
+
*
|
|
789
|
+
* Both options are read at the call, before signature verification suspends it, so a caller that
|
|
790
|
+
* reuses or rewrites its options object -- including calling `setTime` on the `Date` it passed in --
|
|
791
|
+
* cannot change the verdict that call returns.
|
|
792
|
+
*
|
|
793
|
+
* The checks it performs are section 5's items 2, 4, 5, 6 and 7: the signature over the exact
|
|
794
|
+
* `AttributeCertificateInfo` bytes through the one path-validation signature engine; the issuer
|
|
795
|
+
* being the one named; the evaluation instant lying within the validity, where equality with either
|
|
796
|
+
* bound succeeds as the section states; the targeting rule of section 4.3.2, where an AC naming
|
|
797
|
+
* targets is refused at a verifier it does not name; and rejection of any critical extension this
|
|
798
|
+
* verb does not process. Section 5 defines support as parsing the value AND rejecting where the
|
|
799
|
+
* value would reject, so an extension parsed but never evaluated is not supported: targeting is
|
|
800
|
+
* processed and an audit identity states no rule that rejects, while `aaControls` and `acProxying`
|
|
801
|
+
* carry constraints this verb does not evaluate, so a critical one is refused. A critical extension
|
|
802
|
+
* defined in future defaults to refused. The verdict carries `extensions` so an audit identity
|
|
803
|
+
* reaches the caller for the logging section 4.3.1 asks for.
|
|
804
|
+
*
|
|
805
|
+
* Targeting compares `opts.target` against the `targetName` and `targetGroup` alternatives only,
|
|
806
|
+
* under each GeneralName form's own matching rule: a dNSName folds case across the whole name
|
|
807
|
+
* (RFC 5280 sec. 7.2), a mailbox matches its local-part exactly and its host-part case-insensitively
|
|
808
|
+
* (sec. 7.5), and a directoryName goes through the same distinguished-name comparison the rest of
|
|
809
|
+
* the toolkit uses (sec. 7.1). A URI compares as encoded, because sec. 7.4 makes URI equality a full
|
|
810
|
+
* RFC 3987 normalization -- percent-encoding, path segments and scheme-based rules -- and a partial
|
|
811
|
+
* one could match a URI it should not. A form with no comparison here (otherName, ediPartyName,
|
|
812
|
+
* x400Address) leaves `targetingChecked: false` and never a pass. Every one of those limits can
|
|
813
|
+
* refuse a target a fuller comparison would accept, and none can accept one it would refuse.
|
|
814
|
+
* Re-open condition: an RFC 3987 normalizer, at which point URIs compare under sec. 7.4.
|
|
815
|
+
*
|
|
816
|
+
* The third `Target` alternative, `targetCert`, is one section 4.3.2 says "MUST NOT be used", so an
|
|
817
|
+
* attribute certificate carrying one is refused before any match is considered, with
|
|
818
|
+
* `targetingChecked: false`. A match on some other entry does not rescue it: the issuer broke a MUST
|
|
819
|
+
* NOT of its own profile, and letting the verdict turn on which entries rode alongside the forbidden
|
|
820
|
+
* one would leave the outcome to whoever assembled the certificate.
|
|
821
|
+
*
|
|
822
|
+
* Revocation is section 6, outside the seven. This verb implements that section's "never revoke"
|
|
823
|
+
* scheme, which AC users MUST support: it holds no revocation evidence and follows no pointer out
|
|
824
|
+
* of the certificate. The section states the consequence -- "If only the 'never revoke' scheme is
|
|
825
|
+
* supported, then all ACs that do not contain a noRevAvail extension, MUST be rejected" -- because
|
|
826
|
+
* an issuer that omits noRevAvail is stating that revocation status checks are supported, and a
|
|
827
|
+
* verdict that skipped one would grant privileges the issuer expected to be able to withdraw. So an
|
|
828
|
+
* AC carrying `noRevAvail` verifies with `revocationChecked: true` and `noRevAvail: true`, and one
|
|
829
|
+
* without it is refused unless the caller supplies `opts.revocationStatus`. A caller running the
|
|
830
|
+
* section's "pointer in AC" scheme reads the certificate's own `crlDistributionPoints` /
|
|
831
|
+
* `authorityInfoAccess` from the verdict's `extensions`, establishes the status, and passes it back.
|
|
832
|
+
*
|
|
833
|
+
* Items 1 and 3, and the issuer path in item 2, need certificates this verb is not given: the
|
|
834
|
+
* holder's own public-key certificate and its chain, the AC issuer's chain, and the section 4.5
|
|
835
|
+
* profile of the issuer's certificate. Run those through `pki.path.validate` with the certificates
|
|
836
|
+
* you hold. The verdict reports them as `holderBindingChecked: false` and `issuerPathChecked:
|
|
837
|
+
* false` rather than leaving their absence to read as a pass, and `verified` never means more than
|
|
838
|
+
* the checks whose slots are true. Re-open condition: when a caller can pass the holder and issuer
|
|
839
|
+
* certificates, both become checks this verb performs and both slots follow the arguments.
|
|
840
|
+
*
|
|
841
|
+
* @opts
|
|
842
|
+
* time the instant to evaluate the AC at. Omitting it leaves section 5 item 5 unasked, which
|
|
843
|
+
* reports `validityChecked: false` and never `verified: true`.
|
|
844
|
+
* target a GeneralName naming this verifier, for the section 4.3.2 targeting check.
|
|
845
|
+
* revocationStatus
|
|
846
|
+
* `"notRevoked"` or `"revoked"`, the status the caller established for this AC through
|
|
847
|
+
* the section 6 "pointer in AC" scheme. Supplying it answers section 6 for an AC that
|
|
848
|
+
* carries no `noRevAvail`; omitting it leaves such an AC refused.
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
852
|
+
* var aaSpki = await pki.key.export(pair.publicKey);
|
|
853
|
+
* var aaKey = await pki.key.export(pair.privateKey);
|
|
854
|
+
* var acDer = await pki.attrcert.sign({
|
|
855
|
+
* holder: { entityName: { directoryName: "CN=Alice" } },
|
|
856
|
+
* notBeforeTime: new Date("2026-01-01T00:00:00Z"),
|
|
857
|
+
* notAfterTime: new Date("2027-01-01T00:00:00Z"),
|
|
858
|
+
* attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } },
|
|
859
|
+
* // RFC 5755 sec. 6: the issuer states no revocation information will exist for this AC.
|
|
860
|
+
* extensions: { noRevAvail: true },
|
|
861
|
+
* }, { name: "CN=Example AA", publicKey: aaSpki, key: aaKey });
|
|
862
|
+
* var r = await pki.attrcert.verify(acDer, { name: "CN=Example AA", publicKey: aaSpki },
|
|
863
|
+
* { time: new Date("2026-06-01T00:00:00Z") });
|
|
864
|
+
* r.verified; // true when every check whose slot is true passed
|
|
865
|
+
* r.attributes; // the privileges, re-derived from the signed bytes
|
|
866
|
+
*/
|
|
867
|
+
function verify(ac, issuer, opts) {
|
|
868
|
+
return guard.async.deferred(function () { return _verify(ac, issuer, opts); });
|
|
869
|
+
}
|
|
870
|
+
// Every caller-owned value the verdict depends on, read HERE and reduced to a value the caller no
|
|
871
|
+
// longer reaches: the instant rather than the Date (a Date is mutable through setTime even when the
|
|
872
|
+
// property is never reassigned), and the target already encoded and decoded rather than the object.
|
|
873
|
+
// Signature verification suspends this call for at least a turn, and an option re-read after it
|
|
874
|
+
// resumes answers for whatever the caller last wrote, so a caller could hand in an expired instant,
|
|
875
|
+
// replace it while WebCrypto works, and receive `verified: true`. The reduction is returned rather
|
|
876
|
+
// than performed at each use, so reading an option late is not an available mistake.
|
|
877
|
+
function _fixVerifyOptions(opts) {
|
|
878
|
+
var o = guard.identifier.optionsObject(opts, _err, "attrcert/bad-input", "pki.attrcert.verify options");
|
|
879
|
+
guard.identifier.assertKnownKeys(o, KNOWN_VERIFY_OPTS, _err, "attrcert/bad-input",
|
|
880
|
+
"pki.attrcert.verify has an unknown option ");
|
|
881
|
+
var at = null;
|
|
882
|
+
if (o.time !== undefined) {
|
|
883
|
+
guard.time.assertValid(o.time, _err, "attrcert/bad-input", "pki.attrcert.verify opts.time");
|
|
884
|
+
at = guard.time.instantOf(o.time);
|
|
885
|
+
}
|
|
886
|
+
var revocationStatus = null;
|
|
887
|
+
if (o.revocationStatus !== undefined) {
|
|
888
|
+
if (typeof o.revocationStatus !== "string" ||
|
|
889
|
+
!Object.prototype.hasOwnProperty.call(REVOCATION_STATUS, o.revocationStatus)) {
|
|
890
|
+
throw _err("attrcert/bad-input", 'pki.attrcert.verify opts.revocationStatus must be "notRevoked" or "revoked"');
|
|
891
|
+
}
|
|
892
|
+
revocationStatus = o.revocationStatus;
|
|
893
|
+
}
|
|
894
|
+
// A supplied target is encoded and read back whether or not the AC turns out to be targeted, so a
|
|
895
|
+
// malformed one is refused at the call that supplied it.
|
|
896
|
+
return {
|
|
897
|
+
at: at, revocationStatus: revocationStatus,
|
|
898
|
+
target: o.target === undefined ? undefined : _callerTarget(o.target),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// RFC 5755 sec. 6 "pointer in AC": "ACs may 'point' to sources of revocation status information,
|
|
903
|
+
// using either an authorityInfoAccess extension or a crlDistributionPoints extension within the AC."
|
|
904
|
+
// Named by OID, since the OID is what an issuer signs and a display name is not.
|
|
905
|
+
var REVOCATION_POINTERS = Object.create(null);
|
|
906
|
+
REVOCATION_POINTERS[O("cRLDistributionPoints")] = "crlDistributionPoints";
|
|
907
|
+
REVOCATION_POINTERS[O("authorityInfoAccess")] = "authorityInfoAccess";
|
|
908
|
+
|
|
909
|
+
// The two revocation schemes an AC declares, read from its extensions. RFC 5755 sec. 4.3.6 / sec. 6:
|
|
910
|
+
// noRevAvail is the issuer stating that no revocation information will ever be made available for
|
|
911
|
+
// this AC, and it is a bare NULL, so its presence is the whole content.
|
|
912
|
+
function _revocationScheme(ac) {
|
|
913
|
+
var out = { noRevAvail: false, pointer: null };
|
|
914
|
+
for (var i = 0; i < ac.extensions.length; i++) {
|
|
915
|
+
var e = ac.extensions[i];
|
|
916
|
+
if (e.name === "noRevAvail") out.noRevAvail = true;
|
|
917
|
+
var p = REVOCATION_POINTERS[e.oid];
|
|
918
|
+
if (p !== undefined && Object.prototype.hasOwnProperty.call(REVOCATION_POINTERS, e.oid)) out.pointer = p;
|
|
919
|
+
}
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function _verify(ac, issuer, opts) {
|
|
924
|
+
var fixed = _fixVerifyOptions(opts);
|
|
925
|
+
var parsed = _coerceAc(ac);
|
|
926
|
+
var trust = _resolveIssuer(issuer);
|
|
927
|
+
|
|
928
|
+
var acIssuer = _acIssuerName(parsed);
|
|
929
|
+
var result = {
|
|
930
|
+
holder: parsed.holder,
|
|
931
|
+
issuer: acIssuer,
|
|
932
|
+
attributes: parsed.attributes,
|
|
933
|
+
// Carried so a caller can act on what rode along: sec. 4.3.1 asks that an audit identity be
|
|
934
|
+
// used for audit and logging, which it cannot be if the verdict drops it.
|
|
935
|
+
extensions: parsed.extensions,
|
|
936
|
+
notBefore: parsed.validity.notBeforeTime,
|
|
937
|
+
notAfter: parsed.validity.notAfterTime,
|
|
938
|
+
serialNumberHex: parsed.serialNumberHex,
|
|
939
|
+
signatureValid: false,
|
|
940
|
+
validityChecked: false,
|
|
941
|
+
targetingChecked: false,
|
|
942
|
+
revocationChecked: false,
|
|
943
|
+
noRevAvail: false,
|
|
944
|
+
holderBindingChecked: false, // item 1 needs the holder's certificate, which this verb is not given
|
|
945
|
+
issuerPathChecked: false, // items 2 and 3 need the issuer's chain, likewise
|
|
946
|
+
verified: false,
|
|
947
|
+
reason: undefined,
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// The signature runs first so `signatureValid` is answered even when a later gate refuses: an
|
|
951
|
+
// operator reading an out-of-window verdict still learns whether the bytes were authentic.
|
|
952
|
+
return Promise.resolve(attrcertVerify.verifyAcSignature(parsed, trust.spki)).then(function (sigOk) {
|
|
953
|
+
result.signatureValid = sigOk === true;
|
|
954
|
+
var fail = function (reason) { result.reason = reason; return result; };
|
|
955
|
+
if (!result.signatureValid) return fail("the signature does not verify under the issuer public key");
|
|
956
|
+
|
|
957
|
+
// Item 4: the caller trusts a named AC issuer, and this AC must be the one they named.
|
|
958
|
+
if (!guard.name.dnEqual(acIssuer.rdns, trust.rdns, _err,
|
|
959
|
+
"attrcert/bad-input", "the AC issuer")) {
|
|
960
|
+
return fail("this attribute certificate names issuer " + JSON.stringify(acIssuer.dn) +
|
|
961
|
+
", which is not the AC issuer this verifier trusts");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Item 7, before the fields are read: an unsupported critical extension makes the whole AC
|
|
965
|
+
// unusable, so a caller must not act on attributes carried beside one.
|
|
966
|
+
var badExt = _unsupportedCritical(parsed);
|
|
967
|
+
if (badExt) return fail("this attribute certificate carries an unsupported critical extension (" + badExt + "), which RFC 5755 sec. 5 item 7 requires be rejected");
|
|
968
|
+
|
|
969
|
+
// Item 6.
|
|
970
|
+
var targeting = _checkTargeting(parsed, fixed.target);
|
|
971
|
+
result.targetingChecked = targeting.checked;
|
|
972
|
+
if (!targeting.ok) return fail(targeting.reason);
|
|
973
|
+
|
|
974
|
+
// Item 5. An absent instant asks nothing, which cannot read as a pass.
|
|
975
|
+
if (fixed.at === null) {
|
|
976
|
+
return fail("no opts.time was supplied, so RFC 5755 sec. 5 item 5 (the evaluation instant lies within the validity) went unasked");
|
|
977
|
+
}
|
|
978
|
+
result.validityChecked = true;
|
|
979
|
+
// "If the evaluation time is equal to either notBeforeTime or notAfterTime, then the AC is
|
|
980
|
+
// timely and this check succeeds" -- both bounds are inclusive.
|
|
981
|
+
if (fixed.at < guard.time.instantOf(result.notBefore) || fixed.at > guard.time.instantOf(result.notAfter)) {
|
|
982
|
+
return fail("the evaluation instant lies outside this attribute certificate's validity");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// RFC 5755 sec. 6. This verb implements the "never revoke" scheme: it holds no revocation
|
|
986
|
+
// evidence of its own and follows no pointer out of the certificate. The section says exactly
|
|
987
|
+
// what that obliges -- "If only the 'never revoke' scheme is supported, then all ACs that do not
|
|
988
|
+
// contain a noRevAvail extension, MUST be rejected" -- because where noRevAvail is absent "the
|
|
989
|
+
// AC issuer is implicitly stating that revocation status checks are supported", so a verdict
|
|
990
|
+
// that skipped one would grant privileges the issuer expected to be able to withdraw. A caller
|
|
991
|
+
// who has established the status passes it as opts.revocationStatus; the AC's own
|
|
992
|
+
// crlDistributionPoints / authorityInfoAccess pointers ride out on the verdict's `extensions`
|
|
993
|
+
// for them to follow.
|
|
994
|
+
//
|
|
995
|
+
// That status is accepted whether or not the AC carries a pointer, because the section closes by
|
|
996
|
+
// saying where it may come from: "An AC verifier MAY use any source for AC revocation status
|
|
997
|
+
// information." Requiring a pointer in the certificate would refuse a verifier who obtained the
|
|
998
|
+
// status from a directory, an out-of-band CRL, or a local record, all of which that sentence
|
|
999
|
+
// permits. The verdict keeps the two apart: `noRevAvail` says the issuer declared none would
|
|
1000
|
+
// exist, and its absence beside `revocationChecked: true` says the caller supplied one.
|
|
1001
|
+
var scheme = _revocationScheme(parsed);
|
|
1002
|
+
result.noRevAvail = scheme.noRevAvail;
|
|
1003
|
+
// The two schemes are alternatives, and the section closes by saying so: "An AC MUST NOT contain
|
|
1004
|
+
// both a noRevAvail extension and a 'pointer in AC'." An AC carrying both tells the verifier two
|
|
1005
|
+
// different things about its own revocation, and reading the never-revoke claim as settling the
|
|
1006
|
+
// question would resolve that contradiction in the accepting direction. It is refused whichever
|
|
1007
|
+
// way the caller answers, since the certificate itself is the thing that is wrong.
|
|
1008
|
+
if (scheme.noRevAvail && scheme.pointer) {
|
|
1009
|
+
return fail("this attribute certificate carries both noRevAvail and a " + scheme.pointer +
|
|
1010
|
+
" revocation pointer, which RFC 5755 sec. 6 says an AC MUST NOT do");
|
|
1011
|
+
}
|
|
1012
|
+
if (fixed.revocationStatus !== null) {
|
|
1013
|
+
result.revocationChecked = true;
|
|
1014
|
+
if (fixed.revocationStatus === "revoked") {
|
|
1015
|
+
return fail("this attribute certificate is revoked, as opts.revocationStatus reports");
|
|
1016
|
+
}
|
|
1017
|
+
} else if (result.noRevAvail) {
|
|
1018
|
+
// The issuer has stated no revocation information will exist, so there is no status to obtain.
|
|
1019
|
+
result.revocationChecked = true;
|
|
1020
|
+
} else {
|
|
1021
|
+
return fail("this attribute certificate carries no noRevAvail extension, so its issuer supports revocation status checks and RFC 5755 sec. 6 requires one; establish the status from the certificate's own pointers and pass it as opts.revocationStatus");
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
result.verified = true;
|
|
1025
|
+
return result;
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
module.exports = { sign: sign, verify: verify };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- the RFC 5755 attribute-certificate signature seam, shared by pki.attrcert.verify and
|
|
6
|
+
// by any path that accepts an inbound AC. The slot, the wiring assertion and the octet-alignment
|
|
7
|
+
// refusal are the shared seam (lib/verify-seam.js); what is domain-specific is the error class and
|
|
8
|
+
// the byte range the signature covers. It is not wired into index.js and is reached only by require.
|
|
9
|
+
|
|
10
|
+
var frameworkError = require("./framework-error");
|
|
11
|
+
var seam = require("./verify-seam");
|
|
12
|
+
|
|
13
|
+
var _seam = seam.makeSeam("attrcert-verify", frameworkError.AttrCertError, "attrcert/bad-input");
|
|
14
|
+
|
|
15
|
+
// Verify a parsed attribute certificate's signature over its exact AttributeCertificateInfo bytes
|
|
16
|
+
// under the AC issuer's SubjectPublicKeyInfo DER (RFC 5755 sec. 5, item 2).
|
|
17
|
+
function verifyAcSignature(ac, spkiBytes) {
|
|
18
|
+
return _seam.verify(ac.signatureAlgorithm, ac.signatureValue, spkiBytes, ac.tbsBytes);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { setEngine: _seam.setEngine, verifyAcSignature: verifyAcSignature };
|
package/lib/crl-verify.js
CHANGED
|
@@ -4,30 +4,21 @@
|
|
|
4
4
|
//
|
|
5
5
|
// @internal -- the single CRL signature-verify seam, shared by pki.path.crlChecker and pki.crl.verify
|
|
6
6
|
// so both route through one signature engine (algorithm-confusion + EdDSA low-order + composite
|
|
7
|
-
// gates) and no second, weaker CRL verifier can drift.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// gates) and no second, weaker CRL verifier can drift. The slot, the wiring assertion and the
|
|
8
|
+
// octet-alignment refusal are the shared seam (lib/verify-seam.js). The seam lives in this internal
|
|
9
|
+
// module, not on path-validate's module.exports, which keeps it off the public pki.path surface: it
|
|
10
|
+
// takes the path-internal SubjectPublicKeyInfo bytes, not a documented issuer shape. It is not
|
|
11
|
+
// wired into index.js and is reached only by require.
|
|
12
12
|
|
|
13
|
-
var guard = require("./guard-all");
|
|
14
13
|
var frameworkError = require("./framework-error");
|
|
14
|
+
var seam = require("./verify-seam");
|
|
15
15
|
|
|
16
|
-
var
|
|
17
|
-
var _engine = null; // path-validate's _verifyWithSpki, injected at path-validate's module load via setEngine.
|
|
16
|
+
var _seam = seam.makeSeam("crl-verify", frameworkError.CrlError, "crl/bad-input");
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Verify a parsed CRL's signature over its raw tbsCertList bytes under the issuer SubjectPublicKeyInfo DER.
|
|
22
|
-
// Fail-closed to false on a non-octet-aligned signature or any engine fault (the engine never throws out).
|
|
18
|
+
// Verify a parsed CRL's signature over its raw tbsCertList bytes under the issuer
|
|
19
|
+
// SubjectPublicKeyInfo DER.
|
|
23
20
|
function verifyCrlSignature(crl, spkiBytes) {
|
|
24
|
-
|
|
25
|
-
// route that reaches here through the public verb. A future internal consumer that requires this
|
|
26
|
-
// module without it would otherwise call null and surface a raw TypeError out of a verify, which
|
|
27
|
-
// names neither the defect nor a verdict, so the wiring is asserted rather than assumed.
|
|
28
|
-
if (_engine == null) throw new CrlError("crl/bad-input", "the crl-verify signature engine is not initialized (require pki before use)");
|
|
29
|
-
if (!guard.crypto.isOctetAligned(crl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
|
|
30
|
-
return _engine(crl.signatureAlgorithm, crl.signatureValue.bytes, spkiBytes, crl.tbsBytes);
|
|
21
|
+
return _seam.verify(crl.signatureAlgorithm, crl.signatureValue, spkiBytes, crl.tbsBytes);
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
module.exports = { setEngine: setEngine, verifyCrlSignature: verifyCrlSignature };
|
|
24
|
+
module.exports = { setEngine: _seam.setEngine, verifyCrlSignature: verifyCrlSignature };
|
package/lib/crmf-sign.js
CHANGED
|
@@ -32,6 +32,8 @@ var pkix = require("./schema-pkix");
|
|
|
32
32
|
var pkiBuild = require("./pki-build");
|
|
33
33
|
var frameworkError = require("./framework-error");
|
|
34
34
|
var guard = require("./guard-all");
|
|
35
|
+
require("./path-validate"); // side-effect: path-validate injects its signature engine into crmf-verify at load
|
|
36
|
+
var crmfVerify = require("./crmf-verify");
|
|
35
37
|
|
|
36
38
|
var CrmfError = frameworkError.CrmfError;
|
|
37
39
|
var b = asn1.build;
|
|
@@ -310,4 +312,126 @@ function _build(spec, key, opts) {
|
|
|
310
312
|
*/
|
|
311
313
|
function buildCertTemplate(template) { return _encodeCertTemplate(template, { revocation: true }).der; }
|
|
312
314
|
|
|
313
|
-
|
|
315
|
+
function _coerceMessages(input) {
|
|
316
|
+
return guard.parsed.acceptDerived(input, "crmf", crmf.parse, _err, "crmf/bad-input", "the certificate request messages");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// One CertReqMsg's proof of possession, reduced to a verdict. Every return names a method and
|
|
320
|
+
// reports the requested identity, so a refusal is as informative as an acceptance.
|
|
321
|
+
function _verifyOne(msg) {
|
|
322
|
+
var t = msg.certReq && msg.certReq.certTemplate;
|
|
323
|
+
var base = {
|
|
324
|
+
certReqId: msg.certReq && msg.certReq.certReqId,
|
|
325
|
+
subject: (t && t.subject) || null,
|
|
326
|
+
subjectBound: false,
|
|
327
|
+
publicKey: (t && t.publicKey && t.publicKey.bytes) || null,
|
|
328
|
+
};
|
|
329
|
+
function settle(extra) { return Promise.resolve(Object.assign({}, base, extra)); }
|
|
330
|
+
|
|
331
|
+
var popo = msg.popo;
|
|
332
|
+
if (popo == null) {
|
|
333
|
+
return settle({ verified: false, method: null, cryptographicallyVerified: false,
|
|
334
|
+
reason: "the request carries no proof of possession" });
|
|
335
|
+
}
|
|
336
|
+
if (popo.type === "raVerified") {
|
|
337
|
+
// sec. 4: an assertion by the RA that it checked possession out of band. It is not a proof and
|
|
338
|
+
// must not read as one; a caller that trusts its RA opts in by reading `method`.
|
|
339
|
+
return settle({ verified: false, method: "raVerified", cryptographicallyVerified: false,
|
|
340
|
+
reason: "raVerified asserts the RA checked possession out of band -- this is not a proof and cannot be verified from the message" });
|
|
341
|
+
}
|
|
342
|
+
if (popo.type !== "signature") {
|
|
343
|
+
// keyEncipherment / keyAgreement complete over a later protocol exchange (a challenge the
|
|
344
|
+
// verifier does not hold) or need the CA's decryption key, so no verdict is available here.
|
|
345
|
+
return settle({ verified: false, method: popo.type, cryptographicallyVerified: false,
|
|
346
|
+
reason: "a " + popo.type + " proof of possession completes over an exchange this verifier does not hold (RFC 4211 sec. 4.2, 4.3)" });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Which bytes the signature covers is fixed by whether poposkInput is present: the DER of
|
|
350
|
+
// poposkInput when it is, the DER of certReq when it is not (RFC 4211 sec. 4.1, and the ASN.1
|
|
351
|
+
// module on p.33 -- sec. 4.1's case-3 prose says "certificate template" where the field
|
|
352
|
+
// definition and the module both say certReq).
|
|
353
|
+
//
|
|
354
|
+
// The three conformance rules that decide which of those is legitimate are enforced ONE layer
|
|
355
|
+
// down, at parse: poposkInput MUST be omitted exactly when the template carries both subject and
|
|
356
|
+
// publicKey, and poposkInput.publicKey MUST equal the template's (schema-crmf.js, the CertReqMsg
|
|
357
|
+
// build). A message breaking either never reaches this function, and re-checking here would add
|
|
358
|
+
// a branch no input can take while implying the parser might let one through.
|
|
359
|
+
var pin = popo.poposkInput;
|
|
360
|
+
var preimage = pin ? pin.signedBytes : msg.certReq.certReqBytes;
|
|
361
|
+
// The key possession is proven for is the one inside the preimage. With poposkInput that is its
|
|
362
|
+
// own publicKey; sec. 4.1 requires the template to carry the same value, and the parser enforces
|
|
363
|
+
// that whenever the template has one at all. A template that omits it entirely still parses (the
|
|
364
|
+
// equality rule has nothing to compare), so reading the template here would report null for a key
|
|
365
|
+
// whose possession was just proven.
|
|
366
|
+
var spki = pin ? pin.publicKey : base.publicKey;
|
|
367
|
+
// Which fields the verdict may report follows from which bytes were signed. The certReq preimage
|
|
368
|
+
// is the whole CertRequest, so the template's subject rides with it. The poposkInput preimage
|
|
369
|
+
// covers the key and the authInfo sender and never the template, so a subject sitting in the
|
|
370
|
+
// message alongside it is unsigned: reporting it beside `verified: true` would let a CA issue a
|
|
371
|
+
// name the requester proved nothing about. It is withheld, and `subjectBound` says so rather than
|
|
372
|
+
// leaving the absence to look like a request that named no subject.
|
|
373
|
+
var bound = !pin;
|
|
374
|
+
return crmfVerify.verifyPopSignature(popo, spki, preimage).then(function (ok) {
|
|
375
|
+
return Object.assign({}, base, {
|
|
376
|
+
verified: ok === true, method: "signature", cryptographicallyVerified: ok === true,
|
|
377
|
+
subject: bound ? base.subject : null,
|
|
378
|
+
subjectBound: bound,
|
|
379
|
+
publicKey: spki || null,
|
|
380
|
+
reason: ok === true ? undefined : "the proof-of-possession signature does not verify under the requested public key",
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @primitive pki.crmf.verifyPop
|
|
387
|
+
* @signature pki.crmf.verifyPop(messages) -> Promise<{ verified, messages: [{ verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }>
|
|
388
|
+
* @since 0.5.14
|
|
389
|
+
* @status stable
|
|
390
|
+
* @spec RFC 4211 sec. 4.1
|
|
391
|
+
* @defends crmf-proof-of-possession-bypass (CWE-347)
|
|
392
|
+
* @related pki.crmf.build, pki.csr.verify, pki.schema.crmf.parse
|
|
393
|
+
*
|
|
394
|
+
* Verify the proof of possession on each `CertReqMsg` in a `CertReqMessages`. `messages` is a DER
|
|
395
|
+
* `Buffer` or a parsed result. A CA or RA that issues without this certifies a key the requester
|
|
396
|
+
* may not hold. One verdict is returned per message, in order, and the top-level `verified` is true
|
|
397
|
+
* only when every message carried a proof that verified.
|
|
398
|
+
*
|
|
399
|
+
* For the `signature` proof the covered bytes are the ones RFC 4211 names: the DER of `poposkInput`
|
|
400
|
+
* when that field is present, and the DER of `certReq` when it is absent. Two conformance rules ride
|
|
401
|
+
* with it, both refusals rather than warnings, because each lets a certificate be issued for
|
|
402
|
+
* something nobody signed: `poposkInput` MUST be omitted exactly when the template carries both
|
|
403
|
+
* subject and public key (its preimage covers the key and the sender, never the subject), and
|
|
404
|
+
* `poposkInput.publicKey` MUST be exactly the template's public key (sec. 4.1). Verification
|
|
405
|
+
* composes the one path-validation signature engine, with the same algorithm-confusion (RFC 9814
|
|
406
|
+
* sec. 4) and EdDSA low-order-point gates.
|
|
407
|
+
*
|
|
408
|
+
* The other proofs are reported, never guessed. `raVerified` is an RA's out-of-band assertion, so it
|
|
409
|
+
* returns `verified: false` with `method: "raVerified"` and a caller who trusts that RA opts in by
|
|
410
|
+
* reading `method`. `keyEncipherment` and `keyAgreement` complete over a later protocol exchange, or
|
|
411
|
+
* need the CA's decryption key, so they return `verified: false` naming the arm.
|
|
412
|
+
*
|
|
413
|
+
* Each verdict carries what the verified preimage covers, so a CA issues from what was checked.
|
|
414
|
+
* `publicKey` is the key possession was proven for. `subject` is the requested name when the
|
|
415
|
+
* preimage was the `certReq`, which covers the whole template; when the preimage was `poposkInput`
|
|
416
|
+
* it covers the key and the sender alone, so any subject in the message is unsigned and is withheld
|
|
417
|
+
* with `subjectBound: false`. Bind the name by other means before issuing in that case.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
421
|
+
* var spki = await pki.key.export(pair.publicKey);
|
|
422
|
+
* var pkcs8 = await pki.key.export(pair.privateKey);
|
|
423
|
+
* var req = await pki.crmf.build({ certReqId: 1n,
|
|
424
|
+
* certTemplate: { subject: "device-42", publicKey: spki } }, { key: pkcs8 }); // a bare string is the commonName VALUE
|
|
425
|
+
* var r = await pki.crmf.verifyPop(req);
|
|
426
|
+
* r.verified; // true
|
|
427
|
+
* r.messages[0].method; // "signature"
|
|
428
|
+
*/
|
|
429
|
+
function verifyPop(messages) { return guard.async.deferred(function () { return _verifyPop(messages); }); }
|
|
430
|
+
function _verifyPop(messages) {
|
|
431
|
+
var parsed = _coerceMessages(messages);
|
|
432
|
+
return Promise.all(parsed.messages.map(_verifyOne)).then(function (out) {
|
|
433
|
+
return { verified: out.length > 0 && out.every(function (m) { return m.verified === true; }), messages: out };
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = { build: build, buildCertTemplate: buildCertTemplate, verifyPop: verifyPop };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- the single RFC 4211 proof-of-possession signature seam for an INBOUND CertReqMsg,
|
|
6
|
+
// shared by pki.crmf.verifyPop and by every enrollment path that accepts one. The slot, the wiring
|
|
7
|
+
// assertion and the octet-alignment refusal are the shared seam (lib/verify-seam.js); what is
|
|
8
|
+
// domain-specific is the error class and which bytes the signature covers. It is not wired into
|
|
9
|
+
// index.js and is reached only by require.
|
|
10
|
+
|
|
11
|
+
var frameworkError = require("./framework-error");
|
|
12
|
+
var seam = require("./verify-seam");
|
|
13
|
+
|
|
14
|
+
var _seam = seam.makeSeam("crmf-verify", frameworkError.CrmfError, "crmf/bad-input");
|
|
15
|
+
|
|
16
|
+
// Verify one CertReqMsg's POPOSigningKey signature over `preimage` under `spkiBytes`.
|
|
17
|
+
//
|
|
18
|
+
// `preimage` is chosen by the caller from the two the RFC defines, because the choice is a
|
|
19
|
+
// conformance rule about the message rather than a property of the signature: the DER of
|
|
20
|
+
// poposkInput when that field is present, and the DER of `CertReqMsg certReq` when it is absent
|
|
21
|
+
// (RFC 4211 sec. 4.1 and the ASN.1 module on p.33).
|
|
22
|
+
function verifyPopSignature(popo, spkiBytes, preimage) {
|
|
23
|
+
return _seam.verify(popo.algorithmIdentifier, popo.signature, spkiBytes, preimage);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { setEngine: _seam.setEngine, verifyPopSignature: verifyPopSignature };
|
package/lib/csr-sign.js
CHANGED
|
@@ -219,7 +219,8 @@ function _coerceCsr(request) {
|
|
|
219
219
|
* var pair = await pki.key.generate("Ed25519");
|
|
220
220
|
* var spki = await pki.key.export(pair.publicKey);
|
|
221
221
|
* var pkcs8 = await pki.key.export(pair.privateKey);
|
|
222
|
-
*
|
|
222
|
+
* // A bare string is the commonName VALUE, so this asks for CN=device-42.
|
|
223
|
+
* var req = await pki.csr.sign({ subject: "device-42", subjectPublicKey: spki }, { key: pkcs8 });
|
|
223
224
|
* var r = await pki.csr.verify(req);
|
|
224
225
|
* // Issue from r.subject / r.subjectPublicKeyInfo / r.attributes, which are the verified fields.
|
|
225
226
|
* var issued = r.verified
|
package/lib/csr-verify.js
CHANGED
|
@@ -4,41 +4,24 @@
|
|
|
4
4
|
//
|
|
5
5
|
// @internal -- the single PKCS#10 proof-of-possession signature seam for an INBOUND request, shared
|
|
6
6
|
// by pki.csr.verify and by every enrollment path that accepts one, so all of them route through one
|
|
7
|
-
// signature engine
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// path-validate's module.exports, which keeps it off the public pki.path surface. It is not wired
|
|
13
|
-
// into index.js and is reached only by require.
|
|
14
|
-
//
|
|
15
|
-
// It deliberately does NOT compose pki-build's assertSignatureVerifies. That helper is a producing-
|
|
16
|
-
// side self-check over a key the caller already controls, and it waives the EdDSA low-order-point
|
|
17
|
-
// gate on exactly that reasoning. An inbound request is untrusted input, where the gate is the
|
|
18
|
-
// point.
|
|
7
|
+
// signature engine and no second, weaker verifier of untrusted requests can drift. The producing
|
|
8
|
+
// side runs its own check over what it just signed (pki-build's assertSignatureVerifies, called
|
|
9
|
+
// from csr-sign), which is a different question about a key the caller already controls. The slot,
|
|
10
|
+
// the wiring assertion and the octet-alignment refusal are the shared seam (lib/verify-seam.js). It
|
|
11
|
+
// is not wired into index.js and is reached only by require.
|
|
19
12
|
|
|
20
|
-
var guard = require("./guard-all");
|
|
21
13
|
var frameworkError = require("./framework-error");
|
|
14
|
+
var seam = require("./verify-seam");
|
|
22
15
|
|
|
23
|
-
var
|
|
24
|
-
var _engine = null; // path-validate's _verifyWithSpki, injected at path-validate's module load via setEngine.
|
|
25
|
-
|
|
26
|
-
function setEngine(verifyWithSpki) { _engine = verifyWithSpki; }
|
|
16
|
+
var _seam = seam.makeSeam("csr-verify", frameworkError.CsrError, "csr/bad-input");
|
|
27
17
|
|
|
28
18
|
// Verify a parsed certification request's signature over its exact certificationRequestInfo bytes
|
|
29
19
|
// under the subjectPKInfo the request carries. A CSR has no issuer: the verifying key is the one
|
|
30
20
|
// inside the signed preimage, which is what makes the signature a proof of possession and nothing
|
|
31
|
-
// more.
|
|
32
|
-
// never throws out).
|
|
21
|
+
// more.
|
|
33
22
|
function verifyCsrSignature(parsed) {
|
|
34
|
-
|
|
35
|
-
// route that reaches here through the public verb. A future internal consumer that requires this
|
|
36
|
-
// module without it would otherwise call null and surface a raw TypeError out of a verify, which
|
|
37
|
-
// names neither the defect nor a verdict, so the wiring is asserted rather than assumed.
|
|
38
|
-
if (_engine == null) throw new CsrError("csr/bad-input", "the csr-verify signature engine is not initialized (require pki before use)");
|
|
39
|
-
if (!guard.crypto.isOctetAligned(parsed.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
|
|
40
|
-
return _engine(parsed.signatureAlgorithm, parsed.signatureValue.bytes,
|
|
23
|
+
return _seam.verify(parsed.signatureAlgorithm, parsed.signatureValue,
|
|
41
24
|
parsed.subjectPublicKeyInfo.bytes, parsed.certificationRequestInfoBytes);
|
|
42
25
|
}
|
|
43
26
|
|
|
44
|
-
module.exports = { setEngine: setEngine, verifyCsrSignature: verifyCsrSignature };
|
|
27
|
+
module.exports = { setEngine: _seam.setEngine, verifyCsrSignature: verifyCsrSignature };
|
package/lib/guard-parsed.js
CHANGED
|
@@ -508,17 +508,29 @@ function fromTrustedSource(input, kind, claimFields, parse, E, code, why) {
|
|
|
508
508
|
// The claim fields are the ones a parsed structure has and a byte input does not, so an object that
|
|
509
509
|
// was rebuilt is told what happened, never handed to a byte parser that would report something
|
|
510
510
|
// about its type.
|
|
511
|
+
//
|
|
512
|
+
// This list is a TYPE DETECTOR, not the integrity boundary, and reads as the latter if skimmed.
|
|
513
|
+
// Nothing is trusted because it appears here, and no field is unprotected because it does not:
|
|
514
|
+
// fromTrustedSource re-derives the WHOLE structure from the bytes the provenance record names, so a
|
|
515
|
+
// field edited on a parse result is discarded whether or not it is listed. Adding every
|
|
516
|
+
// security-relevant field would therefore change no verdict. What the list decides is narrower: it
|
|
517
|
+
// separates "an object claiming to be a parse result" from "bytes", so the first gets this domain's
|
|
518
|
+
// typed refusal instead of being handed to a byte parser.
|
|
511
519
|
var _CLAIMS = {
|
|
512
520
|
certificate: ["tbsBytes", "subjectPublicKeyInfo", "serialNumberHex"],
|
|
513
521
|
crl: ["tbsBytes", "revokedCertificates", "crlExtensions"],
|
|
514
522
|
cms: ["signerInfos", "encapContentInfo"],
|
|
515
523
|
csr: ["certificationRequestInfoBytes", "subjectPublicKeyInfo", "attributes"],
|
|
524
|
+
crmf: ["messages"],
|
|
525
|
+
attributeCertificate: ["tbsBytes", "holder", "attributes"],
|
|
516
526
|
};
|
|
517
527
|
var _WHY = {
|
|
518
528
|
certificate: "the signed byte range, the signature and the fields that range encodes are separate properties of a parsed object, so a rebuilt certificate (Object.assign, spread, a JSON round-trip) could have them describe different certificates: keep a real CA certificate's signed bytes and signature, replace only its public key, and every field is still well-formed",
|
|
519
529
|
crl: "the signed byte range, the revocation list and the scope extensions are separate properties of a parsed object, so a rebuilt CRL could have them describe different CRLs: empty the revocation list and a correctly signed CRL reports a revoked certificate as good",
|
|
520
530
|
cms: "the signed attribute bytes, the signature, the encapsulated content and the certificates that verify it are separate properties of a parsed object, so a rebuilt SignedData could have them describe different messages: keep a genuine signer's signature and signed attributes, put other content beside them, and every part of the check passes for content that signer never signed",
|
|
521
531
|
csr: "the signed byte range, the key it proves possession of and the subject and requested extensions that range encodes are separate properties of a parsed object, so a rebuilt request could have them describe different requests: keep a genuine requester's signed bytes and signature, replace only the subject or the extensionRequest, and the proof of possession still verifies while the certificate a CA issues from those fields is for a name and a set of extensions nobody signed",
|
|
532
|
+
crmf: "the byte range a proof of possession covers, the key that possession is proven for and the subject a CA will issue to are separate properties of a parsed object, so a rebuilt message set could have them describe different requests: keep a genuine requester's signed range and signature, replace only the certTemplate subject or public key, and the proof still verifies while the certificate is issued for a name and a key nobody proved anything about",
|
|
533
|
+
attributeCertificate: "the signed byte range, the holder the privileges attach to and the attributes granting them are separate properties of a parsed object, so a rebuilt attribute certificate could have them describe different grants: keep a genuine issuer's signed bytes and signature, replace only the holder or an attribute, and the signature still verifies while the privileges are read for someone the issuer never granted them to",
|
|
522
534
|
};
|
|
523
535
|
// @enforced-by guard-shape-reinlined -- shares the fromTrustedSource shape it composes: a door that
|
|
524
536
|
// tests the claim fields itself, instead of routing here, is the re-inline both replace.
|
|
@@ -542,7 +554,8 @@ function acceptDerived(input, kind, parse, E, code, label) {
|
|
|
542
554
|
throw E(code, who + " must be a " + kind + " DER Buffer, a PEM string, or a parsed " + kind);
|
|
543
555
|
}
|
|
544
556
|
}
|
|
545
|
-
var ns = { certificate: "x509", crl: "crl", cms: "cms", csr: "csr"
|
|
557
|
+
var ns = { certificate: "x509", crl: "crl", cms: "cms", csr: "csr", crmf: "crmf",
|
|
558
|
+
attributeCertificate: "attrcert" }[kind];
|
|
546
559
|
return fromTrustedSource(input, kind, claims, parse, E, code,
|
|
547
560
|
who + " must be its DER bytes, a PEM string, or an unmodified pki.schema." + ns +
|
|
548
561
|
".parse result: " + _WHY[kind]);
|
package/lib/path-validate.js
CHANGED
|
@@ -41,6 +41,8 @@ var ocsp = require("./schema-ocsp");
|
|
|
41
41
|
var ocspVerify = require("./ocsp-verify");
|
|
42
42
|
var crlVerify = require("./crl-verify");
|
|
43
43
|
var csrVerify = require("./csr-verify");
|
|
44
|
+
var crmfVerify = require("./crmf-verify");
|
|
45
|
+
var attrcertVerify = require("./attrcert-verify");
|
|
44
46
|
var cmpVerify = require("./cmp-verify");
|
|
45
47
|
var cmsVerify = require("./cms-verify");
|
|
46
48
|
var cmpSession = require("./cmp-session");
|
|
@@ -2275,6 +2277,14 @@ crlVerify.setEngine(_verifyWithSpki);
|
|
|
2275
2277
|
// EdDSA low-order-point gate because it runs over a key the caller already controls.
|
|
2276
2278
|
csrVerify.setEngine(_verifyWithSpki);
|
|
2277
2279
|
|
|
2280
|
+
// And for RFC 4211 proof of possession, so an inbound CertReqMsg is checked through the same one
|
|
2281
|
+
// engine as the PKCS#10 form of the same question.
|
|
2282
|
+
crmfVerify.setEngine(_verifyWithSpki);
|
|
2283
|
+
|
|
2284
|
+
// And for an RFC 5755 attribute certificate, whose AC issuer is an out-of-path signer reached the
|
|
2285
|
+
// same way.
|
|
2286
|
+
attrcertVerify.setEngine(_verifyWithSpki);
|
|
2287
|
+
|
|
2278
2288
|
// ---- the OCSP revocation checker (RFC 6960) -------------------------------
|
|
2279
2289
|
|
|
2280
2290
|
// The OCSP response-verification core (responder authorization + CertID binding + currency +
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -359,7 +359,7 @@ function _legacyV1Error() {
|
|
|
359
359
|
return new AttrCertError("attrcert/legacy-v1-not-supported", "AttributeCertificateV1 (X.509-1997) is obsolete and not parsed by this build (RFC 5755 sec. 1)");
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
function
|
|
362
|
+
function _parseAc(input) {
|
|
363
363
|
var root = pkix.decodeRoot(pkix.coerceToDer(input, PARSE_OPTS), PARSE_OPTS);
|
|
364
364
|
// A well-formed legacy AttributeCertificateV1 gets the advertised, stable
|
|
365
365
|
// attrcert/legacy-v1-not-supported on the DIRECT path too -- not a low-level asn1/*
|
|
@@ -369,6 +369,12 @@ function parse(input) {
|
|
|
369
369
|
return schema.walk(ATTRIBUTE_CERTIFICATE, root, NS).result;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
// Recording, because pki.attrcert.verify reaches a verdict from this result: the signed byte range,
|
|
373
|
+
// the holder the privileges attach to and the attributes themselves are separate properties here,
|
|
374
|
+
// so the record is what binds them to one another.
|
|
375
|
+
var parse = guard.parsed.recordingParser("attributeCertificate", _parseAc,
|
|
376
|
+
AttrCertError, "attrcert/bad-input", "an attribute certificate");
|
|
377
|
+
|
|
372
378
|
// The obsolete X.509-1997 AttributeCertificateV1 (RFC 5652 sec. 10.2) is recognized and
|
|
373
379
|
// deferred: decode the envelope (so a malformed input still fails as bad DER), then
|
|
374
380
|
// throw the precise diagnostic rather than routing it to a wrong format.
|
|
@@ -482,6 +488,17 @@ function _oneGN(node, code) {
|
|
|
482
488
|
return { tagNumber: gn.tagNumber, value: gn.value };
|
|
483
489
|
}
|
|
484
490
|
|
|
491
|
+
// readGeneralName(der) -> { tagNumber, value }, the shape every GeneralName in a parsed attribute
|
|
492
|
+
// certificate is surfaced in.
|
|
493
|
+
//
|
|
494
|
+
// @internal -- exported so a verifier comparing a caller's GeneralName against one this module
|
|
495
|
+
// decoded puts BOTH sides through this one decoder. A second decoder would be a second set of
|
|
496
|
+
// per-form conventions, and a comparison whose two halves were produced by different rules is a
|
|
497
|
+
// comparison of the rules rather than of the names.
|
|
498
|
+
function readGeneralName(der, code) {
|
|
499
|
+
return _oneGN(asn1.decode(der), code || "attrcert/bad-input");
|
|
500
|
+
}
|
|
501
|
+
|
|
485
502
|
// SvceAuthInfo ::= SEQUENCE { service GeneralName, ident GeneralName, authInfo OCTET STRING OPTIONAL }
|
|
486
503
|
// -- authenticationInfo (sec. 4.4.1) allows authInfo; accessIdentity (sec. 4.4.2) MUST omit it.
|
|
487
504
|
function _svceAuthInfo(authInfoAllowed, code) {
|
|
@@ -715,6 +732,7 @@ module.exports = {
|
|
|
715
732
|
pemEncode: pemEncode,
|
|
716
733
|
matches: matches,
|
|
717
734
|
matchesV1: matchesV1,
|
|
735
|
+
readGeneralName: readGeneralName,
|
|
718
736
|
validateAttributeValue: validateAttributeValue,
|
|
719
737
|
validateExtensionValue: validateExtensionValue,
|
|
720
738
|
};
|
package/lib/schema-crmf.js
CHANGED
|
@@ -489,10 +489,13 @@ var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
|
|
|
489
489
|
* var m = pki.schema.crmf.parse(der);
|
|
490
490
|
* m.messages[0].certReq.certTemplate.subject.dn; // "CN=req.example"
|
|
491
491
|
*/
|
|
492
|
-
|
|
492
|
+
// Recording, because pki.crmf.verifyPop reaches a verdict from this result: the byte range a
|
|
493
|
+
// proof of possession covers, the key that possession is proven for, and the subject the CA will
|
|
494
|
+
// issue to are separate properties here, so the record is what binds them to one another.
|
|
495
|
+
var parse = pkix.makeRecordingParser({
|
|
493
496
|
pemLabel: null, PemError: PemError, ErrorClass: CrmfError, prefix: "crmf",
|
|
494
497
|
what: "certificate request message", topSchema: CERT_REQ_MESSAGES, ns: NS,
|
|
495
|
-
});
|
|
498
|
+
}, "crmf");
|
|
496
499
|
|
|
497
500
|
/**
|
|
498
501
|
* @primitive pki.schema.crmf.pemDecode
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- the injected-signature-engine seam, once.
|
|
6
|
+
//
|
|
7
|
+
// A CRL, a PKCS#10 certification request and an RFC 4211 CertReqMsg all verify an untrusted
|
|
8
|
+
// signature through the same path-validation engine, which carries the algorithm-confusion, EdDSA
|
|
9
|
+
// low-order and composite gates. None of them may reach that engine through pki.path's public
|
|
10
|
+
// surface, so each holds a slot that path-validate fills at its module load. Each was writing the
|
|
11
|
+
// same slot, the same wiring assertion and the same octet-alignment refusal. That shape lives here
|
|
12
|
+
// now. A format module supplies what is its own: an error class, a code, and the preimage bytes.
|
|
13
|
+
//
|
|
14
|
+
// The producing side of each format asks a different question, through pki-build's
|
|
15
|
+
// assertSignatureVerifies: whether the signature it has just made verifies under a key the caller
|
|
16
|
+
// already controls. That check waives the EdDSA low-order-point gate for that reason, and is a
|
|
17
|
+
// separate mechanism from this one.
|
|
18
|
+
|
|
19
|
+
var guard = require("./guard-all");
|
|
20
|
+
|
|
21
|
+
// makeSeam(name, ErrorClass, code) -> { setEngine, verify }.
|
|
22
|
+
//
|
|
23
|
+
// `verify(sigAlg, signature, spkiBytes, preimage)` takes the signature as the parsed BIT STRING
|
|
24
|
+
// ({ unusedBits, bytes }), because the octet-alignment refusal is part of the contract: a signature
|
|
25
|
+
// carrying unused bits is malformed for every algorithm these formats admit, and a caller that had
|
|
26
|
+
// already discarded `unusedBits` could not be held to it.
|
|
27
|
+
function makeSeam(name, ErrorClass, code) {
|
|
28
|
+
var engine = null;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
setEngine: function (verifyWithSpki) { engine = verifyWithSpki; },
|
|
32
|
+
|
|
33
|
+
verify: function (sigAlg, signature, spkiBytes, preimage) {
|
|
34
|
+
// Each format's producing verb requires path-validate for the injection side-effect, so the
|
|
35
|
+
// engine is set on every route that reaches here through a public verb. A future internal
|
|
36
|
+
// consumer that required a seam without it would otherwise call null and surface a raw
|
|
37
|
+
// TypeError out of a verify, which names neither the defect nor a verdict.
|
|
38
|
+
if (engine == null) {
|
|
39
|
+
throw new ErrorClass(code, "the " + name + " signature engine is not initialized (require pki before use)");
|
|
40
|
+
}
|
|
41
|
+
if (!guard.crypto.isOctetAligned(signature)) return Promise.resolve(false);
|
|
42
|
+
return engine(sigAlg, signature.bytes, spkiBytes, preimage);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { makeSeam: makeSeam };
|
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:75f14a7c-3b79-4cbc-8db6-fb4987becceb",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-19T13:58:32.723Z",
|
|
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.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.15",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.15",
|
|
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.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.15",
|
|
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.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.15",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|