@blamejs/pki 0.3.25 → 0.3.27
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 +20 -0
- package/README.md +2 -3
- package/index.js +5 -4
- package/lib/acme.js +12 -17
- package/lib/cmp-build.js +17 -2
- package/lib/cmp-session.js +1045 -0
- package/lib/cmp-verify.js +723 -0
- package/lib/est.js +29 -12
- package/lib/lint.js +3 -26
- package/lib/path-validate.js +11 -0
- package/lib/schema-pkcs12.js +5 -66
- package/lib/schema-pkix.js +117 -0
- package/lib/sleep.js +23 -0
- package/lib/webcrypto.js +31 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.3.27 — 2026-07-31
|
|
8
|
+
|
|
9
|
+
pki.cmp.session drives a full CMP certificate enrollment end to end -- build, transfer, and verify every leg of an ir/cr/kur/p10cr exchange, with every response protection-checked before its body is read.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cmp.session(opts) returns a stateful CMP enrollment session; session.enroll(request) drives an RFC 9810 ir / cr / kur / p10cr exchange to completion and returns a terminal verdict { outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }. Every response is protection-verified (pki.cmp.verify) and bound to the transaction by transactionID and a fresh-senderNonce / echoed-recipNonce chain before its body is read, and is advanced only if its signer is trusted (chains to a supplied anchor, or the shared secret matches); a waiting status is polled under a bounded pollReq/pollRep loop; a grant is confirmed by a certConf/pkiConf handshake unless implicit confirmation was granted. opts.key + opts.cert select signature protection (with opts.trustAnchors REQUIRED to authenticate the CA's response signer, opts.intermediates an extra chain pool) or opts.mac selects PBMAC1; opts.transport / opts.tls / opts.timeout / opts.maxResponseBytes configure transfer; opts.maxPolls / opts.maxTotalWait / opts.sleep bound the poll loop; opts.sender / opts.recipient / opts.implicitConfirm / opts.extraCerts tune the request. opts.acceptCert is an acceptance policy consulted before confirmation: it can inspect and veto a grantedWithMods certificate the CA changed, sending a rejecting certConf and returning outcome rejected with the certificate still surfaced. A MAC-protected ir / cr / kur must carry the requested key's private half as the request-arm key so the CRMF proof of possession is signed (a signature session reuses its protection key). A verified rejection or error, or an exhausted poll budget, is a terminal outcome; a tampered, unverifiable, untrusted, or nonce-mismatched response is a typed CmpError throw. RFC 9810 sec. 5.1.1 / 5.2.3 / 5.3.4 / 5.3.18 / 5.3.22, RFC 9811, RFC 9483.
|
|
14
|
+
|
|
15
|
+
## v0.3.26 — 2026-07-30
|
|
16
|
+
|
|
17
|
+
pki.cmp.verify checks the protection on an incoming CMP PKIMessage -- verify a signature or PBMAC1 MAC over the exact protected bytes, and optionally chain the signer certificate to a trust anchor.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- pki.cmp.verify(message, opts) verifies the protection on an incoming CMP PKIMessage -- a DER Buffer, a PEM CMP block, or an already-parsed pki.schema.cmp.parse result (the protection is always recomputed from the parser's raw header/body wire slices, so a mutated display field on a parsed object cannot desync the crypto). Signature protection is verified through the shared certification-path signature engine; PBMAC1 protection is recomputed from opts.sharedSecret and compared in constant time. opts.signerCert / opts.trustAnchors / opts.intermediates / opts.time drive signer-certificate resolution (opts.signerCert, the message senderKID, or RFC 9483 extraCerts[0]) and full out-of-path certificate validation; opts.transactionID / opts.expectRecipNonce add opt-in response-echo checks; opts.maxIterations bounds the PBKDF2 work. The result is a fail-closed verdict { valid, trusted, protectionType, protectionAlg, signer, transactionID, senderNonce, recipNonce, header, body } carrying a typed cmp/* code on rejection; a malformed message or a flavor/credential mismatch throws a typed CmpError. RFC 9810 sec. 5.1.3, RFC 9481 sec. 3 / 6.1.2, RFC 9579, RFC 9483 sec. 3.1 / 3.2 / 3.3.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The EST and ACME clients (pki.est / pki.acme) now reset the origin-specific tls.servername (SNI) on a cross-origin redirect / request even when no mTLS client certificate is set, so the trusted host's SNI is never sent to a different origin. A caller's tls.checkServerIdentity pin is RETAINED across the origin boundary and re-evaluated against the redirected host, so a certificate / SPKI pin keeps applying rather than being silently bypassed by dropping the callback.
|
|
26
|
+
|
|
7
27
|
## v0.3.25 — 2026-07-26
|
|
8
28
|
|
|
9
29
|
pki.path.build can now fetch a missing intermediate over the network -- opt in with `fetchAia` and it discovers the issuer from a certificate's AIA caIssuers URL, so a chain with a gap in the supplied pool still builds.
|
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ is callable today; nothing below is a stub.
|
|
|
227
227
|
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985) — `sign(spec, key, opts)` builds and signs a `CertificationRequest`: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER; may be empty), the public key being certified, an optional `extensionRequest` (requested v3 extensions — subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, or an array of pre-encoded Extension DER — that a CA copies into the issued certificate), 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 (what `openssl req -verify` checks). The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 / 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`. Parsing stays at `pki.schema.csr.parse` — `sign` |
|
|
228
228
|
| `pki.attrcert` | RFC 5755 attribute-certificate issuance — `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` public-key-certificate reference, a `fromCertificate` binding derived from a certificate, or an object digest), the validity window (GeneralizedTime), an optional serial (positive, ≤ 20 octets; randomly generated when omitted), the `attributes` (the privilege syntaxes — 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 — 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 / 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` |
|
|
229
229
|
| `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — 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` (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 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
|
|
230
|
-
| `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, 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). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. `transfer(url, message, opts)` carries a built message to a CMP endpoint over the shared `pki.transport` (RFC 9811 HTTP transfer) — one POST of the DER PKIMessage, the response classified fail-closed (200-only success, a non-200 2xx or an un-followed 3xx refused, a 4xx/5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict) with protection surfaced not verified; `wellKnownUrl(base, opts)` builds the RFC 9811 §3.4 `/.well-known/cmp` request-URIs. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl` |
|
|
230
|
+
| `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, 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). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. `transfer(url, message, opts)` carries a built message to a CMP endpoint over the shared `pki.transport` (RFC 9811 HTTP transfer) — one POST of the DER PKIMessage, the response classified fail-closed (200-only success, a non-200 2xx or an un-followed 3xx refused, a 4xx/5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict) with protection surfaced not verified; `wellKnownUrl(base, opts)` builds the RFC 9811 §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming `PKIMessage` — a signature (through the same certification-path engine `pki.crl.verify` / `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, constant-time compared) over the exact `ProtectedPart` reconstructed from the parser's raw slices; fail-closed on an unprotected message, a legacy / KEM MAC algorithm, an omitted keyLength, or a SHA-1 PRF, returning a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signature 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 — 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 RFC 9810 `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport — composing `build` / `transfer` / `verify` — with every response protection-verified, signer-trusted (chained to a supplied anchor, or the shared secret matched), and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` / echoed-`recipNonce` chain) before its body is read, a bounded `pollReq` / `pollRep` loop for a `waiting` status, and a `certConf` / `pkiConf` (or implicit) confirmation with an explicit `hashAlg` for a sig algorithm that does not convey its hash, returning a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`; the signature flavor requires `opts.trustAnchors` to authenticate the CA, and a verified rejection / error or an exhausted poll budget is a terminal verdict while a tampered / untrusted / desynchronized response is a typed throw. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `session` |
|
|
231
231
|
| `pki.crl` | RFC 5280 §5 certificate revocation list issuance — `sign(spec, issuer, opts)` builds and signs a `CertificateList`: a `spec` of `thisUpdate` / `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` + `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; an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 / 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, per-extension criticality is fixed by the RFC, 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), and `isRevoked(crl, serialNumber)` looks a serial up in the revocation list. 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` |
|
|
232
232
|
| `pki.key` | RFC 5958 / RFC 8018 key-material lifecycle — `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 + AES-CBC-Pad): `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, scrypt refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), a malformed parameter set or wrong-length IV is a distinct typed error, and — because a MAC-less PBES2-CBC decrypt must not be 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)` / `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo, delegating the encoding to WebCrypto so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters (an ambiguous RSA/EC import requires `opts.algorithm`). `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards/Montgomery curves, and the FIPS post-quantum ML-DSA / ML-KEM, and `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM; fail-closed with typed `KeyError`. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt` / `decrypt` / `export` / `import` / `generate` / `publicFromPrivate` |
|
|
233
233
|
| `pki.pkcs12` | RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) issuance — `build(spec, opts)` assembles a password-integrity store. `spec` is 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 / nested `safeContents` bags. Keys and certs are validated before wrapping; `friendlyName` (BMPString) and `localKeyId` attributes are single-value. The store is protected by a classic Appendix B HMAC (default, max interop) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`), over SHA-256/384/512, with the 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 (what OpenSSL and NSS consume) — so a file it emits opens in OpenSSL and NSS, cross-checked bidirectionally. The MAC is computed over 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 store's 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 — a signature from any `pki.cms.sign` signer (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite), no MacData (RFC 7292 §4); privacy stays independent, so the `password` still PBES2-encrypts the bags. **Public-key privacy** — per-safe `recipients` (or the `opts.recipientCerts` convenience) wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData` — never GCM) encrypting it to recipient public keys through the shipped `pki.cms.encrypt` recipient model, restricted to certificate recipients (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM — a password or KEK recipient, which `open` could not reopen, is rejected); all four integrity × privacy combinations are permitted (RFC 7292 §3.1). **Legacy-PBE read** — `open` decrypts the RFC 7292 Appendix C 3DES and RC2 bags an `openssl pkcs12 -legacy` / NSS store uses (RC2 via an in-tree RFC 2268 cipher), so an older store opens; the legacy RC4 schemes are refused. Returns DER or a PEM `PKCS12`; fail-closed with typed `Pkcs12Error`. `open(pfx, password, opts)` reads a store back: it verifies the MAC **first** (a wrong password is the MAC verdict, not 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/secrets as raw DER, all with `friendlyName`/`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, the signer surfaced in `signers` but never trust-chained — the caller's `pki.path.validate` step); a legacy-PBE (App. C) store's 3DES / RC2 bags are decrypted (RC4 refused); an `id-envelopedData` (public-key privacy) safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` if absent, every recipient-side fault the uniform `pkcs12/decrypt-failed`); a post-integrity decrypt failure is the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`; it reads what OpenSSL and NSS produce. Parsing stays at `pki.schema.pkcs12.parse` — `build` / `verifyMac` / `open` |
|
|
@@ -272,8 +272,7 @@ certificate and writes a DER (or `--pem`) SignedData to `--out` or stdout.
|
|
|
272
272
|
|
|
273
273
|
### What's coming
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
the remaining EST network verbs (`/serverkeygen`, `/csrattrs`, `/fullcmc`) and
|
|
275
|
+
The remaining EST network verbs (`/serverkeygen`, `/csrattrs`, `/fullcmc`) and
|
|
277
276
|
HTTP Digest auth, SCEP and CMC enrollment, and additional NIST-on-ramp PQC
|
|
278
277
|
signatures as the OID registry admits them are on the roadmap and ride this same
|
|
279
278
|
core. See
|
package/index.js
CHANGED
|
@@ -44,7 +44,7 @@ var x509 = require("./lib/x509-sign");
|
|
|
44
44
|
var csr = require("./lib/csr-sign");
|
|
45
45
|
var attrcert = require("./lib/attrcert-sign");
|
|
46
46
|
var crmf = require("./lib/crmf-sign");
|
|
47
|
-
var cmp = require("./lib/cmp-
|
|
47
|
+
var cmp = require("./lib/cmp-session");
|
|
48
48
|
var crl = require("./lib/crl-sign");
|
|
49
49
|
var key = require("./lib/key");
|
|
50
50
|
var pkcs12 = require("./lib/pkcs12-build");
|
|
@@ -109,9 +109,10 @@ module.exports = {
|
|
|
109
109
|
// or EST enrollment. Parsing lives at pki.schema.crmf.parse.
|
|
110
110
|
crmf: crmf,
|
|
111
111
|
// `cmp` is the RFC 9810 Certificate Management Protocol producing side -- pki.cmp.build assembles a
|
|
112
|
-
// protected PKIMessage (a certificate request / confirmation / revocation / general message)
|
|
113
|
-
// Parsing lives at pki.schema.cmp.parse.
|
|
114
|
-
|
|
112
|
+
// protected PKIMessage (a certificate request / confirmation / revocation / general message),
|
|
113
|
+
// and pki.cmp.verify checks the protection on an incoming one. Parsing lives at pki.schema.cmp.parse.
|
|
114
|
+
// setEngine is the @internal path-validate seam -- kept off the public surface.
|
|
115
|
+
cmp: { build: cmp.build, transfer: cmp.transfer, wellKnownUrl: cmp.wellKnownUrl, verify: cmp.verify, session: cmp.session },
|
|
115
116
|
// `crl` is the RFC 5280 sec. 5 CRL producing side -- pki.crl.sign builds and signs a CertificateList
|
|
116
117
|
// over any registry algorithm, pki.crl.verify checks a CRL signature through the one path-validation
|
|
117
118
|
// signature engine, and pki.crl.isRevoked looks a serial up. Parsing lives at pki.schema.crl.parse.
|
package/lib/acme.js
CHANGED
|
@@ -1182,18 +1182,10 @@ function _resolveLocation(loc, base) {
|
|
|
1182
1182
|
catch (e) { throw E("acme/bad-url", "the Location header did not resolve to a valid URL: " + JSON.stringify(loc), e); }
|
|
1183
1183
|
return _clientUrl(abs);
|
|
1184
1184
|
}
|
|
1185
|
-
// The default poll sleeper:
|
|
1186
|
-
// a large (
|
|
1187
|
-
//
|
|
1188
|
-
var
|
|
1189
|
-
function _defaultSleep(ms) {
|
|
1190
|
-
return new Promise(function (resolve) {
|
|
1191
|
-
(function step(remaining) {
|
|
1192
|
-
if (remaining <= SETTIMEOUT_MAX_MS) { setTimeout(resolve, remaining); return; }
|
|
1193
|
-
setTimeout(function () { step(remaining - SETTIMEOUT_MAX_MS); }, SETTIMEOUT_MAX_MS);
|
|
1194
|
-
})(ms);
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1185
|
+
// The default poll sleeper is the shared bounded sleeper (lib/sleep.js): it chunks a delay above Node's
|
|
1186
|
+
// 32-bit setTimeout ceiling so a large (parser-bounded, up to a year) Retry-After is honored rather than
|
|
1187
|
+
// silently clamped to 1 ms and rapidly re-polled. pki.cmp.session composes the SAME home.
|
|
1188
|
+
var _defaultSleep = require("./sleep").sleep;
|
|
1197
1189
|
function _clientTls(o) {
|
|
1198
1190
|
var t = o.tls || {};
|
|
1199
1191
|
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
@@ -1293,12 +1285,15 @@ function client(directoryUrl, opts) {
|
|
|
1293
1285
|
var trustedOrigin = new URL(dirUrl).origin;
|
|
1294
1286
|
function _tlsFor(url) {
|
|
1295
1287
|
var t = budgets.tls;
|
|
1296
|
-
// Cross-origin, strip the ORIGIN-SPECIFIC identity: the client certificate/key (credential leak)
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1288
|
+
// Cross-origin, strip the ORIGIN-SPECIFIC identity: the client certificate/key (credential leak) and the
|
|
1289
|
+
// pinned `servername` (SNI, which selects the trusted host's certificate). The caller's
|
|
1290
|
+
// `checkServerIdentity` is RETAINED -- it is an ADDITIONAL tightening constraint (a certificate / SPKI
|
|
1291
|
+
// pin, or a stricter host check) that node re-evaluates against the actual host, so a pin keeps applying
|
|
1292
|
+
// and a host-pinned check fails the wrong host closed; dropping it would accept the cross-origin host
|
|
1293
|
+
// under only the default hostname validation, bypassing the pin.
|
|
1294
|
+
if (new URL(url).origin !== trustedOrigin && (t.cert != null || t.key != null || t.servername != null)) {
|
|
1300
1295
|
t = Object.assign({}, t);
|
|
1301
|
-
delete t.cert; delete t.key; delete t.servername;
|
|
1296
|
+
delete t.cert; delete t.key; delete t.servername;
|
|
1302
1297
|
}
|
|
1303
1298
|
return t;
|
|
1304
1299
|
}
|
package/lib/cmp-build.js
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* PKCS#10 via `pki.csr.sign`, or a `certConf` / `pollReq` / `genm` / `rr`), and an optional
|
|
13
13
|
* `PKIProtection` computed over the message. Protection is a signature under the sender key (any
|
|
14
14
|
* registry algorithm) or a PBMAC1 shared-secret MAC. The emitted message round-trips through
|
|
15
|
-
* `pki.schema.cmp.parse` and its protection verifies.
|
|
15
|
+
* `pki.schema.cmp.parse` and its protection verifies. `pki.cmp.transfer` carries a message to a CMP
|
|
16
|
+
* endpoint (RFC 9811), `pki.cmp.verify` checks an incoming message's protection, and `pki.cmp.session`
|
|
17
|
+
* drives a full enrollment transaction end to end over these. Parsing lives at `pki.schema.cmp.parse`.
|
|
16
18
|
* @spec RFC 9810
|
|
17
19
|
* @card Build a CMP PKIMessage with signature or PBMAC1 protection.
|
|
18
20
|
*/
|
|
@@ -77,11 +79,14 @@ var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength:
|
|
|
77
79
|
|
|
78
80
|
var PBMAC1_DEFAULT_ITER = 100000;
|
|
79
81
|
var PBMAC1_DEFAULT_SALT_BYTES = 16;
|
|
82
|
+
var PBMAC1_MIN_SALT = 8; // octets -- RFC 8018 sec. 4.1 (64-bit) floor; matches the pki.cmp.verify verifier floor
|
|
80
83
|
var PBMAC1_DEFAULT_KEYLEN = 32; // bytes -- HMAC-SHA256 key
|
|
84
|
+
var PBMAC1_MIN_KEYLEN = 20; // bytes -- RFC 9579 sec. 9 floor (matches the pki.cmp.verify verifier floor)
|
|
81
85
|
var PBMAC1_MAX_KEYLEN = 1024; // bytes -- an HMAC key beyond a hash block is pointless; bound the work
|
|
82
86
|
var PBMAC1_MIN_ITER = 1000; // RFC 8018 sec. 4.2 recommended minimum -- reject a trivially weak count
|
|
83
87
|
var PBMAC1_PRF = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
|
|
84
88
|
var PBMAC1_MAC_OID = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
|
|
89
|
+
var PBMAC1_PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 }; // PBKDF2 PRF output length (one derived block)
|
|
85
90
|
// PKIFailureInfo named bits (RFC 9810 sec. 5.2.3), position = bit index -- mirrors schema-cmp's decode list;
|
|
86
91
|
// the build -> parse round-trip cross-checks the positions against the parser.
|
|
87
92
|
var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId", "badDataFormat",
|
|
@@ -519,9 +524,19 @@ function _resolveProtection(opts) {
|
|
|
519
524
|
// Bound the PBKDF2 work factors BEFORE deriving -- a huge iterationCount / keyLength is self-inflicted work.
|
|
520
525
|
if (iterationCount > constants.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cmp/bad-input", "opts.mac.iterationCount exceeds the PBKDF2 work-factor cap " + constants.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
521
526
|
var keyLength = m.keyLength != null ? m.keyLength : PBMAC1_DEFAULT_KEYLEN;
|
|
522
|
-
|
|
527
|
+
// RFC 9579 sec. 9 floor (>= 20 bytes): produce only messages the verifier (pki.cmp.verify, same floor) can
|
|
528
|
+
// accept -- a shorter derived key is refused at production time rather than emitted as an unverifiable message.
|
|
529
|
+
if (typeof keyLength !== "number" || !Number.isInteger(keyLength) || keyLength < PBMAC1_MIN_KEYLEN) throw _err("cmp/bad-input", "opts.mac.keyLength must be an integer >= " + PBMAC1_MIN_KEYLEN + " bytes (RFC 9579 sec. 9)");
|
|
523
530
|
if (keyLength > PBMAC1_MAX_KEYLEN) throw _err("cmp/bad-input", "opts.mac.keyLength exceeds the cap " + PBMAC1_MAX_KEYLEN + " bytes");
|
|
531
|
+
// Bound the COMBINED work: a keyLength spanning multiple PRF blocks costs iterationCount HMACs per block, so
|
|
532
|
+
// the product can exceed the per-block iteration cap. Cap it against the same ceiling pki.cmp.verify enforces,
|
|
533
|
+
// so build never emits a message its verify-inverse would refuse as over-budget (RFC 8018 sec. 5.2).
|
|
534
|
+
var blocks = Math.ceil(keyLength / PBMAC1_PRF_HLEN[prf]);
|
|
535
|
+
if (iterationCount * blocks > constants.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cmp/bad-input", "opts.mac combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the PBKDF2 work-factor cap " + constants.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
524
536
|
var salt = m.salt != null ? _b.reqDer(m.salt, "opts.mac.salt") : Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(PBMAC1_DEFAULT_SALT_BYTES)));
|
|
537
|
+
// RFC 8018 sec. 4.1 (64-bit) floor: an empty / short salt loses precomputation resistance -- produce only
|
|
538
|
+
// messages pki.cmp.verify (same floor) accepts, refusing a below-minimum salt at construction time.
|
|
539
|
+
if (salt.length < PBMAC1_MIN_SALT) throw _err("cmp/bad-input", "opts.mac.salt must be at least " + PBMAC1_MIN_SALT + " octets (RFC 8018 sec. 4.1)");
|
|
525
540
|
if (salt.length > constants.LIMITS.PBKDF2_MAX_SALT) throw _err("cmp/bad-input", "opts.mac.salt exceeds " + constants.LIMITS.PBKDF2_MAX_SALT + " bytes");
|
|
526
541
|
var macDesc = { salt: salt, iterationCount: iterationCount, keyLength: keyLength, prfName: PBMAC1_PRF[prf], macName: PBMAC1_MAC_OID[prf] };
|
|
527
542
|
|