@blamejs/pki 0.6.51 → 0.6.53
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 +53 -0
- package/README.md +1 -1
- package/index.js +8 -3
- package/lib/cmp-build.js +23 -2
- package/lib/cmp-session.js +345 -84
- package/lib/cmp-verify.js +314 -0
- package/lib/composite-kem.js +166 -2
- package/lib/guard-name.js +59 -1
- package/lib/guard-parsed.js +17 -1
- package/lib/key.js +196 -1
- package/lib/oid.js +2 -0
- package/lib/path-validate.js +8 -0
- package/lib/schema-cmp.js +1 -1
- package/lib/schema-pkix.js +43 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,59 @@ 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.6.53 — 2026-09-07
|
|
8
|
+
|
|
9
|
+
A composite ML-KEM private key states its public half, so a holder can name the recipient it is.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.key.publicFromPrivate derives the public key of a composite ML-KEM private key (draft-ietf-lamps-pq-composite-kem section 4.1). A composite private key is the ML-KEM seed followed by the traditional private key, and the public key is the ML-KEM encapsulation key followed by the traditional public key under the same OID. Each half states its own: the seed generates the encapsulation key, and the traditional half is read in the encoding its algorithm uses, an RSAPrivateKey, a SEC1 ECPrivateKey, or the raw scalar of an OKP curve. All twelve registered families are covered, and each derives byte-for-byte the public key the draft's own Appendix G test vectors publish for that key. opts.pem returns it armored, as it does for every other family.
|
|
14
|
+
- The derived key is the one the rest of the toolkit takes. It is the recipient pki.cms.encrypt accepts for a composite KEM recipient, and pki.kem.encapsulate establishes a shared secret for it that the private key it came from decapsulates to the same value.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- A composite ML-KEM private key reports why it cannot be read, rather than reporting that a public key could not be derived from it. An AlgorithmIdentifier carrying parameters is kem/bad-algorithm, since the draft requires them absent (section 5.3); a key no longer than the ML-KEM seed is kem/bad-key; and an OID no registered family claims is refused rather than guessed at. The private key material and the copy taken of it are wiped before the call returns, on the refusing paths as well as the deriving one.
|
|
19
|
+
- Each component is held to what the composite OID names it must be. The RSA component's modulus must be the size that family states, and the EC component must be on the curve it states: a SEC1 ECPrivateKey carries its own curve, and exporting the point of one curve under an OID naming another would state a public key nothing can use. Decapsulation reaches the same conclusion by failing the agreement, and a derivation performs no operation that would notice, so it is checked.
|
|
20
|
+
- Every copy of private key material the derivation allocates is cleared before it returns, on the refusing paths as well as the deriving one: the caller's snapshot, the key octets, and the ML-KEM seed re-encoded for the key engine to read. The EC component adds two more, the scalar taken out of the ECPrivateKey and the copy a width check rejects, and a decapsulation refused after its Diffie-Hellman has already run clears that agreement's secret before reporting the refusal.
|
|
21
|
+
- The EC component's point is generated from its scalar rather than read from the key, in the derivation and in pki.kem.decapsulate alike. RFC 5915 section 3 gives an ECPrivateKey an optional public point, which may be written compressed, and the composite public key carries the uncompressed point at a fixed width. The combiner hashes that public key, so the two sides read one function for it: a key storing a compressed point now establishes the same shared secret whichever spelling of it a sender encapsulated to. Generating the point also means the answer is the one the scalar produces, so a point stored beside a scalar that does not produce it does not reach the result.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The X25519 and X448 component of a composite ML-KEM key is imported through an encoding that is now cleared once the import has read it. That encoding holds the component's private scalar and is allocated inside the helper both pki.kem.decapsulate and the new derivation reach the curve through, so a decapsulation left a copy of it behind.
|
|
26
|
+
|
|
27
|
+
## v0.6.52 — 2026-09-07
|
|
28
|
+
|
|
29
|
+
A CMP client can accept a key pair the CA generated for it, and refuses one from an authority that never claimed to be one.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- pki.cmp.openKeyPackage(container, opts) opens a centrally generated private key delivered in a CMP response (RFC 9483 section 4.1.6). The container is the CMS EnvelopedData a CA puts in the privateKey field of a granted CertifiedKeyPair, holding a CMS SignedData over an RFC 5958 AsymmetricKeyPackage. Both layers are undone in order and the authority is authorized before any key material is returned. The result is { keys, kga, trusted }: keys is every OneAsymmetricKey in the package, in order, each its own DER Buffer that is a PKCS#8 PrivateKeyInfo ready for pki.key.import; kga is the signer certificate whose signature was checked; trusted says whether that certificate chained. Each element is parsed as a OneAsymmetricKey before it is surfaced, so a SEQUENCE carrying neither an algorithm nor key octets is refused rather than handed back as a key.
|
|
34
|
+
- Both CMS layers are accepted in either form they are seen in. A ContentInfo opens with the OBJECT IDENTIFIER naming its content type; the bare structure opens with its own CMSVersion INTEGER. A CMP EncryptedKey carries the EnvelopedData itself (RFC 9810 section 5.2.2), and RFC 5652 section 6.1 encrypts the content rather than a ContentInfo around it, so the plaintext under an id-signedData encryptedContentInfo is a SignedData. The two forms are structurally exclusive, so neither is guessed at.
|
|
35
|
+
- Which key management technique opens the container follows the protection of the request that asked for the key. opts.key, with an optional opts.cert, opens the key transport and key agreement containers a signature-protected request selects (sections 4.1.6.1 and 4.1.6.2); opts.password opens the password container a MAC-protected request selects (section 4.1.6.3). The shared secret is copied at the door and wiped when the call returns, whether the container opened or not.
|
|
36
|
+
- The key generation authority is authorized on both of the section's terms. Its certificate must chain to an anchor in opts.trustAnchors, and it must assert the id-kp-cmKGA extended key usage (RFC 9480 section 2.2). A certificate carrying no extendedKeyUsage extension satisfies a requiredEku path constraint, since RFC 5280 section 4.2.1.12 constrains a certificate only when the extension is present, so the assertion is checked separately: an absent, empty, or differently-purposed extension does not authorize. The chain is still held to requiredEku as well, which reaches the intermediates an issuer's own extended key usage constrains.
|
|
37
|
+
- opts.trustAnchors and opts.intermediates each take one certificate or a list of them, as DER or PEM, the way pki.cmp.verify takes them. A pki.cmp.session forwards both of its own pools the same way, including an already-parsed certificate, which it hands on as the bytes it was parsed from, and the { name, publicKey, algorithm } anchor tuple, which it passes through; a certificate that went missing on the way would make the authority's chain unbuildable and read as an unauthorized authority.
|
|
38
|
+
- pki.cmp.session takes opts.authorizedBySharedSecret too, for the same exemption, and never infers it. A PBMAC1 session that accepts central key generation authorizes the key generation authority against opts.trustAnchors or by the secret that protects its requests, and must say which: a missing anchor list is a missing option, not a decision to drop the chain check and the id-kp-cmKGA assertion. A session with neither is refused when it is built, so the authority never generates a key for a session that cannot accept one.
|
|
39
|
+
- opts.authorizedBySharedSecret states the exemption section 4.1.6 gives an entity that protected its request with a shared secret, which MAY authorize the authority by that secret instead of validating its certificate. On pki.cmp.openKeyPackage it applies only to a container opened with opts.password, and it refuses opts.trustAnchors so that one call names one authorization rule; the result reports trusted: false. On pki.cmp.session it pairs with opts.trustAnchors, because there those anchors do a second job the exemption does not touch: they path-validate the issued certificate. A session accepting a generated key pair states one of the two rules, and one holding neither is refused when it is built rather than when the grant arrives, so the authority never generates a key for a session that cannot accept it.
|
|
40
|
+
- pki.cmp.session takes opts.acceptCentralKeyGeneration, which drives the whole exchange rather than only the response. The request goes out in the shape section 4.1.6 defines: a certTemplate with no publicKey, or one whose subjectPublicKey is a zero-length BIT STRING stating an algorithm preference, and no proof of possession, since this entity holds no key to prove possession of. It carries cmp2021 in its header, because the response will carry an EnvelopedData (RFC 9480 section 2.20), and so does every later leg of that transaction: a poll asking for the grant expects the EnvelopedData just as the request did, and a resumed poll in a later process is the same transaction. The session then opens the delivered container with the credential it enrolled under, authorizes the authority against the anchors it already holds at the instant opts.time names, and surfaces the result on the enrollment verdict as deliveredKey. A shared-secret session authorizes by its secret when it says so with opts.authorizedBySharedSecret, and never because it holds no anchors. An enrollment that delivered no key reports deliveredKey: null. Without the option, such a request is refused before it is sent and such a grant is refused with cmp/unexpected-arm.
|
|
41
|
+
- The issued certificate must certify the delivered private key, checked before the certConf that would accept the grant. The authority's signature says the key package is authentic and says nothing about which certificate the key belongs with, so a mismatched pair is cmp/bad-key-package: the certificate would be unusable with the key the enrollment received. A central key generation transaction submits no public key of its own, so the delivered key is the only thing its grant is bound to, and a granted CertResponse that carries no privateKey is cmp/bad-cert-response rather than a confirmed issuance.
|
|
42
|
+
- The pair is proven by USING it, not by deriving the public half and comparing. A key structure states its own public half and the key engine reads what it is told: RFC 5958 section 2 gives a OneAsymmetricKey an optional publicKey, an EC key's RFC 5915 ECPrivateKey carries its own public point, and an RSA private key carries the modulus and public exponent outright, so a structure whose private components were replaced while its stored public ones were left alone derives to exactly the public key planted in it. Instead the two halves perform whichever operation the key type can: a signature the public half verifies (RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA), a Diffie-Hellman agreement reached from both sides (X25519, X448), or a key encapsulation the private half decapsulates (ML-KEM). What that establishes is that the delivered key works with the issued certificate; it is not a statement that every field of the private structure is internally consistent, which no operation reveals.
|
|
43
|
+
- One RSA key pair may be identified either by rsaEncryption or by id-RSASSA-PSS (RFC 4055 section 1.2), and the authority that encodes a delivered key need not have chosen what the CA put in the certificate. The key engine reports those two encodings as different key types, so the pair is recognized as one algorithm and exercised under PSS, the padding the restricted encoding permits. What each encoding pins is honored on the section 3.3 terms: the hash and the mask generator must match between the two, while the salt length is a floor rather than a fixed value, so two encodings naming different salt lengths are exercised at the larger and remain a pair. An encoding whose parameters are absent pins nothing, as an rsaEncryption encoding does. Two encodings that pin a different hash or mask generator name no shared operation and are reported as undecidable rather than as different keys, and a different modulus is still refused.
|
|
44
|
+
- A central key generation transaction that outlives its process resumes like any other. Its resumeToken records that the grant is bound to the delivered key rather than to a requested one, and carries no requestedSpki, since there is no requested key. A session resuming it must set opts.acceptCentralKeyGeneration too, and a token naming both bindings, or neither, is refused.
|
|
45
|
+
- pki.schema.cmp.parse surfaces the encoded EnvelopedData of a CertifiedKeyPair privateKey as certifiedKeyPair.privateKey.bytes, alongside the walked structure, so the container can be opened from the bytes the response carried rather than from a re-serialization.
|
|
46
|
+
- The RFC 5958 asymmetric key package content type is registered in the OID registry as aKeyPackage (2.16.840.1.101.2.1.2.78.5).
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The container is held to RFC 9483 section 4.1.6 wherever its shape decides who can read the key or what the signature covers: it must be an EnvelopedData, it must name exactly one RecipientInfo, its encrypted content type must be id-signedData, and the signed content type must be id-ct-KP-aKeyPackage. A second recipient is a second party able to open a key generated for this entity, and the count is read past the RecipientInfo: one KeyAgreeRecipientInfo wraps the content-encryption key once per entry in its recipientEncryptedKeys, each for a different recipient (RFC 5652 section 6.2.2), so a container naming one of those with two entries is refused too. The signed content type is what binds the signature to a key package rather than to whatever else the authority has signed. The section's CMS version literals are not enforced, because RFC 5652 section 5.1 and section 6.1 derive those from the signer and recipient forms in use and a conforming sender reaches different values. Its choice-of-form rules, the signer identifier form and the ordering of the certificates the SignedData carries, are not judged either; both live inside the encrypted SignedData rather than in the container bytes, and the result carries only the signer certificate. The number of keys in the package is held where it decides something: section 4.1.6 gives the eContent a sequence of one, and a pki.cmp.session enrollment requires that, since one grant certifies one key and every further key is one the issued certificate says nothing about. pki.cmp.openKeyPackage on its own surfaces a whole RFC 5958 AsymmetricKeyPackage, which is SIZE (1..MAX), and leaves the binding to its caller.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- pki.path.validate refuses a name constraint whose base carries an empty label, instead of treating it as one that matches nothing. An rfc822Name, dNSName, or uniformResourceIdentifier base written with a doubled dot ("..example.com", "a..b.example.com"), a second trailing dot (".example.com.."), or dots alone ("..") now fails the certificate with path/name-constraint-unsupported, and so does an rfc822Name base left with no domain ("user@") or no local part ("@example.com"). No mailbox or host name carries an empty label, so such a base matched nothing, and an excluded subtree built on one excluded nothing: a sub-CA required to carry an exclusion could hold one that inspection renders in full and validation never applies. The permitted direction already failed closed. This is the comparison's own question, which is narrower than the well-formed host name a caller is held to at the door, so a base that is not a host name and still names a namespace the comparison can place, such as a label carrying an underscore, a label edged with a hyphen, or a label longer than 63 characters, keeps being enforced exactly as before. A dNSName base that is a bare dot still names the root of the namespace and still matches every name.
|
|
55
|
+
- The refusal of a uniformResourceIdentifier constraint base that is not a fully qualified domain name cites the rule it enforces. RFC 5280 section 4.2.1.10 requires a URI constraint to be specified as a fully qualified domain name and states no such requirement for a dNSName constraint, so .com is a valid dNSName base and is not a valid URI base.
|
|
56
|
+
- pki.cmp.build sets cmp2021 on an ir / cr / kur that asks for central key generation. RFC 9480 section 2.20 needs the version when a message exchange uses EnvelopedData, "for the request being sent or for the expected response", and RFC 9483 section 4.1.6 requires it in the header "already for the initial request message". A request carrying a real public key still goes out as cmp2000, unless its proof of possession is the agreeMAC or encryptedKey method, which has always carried the later version. Those three arms are the ones a keyless certTemplate speaks for: a krr names a key to recover and a ccr names one to cross-certify, so omitting the public key there says nothing about who generates a key and leaves the version alone.
|
|
57
|
+
- pki.cmp.session claims its one transaction slot before it reads the caller's request object. Reading such an object runs whatever accessors it carries, and an accessor can call back into the same session; a slot claimed only after validation let that re-entrant call open a transaction the outer call then wrote its own state over. The re-entrant call is now refused with cmp/bad-input, on enroll, revoke, info and resumePoll alike. A request refused during validation still leaves the session retryable, as before.
|
|
58
|
+
- pki.cmp.session.enroll sends the arm it checked. The request arm and its certTemplate are copied out of the caller's object once, so a field that answers differently on a second read cannot turn a validated client-generated enrollment into a central key generation request the session never admitted. enroll also names an unrecognized request field itself, with cmp/bad-input, rather than leaving it to be dropped.
|
|
59
|
+
|
|
7
60
|
## v0.6.51 — 2026-09-07
|
|
8
61
|
|
|
9
62
|
A CMP message that names one key and is protected by another no longer verifies.
|
package/README.md
CHANGED
|
@@ -235,7 +235,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
235
235
|
| `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` |
|
|
236
236
|
| `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` |
|
|
237
237
|
| `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. A key that cannot sign, which is where an ML-KEM enrollment sits, takes a `POPOPrivKey` proof instead (RFC 4211 sec. 4.2, sec. 4.3): `pop.type` of `keyEncipherment` or `keyAgreement` with `pop.method: 'subsequentMessage'` declares whether an encrypted certificate (`encrCert`) or a challenge (`challengeResp`) completes the proof over the enrolling protocol's next exchange, and `pop.method: 'encryptedKey'` encloses the requester's private key for the CA in a CMS `EnvelopedData` under `id-ct-encKeyWithID`, requiring `pop.privateKey`, `pop.recipients`, the `pop.identifier` sec. 4.2.1 makes mandatory, and `pop.archive: true` because RFC 9810 sec. 5.2.8.3.1 permits the method only where archival is intended. The enclosed key must be the private half of `certTemplate.publicKey`, or the proof demonstrates possession of a key the request never asked to have certified. The `thisMessage` and `dhMAC` alternatives the specification deprecates are refused with their successors named, and `agreeMAC` is not built, since it needs the RFC 2875 static Diffie-Hellman secret against a CA certificate the requester already holds. 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` |
|
|
238
|
-
| `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`, `krr` (key recovery, §5.3.7), `ccr` (cross-certification, §5.3.11: a `CertReqMessages` at the normative floor, where the requesting CA keeps its private key so a private-key-transport encryptedKey proof-of-possession is refused, and the optional App. D.6 single-request cardinality is not enforced), `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`, and the registration-authority `nested` wrapper of complete PKIMessages (RFC 9810 §5.1.3.5). Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived), or `opts.kem`, a KEM-based MAC for a client whose key can only establish secrets (RFC 9810 §5.1.3.4): the ciphertext a peer encapsulated to its ML-KEM key is decapsulated, the key is derived under HKDF-SHA256 with a context bound to the transaction identifier of the message that carried the ciphertext, and `pki.cmp.verify` checks it from the shared secret. Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. A `p10cr` request's PKCS#10 proof-of-possession is verified before the message is protected, so one whose self-signature does not verify under its subject public key is refused rather than sent. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a 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 }`. A certification authority can take longer to answer than a process is held open, so a `poll-timeout` carries a `resumeToken` and `resumePoll(token)` continues the same transaction in a later process: the token is plain JSON naming the transaction, the nonce the next request echoes, the polled request and the arm its grant arrives on, and the key the grant must certify. It carries no secret and grants nothing, since the protection material and the endpoint come from the session built to resume with, and the resumed poll verifies, nonce-binds, and key-binds every response. Its fields are what the resumed exchange is held to, so store it where the enrollment's own state is stored: someone who can rewrite it can widen what the resumed session accepts, as far as that session's trust anchors and the authority's protection allow and no further. The same session also drives the other two RFC 9483 operations under that shell: `revoke(request)` sends an `rr` naming a certificate as `{ certificate }` or `{ certDetails }` with an optional CRLReason `reason`, and returns `revoked` / `rejected` / `poll-timeout`; `info(request)` sends a `genm` for one of the four support messages — `{ caCerts }`, `{ rootCaCert }`, `{ certReqTemplate }`, `{ crlUpdate }` — and returns `answered` with the decoded `value` and a `present` flag, since an absent response value is how each says nothing is available. A session revokes its own certificate: the signature over the request is the proof of authorization (§4.2), so the named certificate must be `opts.cert` and a PBMAC1 session is refused. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `session` |
|
|
238
|
+
| `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`, `krr` (key recovery, §5.3.7), `ccr` (cross-certification, §5.3.11: a `CertReqMessages` at the normative floor, where the requesting CA keeps its private key so a private-key-transport encryptedKey proof-of-possession is refused, and the optional App. D.6 single-request cardinality is not enforced), `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`, and the registration-authority `nested` wrapper of complete PKIMessages (RFC 9810 §5.1.3.5). Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived), or `opts.kem`, a KEM-based MAC for a client whose key can only establish secrets (RFC 9810 §5.1.3.4): the ciphertext a peer encapsulated to its ML-KEM key is decapsulated, the key is derived under HKDF-SHA256 with a context bound to the transaction identifier of the message that carried the ciphertext, and `pki.cmp.verify` checks it from the shared secret. Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. A `p10cr` request's PKCS#10 proof-of-possession is verified before the message is protected, so one whose self-signature does not verify under its subject public key is refused rather than sent. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a 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 }`. A certification authority can take longer to answer than a process is held open, so a `poll-timeout` carries a `resumeToken` and `resumePoll(token)` continues the same transaction in a later process: the token is plain JSON naming the transaction, the nonce the next request echoes, the polled request and the arm its grant arrives on, and the key the grant must certify. It carries no secret and grants nothing, since the protection material and the endpoint come from the session built to resume with, and the resumed poll verifies, nonce-binds, and key-binds every response. Its fields are what the resumed exchange is held to, so store it where the enrollment's own state is stored: someone who can rewrite it can widen what the resumed session accepts, as far as that session's trust anchors and the authority's protection allow and no further. The same session also drives the other two RFC 9483 operations under that shell: `revoke(request)` sends an `rr` naming a certificate as `{ certificate }` or `{ certDetails }` with an optional CRLReason `reason`, and returns `revoked` / `rejected` / `poll-timeout`; `info(request)` sends a `genm` for one of the four support messages — `{ caCerts }`, `{ rootCaCert }`, `{ certReqTemplate }`, `{ crlUpdate }` — and returns `answered` with the decoded `value` and a `present` flag, since an absent response value is how each says nothing is available. A session revokes its own certificate: the signature over the request is the proof of authorization (§4.2), so the named certificate must be `opts.cert` and a PBMAC1 session is refused. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. `openKeyPackage(container, opts)` opens a private key the CA generated centrally and delivered in the `privateKey` field of a granted `CertifiedKeyPair` (RFC 9483 §4.1.6): a CMS `SignedData` over an RFC 5958 `AsymmetricKeyPackage`, sealed in a CMS `EnvelopedData`. `opts.key` (with `opts.cert`) opens the key transport and key agreement containers a signature-protected request selects, `opts.password` the password container a MAC-protected request selects. The authority is authorized before any key material is returned: its certificate must chain to an anchor in `opts.trustAnchors` AND assert the `id-kp-cmKGA` extended key usage, since a certificate with no `extendedKeyUsage` permits every purpose and so authorizes nothing; `opts.authorizedBySharedSecret` states §4.1.6's exemption for an entity that authorizes by the secret that protected its request. Returns `{ keys, kga, trusted }`, where each key is a PKCS#8 `PrivateKeyInfo` ready for `pki.key.import`; `keys` is the whole RFC 5958 `AsymmetricKeyPackage` (`SIZE (1..MAX)`), while a session enrollment requires the sequence of one §4.1.6 profiles it to. Both CMS layers are read as a ContentInfo or as the bare structure, told apart by what the SEQUENCE opens with. `session(opts)` drives the whole exchange under `opts.acceptCentralKeyGeneration`: the request omits `certTemplate.publicKey`, carries no proof of possession, and goes out as `cmp2021`; the session opens the delivered container with the credential it enrolled under and surfaces it as `deliveredKey`, having first checked that the issued certificate certifies the delivered key. Without the option, such a request and such a grant are both refused. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `openKeyPackage`, `session` |
|
|
239
239
|
| `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` |
|
|
240
240
|
| `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` |
|
|
241
241
|
| `pki.pkcs12` | PKCS#12 (.p12/.pfx) issuance and reading (RFC 7292 / RFC 9579). `build(spec, opts)` assembles a store from the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key, shroudedKey, cert, crl, secret, or nested `safeContents` bags. Keys and certs are validated before wrapping, and `friendlyName` (BMPString) and `localKeyId` are single-value. Integrity is a classic Appendix B HMAC (the default, for maximum interoperability) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`) over SHA-256/384/512, with shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 — which is what OpenSSL and NSS consume, so a file it emits opens in both, cross-checked bidirectionally. The MAC covers the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. Public-key integrity (`opts.integrity.mode: "public-key"`) wraps the AuthenticatedSafe in a CMS SignedData instead of a MAC, signed by any `pki.cms.sign` signer and carrying no MacData (§4); privacy stays independent, so `password` still PBES2-encrypts the bags. Public-key privacy wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData`, never GCM) encrypted to recipient public keys through the `pki.cms.encrypt` recipient model, via per-safe `recipients` or the `opts.recipientCerts` convenience, restricted to certificate recipients (RSA-OAEP, ECDH, X25519, X448, ML-KEM) since a password or KEK recipient could not be reopened by `open`. All four integrity-by-privacy combinations are permitted (§3.1). `open(pfx, password, opts)` reads a store back: it verifies the MAC first, so a wrong password is the MAC verdict rather than a decrypt error, then PBES2-decrypts every privacy safe and shrouded key bag and returns `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` — keys as re-validated PKCS#8 DER, certs, CRLs and secrets as raw DER, all with `friendlyName` and `localKeyId`, nested safes recursively. A MAC-less store is refused unless `opts.allowUnauthenticated`. A public-key-integrity store is verified through its CMS SignedData signature first (`pkcs12/signature-invalid` on failure), with the signer surfaced in `signers` but never trust-chained, which remains the caller's `pki.path.validate` step. A legacy-PBE store's Appendix C 3DES and RC2 bags are decrypted, RC2 through an in-tree RFC 2268 cipher, so an `openssl pkcs12 -legacy` or NSS store opens; the legacy RC4 schemes are refused. An `id-envelopedData` safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` when absent), every recipient-side fault and every post-integrity decrypt failure collapsing to the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`. It reads what OpenSSL and NSS produce. Returns DER or a PEM `PKCS12`, with a typed `Pkcs12Error` on failure. Parsing stays at `pki.schema.pkcs12.parse` — `build`, `verifyMac`, `open` |
|
package/index.js
CHANGED
|
@@ -126,9 +126,11 @@ module.exports = {
|
|
|
126
126
|
crmf: crmf,
|
|
127
127
|
// `cmp` is the RFC 9810 Certificate Management Protocol producing side -- pki.cmp.build assembles a
|
|
128
128
|
// protected PKIMessage (a certificate request / confirmation / revocation / general message),
|
|
129
|
-
// and pki.cmp.verify checks the protection on an incoming one.
|
|
129
|
+
// and pki.cmp.verify checks the protection on an incoming one. pki.cmp.openKeyPackage opens a
|
|
130
|
+
// centrally generated private key a CA delivered. Parsing lives at pki.schema.cmp.parse.
|
|
130
131
|
// setEngine is the @internal path-validate seam -- kept off the public surface.
|
|
131
|
-
cmp: { build: cmp.build, transfer: cmp.transfer, wellKnownUrl: cmp.wellKnownUrl, verify: cmp.verify,
|
|
132
|
+
cmp: { build: cmp.build, transfer: cmp.transfer, wellKnownUrl: cmp.wellKnownUrl, verify: cmp.verify,
|
|
133
|
+
openKeyPackage: cmp.openKeyPackage, session: cmp.session },
|
|
132
134
|
// `crl` is the RFC 5280 sec. 5 CRL producing side -- pki.crl.sign builds and signs a CertificateList
|
|
133
135
|
// over any registry algorithm, pki.crl.verify checks a CRL signature through the one path-validation
|
|
134
136
|
// signature engine, and pki.crl.isRevoked looks a serial up. Parsing lives at pki.schema.crl.parse.
|
|
@@ -136,7 +138,10 @@ module.exports = {
|
|
|
136
138
|
// `key` is the key-material lifecycle domain -- pki.key.encrypt / decrypt a private key under RFC 8018
|
|
137
139
|
// PBES2 (EncryptedPrivateKeyInfo), pki.key.export / import a PKCS#8 private or SPKI public key, and
|
|
138
140
|
// pki.key.generate / publicFromPrivate over every WebCrypto algorithm. Parsing lives at pki.schema.pkcs8.
|
|
139
|
-
|
|
141
|
+
// Curated: correspondsTo is the @internal check pki.cmp.session holds a delivered key and its
|
|
142
|
+
// certificate to each other with, and stays off the public surface.
|
|
143
|
+
key: { encrypt: key.encrypt, decrypt: key.decrypt, export: key.export, import: key.import,
|
|
144
|
+
generate: key.generate, publicFromPrivate: key.publicFromPrivate },
|
|
140
145
|
// `pkcs12` is the RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) producing side -- pki.pkcs12.build assembles a
|
|
141
146
|
// password-integrity store (key/cert/crl/secret bags in an AuthenticatedSafe, shrouded keys + cert safes
|
|
142
147
|
// under PBES2, a classic HMAC or PBMAC1 MAC), and pki.pkcs12.verifyMac checks a store's MAC. Parsing lives
|
package/lib/cmp-build.js
CHANGED
|
@@ -457,7 +457,26 @@ function _encodeKeyRecRepContent(spec, state) {
|
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
var CMP2021_POP_METHODS = intrinsic.assign(intrinsic.create(null), { agreeMAC: 1, encryptedKey: 1 });
|
|
460
|
-
|
|
460
|
+
|
|
461
|
+
/** @internal RFC 9480 sec. 2.20: cmp2021 is needed when a message exchange uses EnvelopedData,
|
|
462
|
+
* "for the request being sent or for the expected response". A request that omits the certTemplate
|
|
463
|
+
* publicKey, or fills its subjectPublicKey with a zero-length BIT STRING, asks the authority to
|
|
464
|
+
* generate the key pair, and the response then carries it in an EnvelopedData, so RFC 9483 sec. 4.1.6
|
|
465
|
+
* requires the version in the header "already for the initial request message". */
|
|
466
|
+
function _templateRequestsCentralKeyGeneration(certTemplate) {
|
|
467
|
+
if (!certTemplate) return true;
|
|
468
|
+
var spki = certTemplate.publicKey;
|
|
469
|
+
if (spki == null) return true;
|
|
470
|
+
return !!spki.publicKey && spki.publicKey.bytes.length === 0;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** @internal The enrollment arms, and the only ones a keyless certTemplate speaks for. RFC 9483
|
|
474
|
+
* sec. 4.1.6 asks the authority to generate the key pair by omitting the public key from an
|
|
475
|
+
* enrollment request; a krr names a key to recover and a ccr names one to cross-certify, so the same
|
|
476
|
+
* omission there says nothing about who generates a key and does not decide the version. */
|
|
477
|
+
var CENTRAL_KEYGEN_ARMS = intrinsic.assign(intrinsic.create(null), { ir: 1, cr: 1, kur: 1 });
|
|
478
|
+
|
|
479
|
+
function _certReqNeedsCmp2021(crmfDer, arm) {
|
|
461
480
|
var messages;
|
|
462
481
|
try { messages = schemaCrmf.parse(crmfDer).messages; }
|
|
463
482
|
catch (e) {
|
|
@@ -467,6 +486,8 @@ function _popNeedsCmp2021(crmfDer) {
|
|
|
467
486
|
for (var i = 0; i < messages.length; i++) {
|
|
468
487
|
var popo = messages[i].popo;
|
|
469
488
|
if (popo && popo.method && _hasOwn(CMP2021_POP_METHODS, popo.method)) return true;
|
|
489
|
+
if (!popo && _hasOwn(CENTRAL_KEYGEN_ARMS, arm)
|
|
490
|
+
&& _templateRequestsCentralKeyGeneration(messages[i].certReq.certTemplate)) return true;
|
|
470
491
|
}
|
|
471
492
|
return false;
|
|
472
493
|
}
|
|
@@ -499,7 +520,7 @@ function _encodeBody(bodySpec, key, opts) {
|
|
|
499
520
|
delete reqSpec.key;
|
|
500
521
|
}
|
|
501
522
|
return crmf.build(reqSpec, popKey, {}).then(function (crmfDer) {
|
|
502
|
-
return { bodyTLV: b.explicit(tag, b.raw(crmfDer)), usesCmp2021:
|
|
523
|
+
return { bodyTLV: b.explicit(tag, b.raw(crmfDer)), usesCmp2021: _certReqNeedsCmp2021(crmfDer, arm) };
|
|
503
524
|
});
|
|
504
525
|
}
|
|
505
526
|
var inner;
|