@blamejs/pki 0.5.14 → 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 +17 -0
- package/README.md +1 -1
- package/lib/attrcert-sign.js +511 -2
- package/lib/attrcert-verify.js +21 -0
- package/lib/guard-parsed.js +12 -1
- package/lib/path-validate.js +5 -0
- package/lib/schema-attrcert.js +19 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
7
24
|
## v0.5.14 — 2026-08-19
|
|
8
25
|
|
|
9
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.
|
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ 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` |
|
|
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
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` |
|
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/guard-parsed.js
CHANGED
|
@@ -508,12 +508,21 @@ 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"],
|
|
516
524
|
crmf: ["messages"],
|
|
525
|
+
attributeCertificate: ["tbsBytes", "holder", "attributes"],
|
|
517
526
|
};
|
|
518
527
|
var _WHY = {
|
|
519
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",
|
|
@@ -521,6 +530,7 @@ var _WHY = {
|
|
|
521
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",
|
|
522
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",
|
|
523
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",
|
|
524
534
|
};
|
|
525
535
|
// @enforced-by guard-shape-reinlined -- shares the fromTrustedSource shape it composes: a door that
|
|
526
536
|
// tests the claim fields itself, instead of routing here, is the re-inline both replace.
|
|
@@ -544,7 +554,8 @@ function acceptDerived(input, kind, parse, E, code, label) {
|
|
|
544
554
|
throw E(code, who + " must be a " + kind + " DER Buffer, a PEM string, or a parsed " + kind);
|
|
545
555
|
}
|
|
546
556
|
}
|
|
547
|
-
var ns = { certificate: "x509", crl: "crl", cms: "cms", csr: "csr", crmf: "crmf"
|
|
557
|
+
var ns = { certificate: "x509", crl: "crl", cms: "cms", csr: "csr", crmf: "crmf",
|
|
558
|
+
attributeCertificate: "attrcert" }[kind];
|
|
548
559
|
return fromTrustedSource(input, kind, claims, parse, E, code,
|
|
549
560
|
who + " must be its DER bytes, a PEM string, or an unmodified pki.schema." + ns +
|
|
550
561
|
".parse result: " + _WHY[kind]);
|
package/lib/path-validate.js
CHANGED
|
@@ -42,6 +42,7 @@ var ocspVerify = require("./ocsp-verify");
|
|
|
42
42
|
var crlVerify = require("./crl-verify");
|
|
43
43
|
var csrVerify = require("./csr-verify");
|
|
44
44
|
var crmfVerify = require("./crmf-verify");
|
|
45
|
+
var attrcertVerify = require("./attrcert-verify");
|
|
45
46
|
var cmpVerify = require("./cmp-verify");
|
|
46
47
|
var cmsVerify = require("./cms-verify");
|
|
47
48
|
var cmpSession = require("./cmp-session");
|
|
@@ -2280,6 +2281,10 @@ csrVerify.setEngine(_verifyWithSpki);
|
|
|
2280
2281
|
// engine as the PKCS#10 form of the same question.
|
|
2281
2282
|
crmfVerify.setEngine(_verifyWithSpki);
|
|
2282
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
|
+
|
|
2283
2288
|
// ---- the OCSP revocation checker (RFC 6960) -------------------------------
|
|
2284
2289
|
|
|
2285
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/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
|
]
|