@blamejs/pki 0.7.44 → 0.8.0

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 CHANGED
@@ -4,6 +4,44 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.8.0 — 2026-09-19
8
+
9
+ Verification is held to the rules signing already applied.
10
+
11
+ ### Added
12
+
13
+ - `pki.C.LIMITS.OID_MAX_SUBIDENTIFIERS` (128) bounds how many sub-identifiers an OBJECT IDENTIFIER may carry, alongside the existing per-sub-identifier octet bound.
14
+ - `pki.C.LIMITS.RSA_MIN_MODULUS_BITS` (2048) and `pki.path.validate` option `minRsaModulusBits`, which may only raise the floor.
15
+ - `allowWeakDigests` on `pki.cms.verify`, `pki.smime.verify` and `pki.tsp.verify`, for verifying an archived signature made under a digest that is no longer collision resistant.
16
+
17
+ ### Changed
18
+
19
+ - `pki.cms.verify` reports a SignerInfo whose message digest is SHA-1 as `cms/weak-digest` instead of verifying it, and does the same for a countersignature at any nesting depth. `pki.smime.verify` and `pki.tsp.verify` inherit this through the same signature check. Pass `allowWeakDigests: true` to verify an archived signature under it. A message digest of MD5, MD4 or MD2 is reported `cms/unsupported-algorithm` and the opt-in does not apply, there being no verification path for those digests. The RFC 3161 message imprint is unaffected: that digest is the requester's choice and an archived timestamp under SHA-1 remains readable.
20
+ - `pki.path.validate` refuses an RSA issuer key below 2048 bits with `path/weak-key`. Raise the floor with `minRsaModulusBits`; it cannot be lowered.
21
+ - A key the crypto engine cannot import now reports `path/unsupported-algorithm` instead of `path/bad-signature`, so a certificate on a curve the engine does not carry no longer reads as forged.
22
+ - `pki.webcrypto.subtle.importKey("spki", ...)` refuses an ECDSA or ECDH key whose parameters are explicit domain parameters or the implicitlyCA form, as RFC 5480 section 2.1.1 requires a namedCurve. Certificate parsing still accepts such a key so it can be inspected and linted.
23
+ - `lint/rfc5280/ski-missing` and `lint/rfc5280/aki-missing` are graded `error` rather than `notice`, matching the RFC 5280 section 4.2.1.1 and 4.2.1.2 MUST clauses and the grade the CRL profile already gives the same absence. `lint/rfc5280/ski-missing-ee` is graded `warn`, its clause being a SHOULD. A pipeline gating on `--severity error` will see findings it did not see before.
24
+ - A `PkiError` whose reason is `timeout` carries `permanent: false`, so a caller can decide to retry from the error itself rather than from a code list of its own. The reason is read without its domain, so a timeout raised by a protocol client under its own prefix (`acme/timeout`, `est/timeout`) carries the flag too. Every other code stays `permanent: true`, including a proxy that refuses CONNECT with a definite status.
25
+
26
+ ### Fixed
27
+
28
+ - Reading an OBJECT IDENTIFIER with more than 128 sub-identifiers throws `oid/too-many-subidentifiers` rather than expanding it. Each sub-identifier costs a bignum and a decimal string, so a 400 KB certificate carrying one such OID cost 55 MB of memory to parse and the ceiling scaled with the 16 MiB input bound. Every format parser reads OIDs through the bounded reader. Encoding is held to the same count, so a value the reader refuses cannot be produced. The generic `pki.asn1.decode` still returns such a value as an opaque primitive node, which allocates nothing beyond the input.
29
+ - `pki.cms.sign` and `pki.cms.countersign` over more than 16 MiB of buffered content threw an untyped `RangeError` instead of a `PkiError`.
30
+ - A CRL extension is decoded only in the scope RFC 5280 defines it for: `cRLNumber` at CRL scope (section 5.2.3), `reasonCode` and `invalidityDate` in a revoked-certificate entry (sections 5.3.1 and 5.3.2). An extension carried in the other scope is left with its raw value, as an unrecognized non-critical extension should be, instead of failing the whole CRL when its value does not parse as that type.
31
+ - The `pki.cms.verify` reference example built a signature with `detached` in the signer descriptor, where it is ignored; the example produced an attached SignedData while describing a detached one.
32
+
33
+ ### Documentation
34
+
35
+ - SECURITY.md states that the toolkit matches no reference identity: name constraints reach the name forms a certificate carries plus the `emailAddress` synthesis RFC 5280 section 4.2.1.10 requires, and a `dNSName` constraint does not reach a subject `commonName`. RFC 9525 section 1.3 retires `commonName` as a source of hostname identity.
36
+
37
+ ## v0.7.45 — 2026-09-13
38
+
39
+ A signature-protected CMP message fills an omitted sender with the signer's subject.
40
+
41
+ ### Changed
42
+
43
+ - pki.cmp.build fills a header sender the caller omits under signature protection with the signer certificate's subject (RFC 9483 sec. 3.1), so a message built without one is receivable rather than refused. A sender the caller states is left as given, matching the signer or not. A signer certificate with an empty subject names itself by a subjectAltName entry, which the builder cannot choose for the caller, so an omitted sender there is still refused with cmp/bad-input. MAC-protected messages carry no signer and are unchanged.
44
+
7
45
  ## v0.7.44 — 2026-09-12
8
46
 
9
47
  A JWS whose embedded jwk carries a private key is refused on verify.
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, the RFC 3739 qualified-certificate statements, the Microsoft certificate template and application policies, subject information access, subject directory attributes, or an array of pre-encoded Extension DER; an extension the issuing CA assigns, such as the authority key identifier or the OCSP no-check marker, is refused by name rather than requested), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned, which is what `openssl req -verify` checks. The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. `verify(request)` asks the same question of a request someone else produced, over its exact signed `certificationRequestInfo` bytes under the `subjectPKInfo` inside them, through the one path-validation signature engine. It answers `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, all re-derived from the signed bytes, so a CA issues from the fields the signature covers rather than from the object it passed in. A `verified: true` says the producer held that private key and that the subject and requested extensions are the ones they signed; who they are is the enrollment protocol's question, not this one. Parsing stays at `pki.schema.csr.parse` — `sign`, `verify` |
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, cRLDistributionPoints, authorityInfoAccess, or pre-encoded Extension DER) each with its RFC 5755 criticality. The authorityKeyIdentifier is emitted without being named (sec. 4.3.3) and holds the AA certificate's subject key identifier; both extension forms are held to the rules sec. 4.3 and 4.4 place on the issuer (an audit identity of 1 to 20 octets, one Targets element and no targetCert, an OCSP location that is an HTTP URL, one distribution point naming a single DN or HTTP / LDAP URL, a role name that is a URI). An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. `verify(ac, issuer, opts)` is the receiving side, performing the RFC 5755 sec. 5 checks an attribute certificate and a named issuer can settle between them: the signature over the exact `AttributeCertificateInfo` bytes through the one path-validation signature engine, the AC naming the issuer this verifier trusts (compared as a distinguished name), the evaluation instant lying within the validity with equality at either bound succeeding as the section states, the sec. 4.3.2 targeting rule, and rejection of any critical extension this verb does not process (section 5 defines support as parsing the value AND rejecting where the value would reject, so `aaControls` and `acProxying`, whose constraints it does not evaluate, are refused when critical). It answers `{ verified, signatureValid, validityChecked, targetingChecked, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }` with the fields re-derived from the signed bytes; the holder's own certificate chain and the AC issuer's chain need certificates the verb is not given, so those slots report `false` rather than letting their absence read as a pass. Parsing stays at `pki.schema.attrcert.parse` — `sign`, `verify` |
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. `pop.method: 'agreeMAC'` completes the four: `pop.key` is the requester's finite-field Diffie-Hellman private key, as a PKCS#8 DER, a PEM `PRIVATE KEY` block or a `KeyObject`, and `pop.caCert` the authority's certificate it already holds; the two agree a static secret (RFC 2875 sec. 3), and the key derived from it MACs the DER `certReq` under `id-dhPop-static-HMAC-SHA1`. Either key may name its group in the PKCS#3 or the X9.42 encoding. `pop.key` must be the private half of `certTemplate.publicKey`, and the template must name both a subject and a public key. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. `verifyPop(messages)` is the receiving side: for each `CertReqMsg` it verifies the `POPOSigningKey` signature over the bytes RFC 4211 sec. 4.1 names — the DER of `poposkInput` when present, of `certReq` when not — through the one path-validation signature engine, and answers `{ verified, messages: [{ verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }` with the fields re-derived from the message's own bytes. Each verdict reports only what its preimage covers: `publicKey` is the key possession was proven for, and `subject` is the requested name when the `certReq` was signed. A `poposkInput` preimage covers the key and the sender alone, so any subject beside it is unsigned and is withheld with `subjectBound: false`. A `raVerified` proof is an RA's out-of-band assertion and a `keyEncipherment` or `keyAgreement` proof completes over a later exchange, so each is reported unverified with its `method` named instead of guessed. Parsing stays at `pki.schema.crmf.parse` — `build`, `verifyPop` |
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; a `transactionID` or `senderNonce` the caller omits is filled with 16 fresh random bytes (RFC 9810 §5.1.1, the width a receiver requires under RFC 9483 §3.5), and a `senderNonce` given under 128 bits is refused; `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 MAC algorithm it does not compute (reported as MAC-protected under that algorithm, never as a failed signature), 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` |
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; a `transactionID` or `senderNonce` the caller omits is filled with 16 fresh random bytes (RFC 9810 §5.1.1, the width a receiver requires under RFC 9483 §3.5), and a `senderNonce` given under 128 bits is refused; a signature-protected `sender` the caller omits is filled with the signer certificate's subject (§3.1); `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 MAC algorithm it does not compute (reported as MAC-protected under that algorithm, never as a failed signature), 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, issuer alternative name, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access, which is every extension RFC 5280 §5.2 defines) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The authority key identifier is emitted on every CRL whether or not it is named, as §5.2.1 places it on every conforming issuer, from the issuer certificate's SKI or the issuer 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, a key paired with a certificate (the `{ key, cert }` form, or bags sharing a `localKeyId`) must be the private half of that certificate's public key, 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/lib/asn1-der.js CHANGED
@@ -483,6 +483,10 @@ function decodeOidContent(buf) {
483
483
  "OID sub-identifier exceeds " + constants.LIMITS.OID_MAX_SUBIDENTIFIER_BYTES + " octets");
484
484
  }
485
485
  if ((b & 0x80) === 0) {
486
+ if (arcs.length >= constants.LIMITS.OID_MAX_SUBIDENTIFIERS) {
487
+ throw new OidError("oid/too-many-subidentifiers",
488
+ "OBJECT IDENTIFIER carries more than " + constants.LIMITS.OID_MAX_SUBIDENTIFIERS + " sub-identifiers");
489
+ }
486
490
  var value = 0n;
487
491
  for (var k = arcStart; k <= i; k++) value = value * 128n + BigInt(buf[k] & 0x7f);
488
492
  arcs.push(value);
@@ -777,7 +781,7 @@ function intToDer(v) {
777
781
  }
778
782
 
779
783
  function encodeOidContent(dotted) {
780
- guard.identifier.assertCanonicalOid(dotted, _oidErr, "oid/bad-input", "OID", "oid/bad-arc");
784
+ guard.identifier.assertCanonicalOid(dotted, _oidErr, "oid/bad-input", "OID", "oid/bad-arc", "oid/too-many-subidentifiers");
781
785
  var arcs = dotted.split(".").map(function (p) { return BigInt(p); });
782
786
  var a1 = arcs[0], a2 = arcs[1];
783
787
  var subids = [a1 * 40n + a2].concat(arcs.slice(2));
package/lib/cmp-build.js CHANGED
@@ -185,6 +185,24 @@ function _headerWithTransactionFields(headerSpec) {
185
185
 
186
186
  var TRANSACTION_FIELD_BYTES = 16;
187
187
 
188
+ /** @internal A signature-protected message names its signer, so a `sender` the caller omits is filled
189
+ * with the signer certificate's subject (RFC 9483 sec. 3.1), the value a receiver holds it to, the
190
+ * same way the transaction fields are filled. This is the fill alone: a `sender` the caller states,
191
+ * whether or not it names the signer, is left as given. An empty-subject signer names itself by a
192
+ * subjectAltName entry, which cannot be chosen for the caller, so an omitted sender there is refused.
193
+ * The header record is this build's own (from `_headerWithTransactionFields`), so the field is set on
194
+ * the object the encoder reads. */
195
+ function _fillSenderFromSigner(header, certDer) {
196
+ if (header.sender != null) return;
197
+ var subject;
198
+ try { subject = x509.parse(certDer).subject; }
199
+ catch (e) { throw _err("cmp/bad-input", "opts.cert is not a valid X.509 certificate", e); }
200
+ if (!subject.rdns.length) {
201
+ throw _err("cmp/bad-input", "message.header.sender is required: the signer certificate's subject is empty, so name one of its subjectAltName entries (RFC 9483 sec. 3.1)");
202
+ }
203
+ header.sender = { directoryName: subject.bytes };
204
+ }
205
+
188
206
  /** @internal 128 fresh random bits, the width RFC 9810 sec. 5.1.1 recommends for a transactionID and
189
207
  * a senderNonce, drawn the way pki.cmp.session draws its nonces. */
190
208
  function _freshTransactionField() {
@@ -842,6 +860,7 @@ async function _build(message, opts) {
842
860
  message = { header: _headerWithTransactionFields(message.header), body: message.body };
843
861
 
844
862
  var prot = _resolveProtection(opts, message);
863
+ if (prot.certDer != null) _fillSenderFromSigner(message.header, prot.certDer);
845
864
  var senderKey = opts.key;
846
865
 
847
866
  var bodyResult = await _encodeBody(message.body, senderKey, opts);
@@ -912,6 +931,10 @@ function _collectExtraCerts(opts, protCertDer) {
912
931
  * wrapped as `NestedMessageContent`, RFC 9810 sec. 5.1.3.5; each entry is checked to parse as a PKIMessage
913
932
  * before it is wrapped, and an empty array or an entry that does not parse is refused). The
914
933
  * message is protected: `opts` carries exactly
934
+ * A signature-protected `header.sender` the caller omits is filled with the signer certificate's subject
935
+ * (RFC 9483 sec. 3.1, the value a receiver holds it to), as the transaction fields are filled; an
936
+ * empty-subject signer, which names itself by a subjectAltName entry, requires an explicit sender. The
937
+ * message is protected: `opts` carries exactly
915
938
  * one of `{ key, cert }` (a signature under the sender key over the message, using any registry algorithm: RSA
916
939
  * / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite, resolved from the certificate) or `{ mac }` (a PBMAC1
917
940
  * shared-secret MAC). The protection is computed over the exact DER of the virtual
package/lib/cms-verify.js CHANGED
@@ -69,6 +69,17 @@ var DIGEST_HASH = intrinsic.assign(intrinsic.create(null), {
69
69
  shake128: "SHAKE128", shake256: "SHAKE256",
70
70
  });
71
71
  var SIG_HASH = intrinsic.assign(intrinsic.create(null), { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" });
72
+ function _digestOidTable(named) {
73
+ var t = intrinsic.create(null);
74
+ intrinsic.forEach(named, function (pair) {
75
+ var dotted = oid.byName(pair[0]);
76
+ if (dotted) t[dotted] = pair[1];
77
+ });
78
+ return t;
79
+ }
80
+ var WEAK_DIGEST_OIDS = _digestOidTable([["sha1", "SHA-1"]]);
81
+ var UNVERIFIABLE_DIGEST_OIDS = _digestOidTable([["md5", "MD5"], ["md4", "MD4"], ["md2", "MD2"]]);
82
+ var WEAK_SIGNATURE_HASHES = intrinsic.assign(intrinsic.create(null), { "SHA-1": 1 });
72
83
  function _supportedDigest(name) { return intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
73
84
  async function _computeDigest(name, content) {
74
85
  return intrinsic.bufferFrom(await subtle.digest(DIGEST_HASH[name], content));
@@ -247,11 +258,21 @@ function _decodeSignedAttrs(setOfBytes) {
247
258
  });
248
259
  }
249
260
 
250
- async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, digestByName) {
261
+ async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, digestByName, allowWeakDigests) {
251
262
  var composite = compositeSig.COMPOSITE_ALGS[si.signatureAlgorithm.oid];
252
263
  if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts, csTarget, digestByName);
253
264
  var scheme = SIG_SCHEME[si.signatureAlgorithm.name];
254
265
  if (!scheme) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported signature algorithm " + JSON.stringify(si.signatureAlgorithm.name) });
266
+ var unverifiableDigest = UNVERIFIABLE_DIGEST_OIDS[si.digestAlgorithm.oid];
267
+ if (unverifiableDigest) {
268
+ return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid,
269
+ message: "the " + unverifiableDigest + " message digest is broken and this toolkit does not verify a signature under it" });
270
+ }
271
+ var weakDigest = WEAK_DIGEST_OIDS[si.digestAlgorithm.oid];
272
+ if (!allowWeakDigests && weakDigest) {
273
+ return _pResolve({ ok: false, code: "cms/weak-digest", sid: si.sid,
274
+ message: "the " + weakDigest + " message digest is not collision resistant; pass allowWeakDigests to verify an archived signature under it" });
275
+ }
255
276
  var digestHash = SIG_HASH[si.digestAlgorithm.name];
256
277
  var pss = scheme.kind === "rsapss" ? _resolvePss(si.signatureAlgorithm.parameters) : null;
257
278
  if (scheme.kind === "rsapss" && !pss) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported or non-conformant RSASSA-PSS parameters (RFC 4055)" });
@@ -261,6 +282,10 @@ async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, dige
261
282
  if (!mldsaNoAttrs && dp !== null && dp !== undefined && !_isDerNull(dp)) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " digest algorithm parameters must be absent or DER NULL (RFC 5754 sec. 2)" });
262
283
  if ((scheme.kind === "mldsa" || scheme.kind === "slhdsa") && si.signedAttrsBytes && (si.digestAlgorithm.name === "shake128" || si.digestAlgorithm.name === "shake256") && dp !== null && dp !== undefined) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "a SHAKE digestAlgorithm carries no parameters (RFC 8702 sec. 3.1)" });
263
284
  var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
285
+ if (!allowWeakDigests && WEAK_SIGNATURE_HASHES[sigHash]) {
286
+ return _pResolve({ ok: false, code: "cms/weak-digest", sid: si.sid,
287
+ message: "the signature algorithm selects the " + sigHash + " digest, which is not collision resistant; pass allowWeakDigests to verify an archived signature under it" });
288
+ }
264
289
  if ((scheme.kind !== "eddsa" && scheme.kind !== "mldsa" && scheme.kind !== "slhdsa" && !sigHash) || (si.signedAttrsBytes && !_supportedDigest(si.digestAlgorithm.name))) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
265
290
  if (scheme.kind === "mldsa" && si.signedAttrsBytes && !_mldsaDigestSuitable(scheme.name, si.digestAlgorithm.name)) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
266
291
  if (scheme.kind === "slhdsa" && si.signedAttrsBytes && si.digestAlgorithm.name !== scheme.digest) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "SLH-DSA " + scheme.name + " requires the " + scheme.digest + " message digest (RFC 9814 sec. 4)" });
@@ -386,7 +411,7 @@ function _surfaceSignedAttrs(si) {
386
411
  });
387
412
  }
388
413
 
389
- async function _verifyCountersignatures(si, parsedCerts) {
414
+ async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
390
415
  var targetSig = _toBuf(si.signature, "the countersigned signature");
391
416
  var values = [];
392
417
  intrinsic.forEach(si.unsignedAttrs || [], function (a) {
@@ -394,16 +419,16 @@ async function _verifyCountersignatures(si, parsedCerts) {
394
419
  intrinsic.forEach(a.values, function (v) { guard.list.append(values, v && v.bytes ? v.bytes : v); });
395
420
  });
396
421
  var out = [];
397
- for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts));
422
+ for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts, allowWeakDigests));
398
423
  return out;
399
424
  }
400
425
 
401
- async function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
426
+ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests) {
402
427
  var csSi;
403
428
  try { csSi = cms.walkCountersignature(asn1.decode(intrinsic.isBuffer(vDer) ? vDer : intrinsic.bufferFrom(vDer))); }
404
429
  catch (e) { return _row({ ok: false, code: (e instanceof CmsError ? e.code : "cms/bad-countersignature"), message: e && e.message }); }
405
- var verdict = await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig);
406
- var nested = await _verifyCountersignatures(csSi, parsedCerts);
430
+ var verdict = await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig, undefined, allowWeakDigests);
431
+ var nested = await _verifyCountersignatures(csSi, parsedCerts, allowWeakDigests);
407
432
  var head = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name };
408
433
  return guard.verdict.of(
409
434
  verdict.code ? guard.verdict.of(head, { code: verdict.code }) : head,
@@ -478,7 +503,7 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
478
503
  * var cert = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
479
504
  * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
480
505
  * var detachedBytes = Buffer.from("hello");
481
- * var p7sDer = await pki.cms.sign(detachedBytes, { cert: cert, key: key, detached: true });
506
+ * var p7sDer = await pki.cms.sign(detachedBytes, { cert: cert, key: key }, { detached: true });
482
507
  * var res = await pki.cms.verify(p7sDer, { content: detachedBytes });
483
508
  * res.valid; // boolean
484
509
  * res.signers[0].ok; // per-signer verdict
@@ -509,7 +534,7 @@ async function _resolveContentDigests(parsed, streamSource) {
509
534
  return byName;
510
535
  }
511
536
 
512
- var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1 });
537
+ var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, allowWeakDigests: 1 });
513
538
 
514
539
  function verify(input, opts) {
515
540
  return guard.async.deferred(function () { return _verify(input, opts); });
@@ -523,6 +548,7 @@ async function _verify(input, opts) {
523
548
  }, _err, "cms/bad-input", "the SignedData");
524
549
  if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
525
550
  var trustCfg = _snapshotTrust(opts);
551
+ var allowWeakDigests = opts.allowWeakDigests === true;
526
552
  var parsedCerts = [];
527
553
  (parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
528
554
  (opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
@@ -544,8 +570,8 @@ async function _verify(input, opts) {
544
570
  var signers = [];
545
571
  for (var _sp = 0; _sp < parsed.signerInfos.length; _sp++) {
546
572
  var siOne = parsed.signerInfos[_sp];
547
- var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName);
548
- var counters = await _verifyCountersignatures(siOne, parsedCerts);
573
+ var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName, allowWeakDigests);
574
+ var counters = await _verifyCountersignatures(siOne, parsedCerts, allowWeakDigests);
549
575
  guard.list.append(signers, guard.verdict.of(verdict, {
550
576
  countersignatures: counters,
551
577
  unsignedAttrs: _surfaceUnsignedAttrs(siOne),
package/lib/constants.js CHANGED
@@ -112,14 +112,17 @@ var LIMITS = Object.assign(Object.create(null), {
112
112
  DER_MAX_BYTES: BYTES.mib(16),
113
113
  DER_MAX_DEPTH: 64,
114
114
  DER_MAX_ITEMS: 4000000,
115
+ NAMED_SCAN_MAX_ELEMENTS: 16777216,
115
116
  MAX_DECODE_DEPTH_CEILING: 256,
116
117
  PEM_MAX_BYTES: BYTES.mib(16),
117
118
  DER_MAX_INTEGER_BYTES: BYTES.kib(16),
118
119
  DH_MAX_MODULUS_BYTES: 512,
119
120
  DH_MIN_SUBGROUP_BYTES: 20,
120
121
  DH_MIN_MODULUS_BYTES: 256,
122
+ RSA_MIN_MODULUS_BITS: 2048,
121
123
  DH_PRIME_MEMO_ENTRIES: 64,
122
124
  OID_MAX_SUBIDENTIFIER_BYTES: 32,
125
+ OID_MAX_SUBIDENTIFIERS: 128,
123
126
  HTTP_MAX_RESPONSE_BYTES: BYTES.mib(24),
124
127
  HTTP_AUTH_HEADER_MAX_BYTES: BYTES.kib(8),
125
128
  CBOR_MAX_BYTES: BYTES.mib(16),
@@ -18,10 +18,14 @@
18
18
  *
19
19
  * `code` is a stable, greppable `domain/reason` string
20
20
  * (`asn1/indefinite-length`, `x509/not-a-certificate`): safe to switch
21
- * on and safe to log. Because every failure here is a deterministic
22
- * verdict on the bytes in hand (a malformed length, an unknown OID
23
- * shape, a truncated certificate), errors are `permanent: true`. The
24
- * same input will never parse on retry.
21
+ * on and safe to log. A failure that is a deterministic verdict on the
22
+ * bytes in hand (a malformed length, an unknown OID shape, a truncated
23
+ * certificate) is `permanent: true`: the same input will never parse on
24
+ * retry. A failure whose reason is `timeout` is `permanent: false`, so a
25
+ * caller can retry on the error rather than on a code list of its own.
26
+ * The reason is read without its domain, so the protocol clients that
27
+ * raise the same failure under their own prefix (`acme/timeout`,
28
+ * `est/timeout`) carry the flag too.
25
29
  *
26
30
  * @card
27
31
  * `PkiError` base class + `defineClass` factory + the per-domain error
@@ -30,9 +34,17 @@
30
34
 
31
35
  var intrinsic = require("./guard-intrinsic");
32
36
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
37
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
33
38
  var _String = intrinsic.String;
34
39
  var _defineProperty = intrinsic.defineProperty;
35
40
 
41
+ var _TRANSIENT_REASONS = intrinsic.assign(intrinsic.create(null), { timeout: 1 });
42
+
43
+ function _reasonOf(c) {
44
+ for (var i = 0; i < c.length; i++) if (_charCodeAt(c, i) === 47) return _strSlice(c, i + 1);
45
+ return c;
46
+ }
47
+
36
48
  function _isCodeShape(c) {
37
49
  var n = c.length, i, ch, slash = -1;
38
50
  if (n < 3) return false;
@@ -79,7 +91,7 @@ class PkiError extends Error {
79
91
  }
80
92
  this.code = c;
81
93
  this.isPkiError = true;
82
- this.permanent = true;
94
+ this.permanent = !_TRANSIENT_REASONS[_reasonOf(c)];
83
95
  }
84
96
  }
85
97
 
@@ -6,6 +6,7 @@ var async = require("./guard-async");
6
6
  var identifier = require("./guard-identifier");
7
7
  var time = require("./guard-time");
8
8
  var intrinsic = require("./guard-intrinsic");
9
+ var _NAMED_SCAN_MAX_ELEMENTS = require("./constants").LIMITS.NAMED_SCAN_MAX_ELEMENTS;
9
10
  var util = intrinsic.assign(intrinsic.create(null), { types: intrinsic.types });
10
11
  var _isArray = intrinsic.isArray;
11
12
  var _bufferFrom = intrinsic.bufferFrom;
@@ -358,7 +359,9 @@ function _deep(v, ErrorClass, code, label, cap, depth, collect) {
358
359
  }
359
360
  if (_isBuffer(v) || _isView(v) || _isArrayBuffer(v)) {
360
361
  var bytesCopy = _copyBytesSameKind(v, ErrorClass, code, label, collect);
361
- _copyNamed(v, bytesCopy, ErrorClass, code, label, cap, depth, collect);
362
+ if (bytesCopy.byteLength <= _NAMED_SCAN_MAX_ELEMENTS) {
363
+ _copyNamed(v, bytesCopy, ErrorClass, code, label, cap, depth, collect);
364
+ }
362
365
  return bytesCopy;
363
366
  }
364
367
  if (require("./guard-parsed").isRecordedAsProduced(v)) return v; // allow:inline-require -- circular load: guard-parsed requires this module
@@ -2,6 +2,7 @@
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
4
  var intrinsic = require("./guard-intrinsic");
5
+ var constants = require("./constants");
5
6
  var util = intrinsic.assign(intrinsic.create(null), { types: intrinsic.types });
6
7
  // @internal
7
8
 
@@ -60,7 +61,7 @@ function _isDottedDecimal(str) {
60
61
  }
61
62
 
62
63
  // @enforced-by behavioral -- string-form OID canonicalization has no rename-proof
63
- function assertCanonicalOid(str, E, code, label, boundsCode) {
64
+ function assertCanonicalOid(str, E, code, label, boundsCode, countCode) {
64
65
  var who = label || "OID";
65
66
  if (typeof str !== "string" || !_isDottedDecimal(str)) {
66
67
  throw E(code, who + " must be a canonical dotted-decimal OID string of two or more arcs with no leading-zero component");
@@ -68,6 +69,9 @@ function assertCanonicalOid(str, E, code, label, boundsCode) {
68
69
  if (boundsCode === null) return str;
69
70
  var bcode = boundsCode === undefined ? code : boundsCode;
70
71
  var parts = _strSplit(str, ".");
72
+ if (parts.length - 1 > constants.LIMITS.OID_MAX_SUBIDENTIFIERS) {
73
+ throw E(countCode || bcode, who + " carries more than " + constants.LIMITS.OID_MAX_SUBIDENTIFIERS + " sub-identifiers");
74
+ }
71
75
  var root = _BigInt(parts[0]);
72
76
  var second = _BigInt(parts[1]);
73
77
  if (root > 2n) throw E(bcode, who + " root arc must be 0, 1, or 2 (X.660)");
package/lib/lint.js CHANGED
@@ -46,6 +46,12 @@ var MS_PER_DAY = C.TIME.days(1);
46
46
  var LintError = frameworkError.LintError;
47
47
  function _cfg(code, message, cause) { return new LintError(code, message, cause); }
48
48
 
49
+ function _sameName(a, b) {
50
+ if (!a || !b || !a.rdns || !b.rdns) return false;
51
+ try { return guard.name.dnEqual(a.rdns, b.rdns, _cfg, "lint/bad-input", "distinguished name") === true; }
52
+ catch (_e) { return a.dn === b.dn; }
53
+ }
54
+
49
55
  var NS = pkix.makeNS("lint", LintError, oid);
50
56
  var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
51
57
 
@@ -368,15 +374,15 @@ var RFC5280_RULES = [
368
374
  },
369
375
  },
370
376
  {
371
- id: "lint/rfc5280/ski-missing", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
372
- message: "a CA certificate should carry a subjectKeyIdentifier extension",
377
+ id: "lint/rfc5280/ski-missing", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
378
+ message: "a CA certificate must carry a subjectKeyIdentifier extension",
373
379
  appliesTo: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return !!(bc && bc.value && bc.value.cA === true); },
374
380
  check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
375
381
  },
376
382
  {
377
- id: "lint/rfc5280/aki-missing", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.1",
378
- message: "a non-self-issued certificate should carry an authorityKeyIdentifier extension",
379
- appliesTo: function (cert) { return !!(cert.issuer && cert.subject && cert.issuer.dn !== cert.subject.dn); },
383
+ id: "lint/rfc5280/aki-missing", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.1",
384
+ message: "a non-self-issued certificate must carry an authorityKeyIdentifier extension",
385
+ appliesTo: function (cert) { return !!(cert.issuer && cert.subject) && !_sameName(cert.issuer, cert.subject); },
380
386
  check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
381
387
  },
382
388
  {
@@ -466,7 +472,7 @@ var RFC5280_RULES = [
466
472
  },
467
473
  },
468
474
  {
469
- id: "lint/rfc5280/ski-missing-ee", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
475
+ id: "lint/rfc5280/ski-missing-ee", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
470
476
  message: "an end-entity certificate should carry a subjectKeyIdentifier extension",
471
477
  appliesTo: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return !(bc && bc.value && bc.value.cA === true); },
472
478
  check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
package/lib/oid.js CHANGED
@@ -208,6 +208,7 @@ var FAMILIES = Object.assign(Object.create(null), {
208
208
  "pbeWithSHAAnd128BitRC2-CBC": 5, "pbeWithSHAAnd40BitRC2-CBC": 6 } },
209
209
 
210
210
  rsadsiDigest: { base: [1, 2, 840, 113549, 2], of: {
211
+ md2: 2, md4: 4, md5: 5,
211
212
  hmacWithSHA1: 7, hmacWithSHA224: 8, hmacWithSHA256: 9,
212
213
  hmacWithSHA384: 10, hmacWithSHA512: 11 } },
213
214
 
@@ -70,7 +70,10 @@ var edwardsPoint = require("./edwards-point");
70
70
  var PathError = errors.PathError;
71
71
  function E(code, message, cause) { return new PathError(code, message, cause); }
72
72
  function pathCode(e, fallback) {
73
- return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
73
+ if (!e || typeof e.code !== "string") return fallback;
74
+ if (e.code.indexOf("path/") === 0) return e.code;
75
+ if (e.code === "webcrypto/not-supported" || e.code === "webcrypto/data") return "path/unsupported-algorithm";
76
+ return fallback;
74
77
  }
75
78
 
76
79
  var contains = guard.list.contains;
@@ -151,6 +154,7 @@ _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", fal
151
154
  _sig("id-alg-hss-lms-hashsig", { name: "HSS-LMS" }, null, "absent", false, true).shbs = true;
152
155
 
153
156
  var OID_RSA_PSS = oid.byName("rsassaPss");
157
+ var OID_RSA_ENCRYPTION = oid.byName("rsaEncryption");
154
158
  var OID_MGF1 = oid.byName("mgf1");
155
159
  var HASH_BY_OID = intrinsic.create(null);
156
160
  HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
@@ -344,6 +348,49 @@ function _shbsVerify(spkiBytes, sigBytes, message) {
344
348
  } catch (e) { return guard.verdict.of({ ok: false, code: pathCode(e, "path/bad-signature"), error: e }); }
345
349
  }
346
350
 
351
+ function _weakIssuerKey(state) {
352
+ var spkiBytes = state.workingPublicKey;
353
+ var isRsa;
354
+ try { isRsa = _issuerKeyIsRsa(spkiBytes); }
355
+ catch (_e) { return null; }
356
+ if (!isRsa) return null;
357
+ var bits;
358
+ try { bits = _rsaModulusBits(spkiBytes); }
359
+ catch (_e) { bits = null; }
360
+ if (bits === null) {
361
+ return E("path/weak-key",
362
+ "the issuer's key names an RSA algorithm and its modulus cannot be read, so its strength cannot be established");
363
+ }
364
+ var floor = state.minRsaModulusBits || constants.LIMITS.RSA_MIN_MODULUS_BITS;
365
+ if (bits >= floor) return null;
366
+ return E("path/weak-key", "the issuer's RSA modulus is " + bits + " bits, below the " + floor + "-bit minimum");
367
+ }
368
+
369
+ function _issuerKeyIsRsa(spkiBytes) {
370
+ if (!spkiBytes || !spkiBytes.length) return false;
371
+ var kids = pkix.rootSequenceChildren(asn1.decode(spkiBytes), 2, 2);
372
+ if (!kids) return false;
373
+ var algKids = pkix.rootSequenceChildren(kids[0], 1);
374
+ if (!algKids) return false;
375
+ var keyAlg = asn1.read.oid(algKids[0]);
376
+ return keyAlg === OID_RSA_ENCRYPTION || keyAlg === OID_RSA_PSS;
377
+ }
378
+
379
+ function _rsaModulusBits(spkiBytes) {
380
+ var spki = asn1.decode(spkiBytes);
381
+ var keyBits = asn1.read.bitString(spki.children[1]);
382
+ var innerKids = pkix.rootSequenceChildren(asn1.decode(keyBits.bytes), 1);
383
+ if (!innerKids) return null;
384
+ var modulus = innerKids[0].content;
385
+ if (!modulus || !modulus.length) return null;
386
+ var i = 0;
387
+ while (i < modulus.length && modulus[i] === 0) i += 1;
388
+ if (i >= modulus.length) return 0;
389
+ var top = modulus[i], topBits = 0;
390
+ while (top > 0) { topBits += 1; top >>= 1; }
391
+ return (modulus.length - i - 1) * 8 + topBits;
392
+ }
393
+
347
394
  function builtinVerify(state, cert) {
348
395
  var d;
349
396
  try {
@@ -702,6 +749,7 @@ function initialize(certs, params, seeds, anchor) {
702
749
  validPolicyTree: rootNode(),
703
750
  policyNodeCount: 1,
704
751
  maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
752
+ minRsaModulusBits: params.minRsaModulusBits !== undefined ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
705
753
  permittedGenerations: intrinsic.filter(seeds.permittedGenerations, function (g) { return g.length > 0; }),
706
754
  excludedSubtrees: seeds.excluded,
707
755
  explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
@@ -1054,7 +1102,7 @@ function validateCriticalExtensionStructure(cert) {
1054
1102
  var _VALIDATE_OPTS = intrinsic.assign(intrinsic.create(null), {
1055
1103
  checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
1056
1104
  initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
1057
- initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, requireRevocation: 1,
1105
+ initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, minRsaModulusBits: 1, requireRevocation: 1,
1058
1106
  requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchors: 1,
1059
1107
  userInitialPolicySet: 1, verifier: 1
1060
1108
  });
@@ -1100,6 +1148,14 @@ async function validate(path, opts) {
1100
1148
  if (!intrinsic.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
1101
1149
  var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
1102
1150
  if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
1151
+ if (opts.minRsaModulusBits != null) {
1152
+ if (typeof opts.minRsaModulusBits !== "number" || !intrinsic.isInteger(opts.minRsaModulusBits)) {
1153
+ throw E("path/bad-input", "validate: opts.minRsaModulusBits must be an integer");
1154
+ }
1155
+ if (opts.minRsaModulusBits < constants.LIMITS.RSA_MIN_MODULUS_BITS) {
1156
+ throw E("path/bad-input", "validate: opts.minRsaModulusBits may only raise the " + constants.LIMITS.RSA_MIN_MODULUS_BITS + "-bit minimum, not lower it");
1157
+ }
1158
+ }
1103
1159
  var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
1104
1160
  var n = certs.length;
1105
1161
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
@@ -1178,7 +1234,10 @@ async function validate(path, opts) {
1178
1234
  var checks = [];
1179
1235
 
1180
1236
  var sigRes;
1181
- if (verifier) {
1237
+ var weak = _weakIssuerKey(state);
1238
+ if (weak) {
1239
+ sigRes = { ok: false, code: "path/weak-key", error: weak };
1240
+ } else if (verifier) {
1182
1241
  var vv;
1183
1242
  try {
1184
1243
  vv = await verifier.verify({
package/lib/schema-crl.js CHANGED
@@ -51,17 +51,19 @@ var TIME = pkix.time(NS);
51
51
 
52
52
  var CRL_VERSION = pkix.versionReader(NS, { "1": 2 });
53
53
 
54
- function decodeExt(ext) {
54
+ function decodeExt(ext, scope) {
55
55
  var value = ext.value;
56
+ var atCrl = scope !== "entry";
57
+ var atEntry = scope !== "crl";
56
58
  try {
57
- if (ext.oid === OID_CRL_NUMBER) {
59
+ if (ext.oid === OID_CRL_NUMBER && atCrl) {
58
60
  value = asn1.read.integer(asn1.decode(ext.value));
59
61
  if (value < 0n) throw new Error("cRLNumber must be non-negative (INTEGER 0..MAX)");
60
- } else if (ext.oid === OID_REASON_CODE) {
62
+ } else if (ext.oid === OID_REASON_CODE && atEntry) {
61
63
  var reason = asn1.read.enumerated(asn1.decode(ext.value));
62
64
  if (!_hasOwn(CRL_REASONS, reason.toString())) throw new Error("undefined CRLReason " + reason.toString());
63
65
  value = Number(reason);
64
- } else if (ext.oid === OID_INVALIDITY_DATE) {
66
+ } else if (ext.oid === OID_INVALIDITY_DATE && atEntry) {
65
67
  var n = asn1.decode(ext.value);
66
68
  if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
67
69
  throw new Error("invalidityDate must be a GeneralizedTime");
@@ -85,7 +87,9 @@ var REVOKED_ENTRY = schema.seq([
85
87
  serialNumber: m.fields.userCertificate.value,
86
88
  serialNumberHex: m.fields.userCertificate.node.content.toString("hex"),
87
89
  revocationDate: m.fields.revocationDate.value,
88
- crlEntryExtensions: m.fields.crlEntryExtensions.present ? m.fields.crlEntryExtensions.value.result.map(decodeExt) : [],
90
+ crlEntryExtensions: m.fields.crlEntryExtensions.present
91
+ ? m.fields.crlEntryExtensions.value.result.map(function (e) { return decodeExt(e, "entry"); })
92
+ : [],
89
93
  };
90
94
  },
91
95
  });
@@ -114,7 +118,9 @@ var TBS_CERTLIST = schema.seq([
114
118
  thisUpdate: m.fields.thisUpdate.value,
115
119
  nextUpdate: m.fields.nextUpdate.present ? m.fields.nextUpdate.value : null,
116
120
  revokedCertificates: m.fields.revokedCertificates.present ? m.fields.revokedCertificates.value.result : [],
117
- crlExtensions: m.fields.crlExtensions.present ? m.fields.crlExtensions.value.result.map(decodeExt) : [],
121
+ crlExtensions: m.fields.crlExtensions.present
122
+ ? m.fields.crlExtensions.value.result.map(function (e) { return decodeExt(e, "crl"); })
123
+ : [],
118
124
  crlExtensionsPresent: m.fields.crlExtensions.present,
119
125
  };
120
126
  },
package/lib/smime.js CHANGED
@@ -56,7 +56,7 @@ var SIGN_OPTS = Object.assign(Object.create(null), {
56
56
  });
57
57
  var VERIFY_OPTS = Object.assign(Object.create(null), {
58
58
  certs: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, strictMicalg: 1,
59
- legacyHeaderProtection: 1, expectedSender: 1,
59
+ legacyHeaderProtection: 1, expectedSender: 1, allowWeakDigests: 1,
60
60
  });
61
61
  var ENCRYPT_OPTS = Object.assign(Object.create(null), {
62
62
  entity: 1, contentType: 1, protectHeaders: 1, headers: 1, hcp: 1,
@@ -758,6 +758,7 @@ async function verify(message, opts) {
758
758
  vOpts.checkPurpose = opts.checkPurpose != null ? opts.checkPurpose : "emailProtection";
759
759
  }
760
760
  if (opts.time !== undefined) vOpts.time = opts.time;
761
+ if (opts.allowWeakDigests === true) vOpts.allowWeakDigests = true;
761
762
  if (_isPkcs7(ct.type, "mime")) {
762
763
  if (ct.params["smime-type"] && ct.params["smime-type"] !== "signed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(ct.params["smime-type"]) + " (only signed-data)");
763
764
  var p7m = _decodeCms(ent);
package/lib/tsp-sign.js CHANGED
@@ -603,7 +603,7 @@ function _buildTsaChains(leaf, pool) {
603
603
  * res.valid; // boolean; pass opts.trustAnchors to also chain the TSA cert to a root
604
604
  * res.genTime; // Date, read from the verified eContent
605
605
  */
606
- var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 });
606
+ var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1, allowWeakDigests: 1 });
607
607
 
608
608
  function _anyCritical(extensions) {
609
609
  return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
@@ -631,7 +631,7 @@ async function verify(token, data, opts) {
631
631
  function fail(code, reason) {
632
632
  return guard.verdict.of({ valid: false, trusted: false, revocationChecked: revocationChecked, anchorConstraints: anchorConstraints, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, policyName: tst.policyName, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null });
633
633
  }
634
- var cmsRes = await cmsVerify.verify(tokenDer, { certs: opts.certs });
634
+ var cmsRes = await cmsVerify.verify(tokenDer, { certs: opts.certs, allowWeakDigests: opts.allowWeakDigests === true });
635
635
  var signer = cmsRes.signers[0];
636
636
  if (!cmsRes.valid) return fail("tsp/bad-signature", signer && (signer.code || signer.message));
637
637
  var tsaCertDer = signer && signer.cert;
package/lib/webcrypto.js CHANGED
@@ -907,6 +907,19 @@ function _preValidatePkcs8(p8, name) {
907
907
  validate(innerBytes);
908
908
  }
909
909
 
910
+ function _preValidateSpki(spki, name) {
911
+ if (name !== "ECDSA" && name !== "ECDH") return;
912
+ var params;
913
+ try {
914
+ var alg = asn1.decode(spki).children[0];
915
+ params = alg.children.length > 1 ? alg.children[1] : null;
916
+ } catch (e) { throw new WebCryptoError("webcrypto/data", "importKey spki: the SubjectPublicKeyInfo is not well-formed", e); }
917
+ if (params && params.tagClass === "universal" && !params.constructed && params.tagNumber === asn1.TAGS.OBJECT_IDENTIFIER) return;
918
+ throw new WebCryptoError("webcrypto/data",
919
+ "importKey spki: an " + name + " key must name its curve as a namedCurve OBJECT IDENTIFIER, " +
920
+ "and this SubjectPublicKeyInfo carries " + (params ? "explicit domain parameters" : "none") + " (RFC 5480 sec. 2.1.1)");
921
+ }
922
+
910
923
  SubtleCrypto.prototype.importKey = async function importKey(format, keyData, algorithm, extractable, keyUsages) {
911
924
  var alg = _normalizeAlg(algorithm, "importKey");
912
925
  var usages = keyUsages || [];
@@ -929,6 +942,7 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
929
942
 
930
943
  if (format === "spki") {
931
944
  var spkiBuf = _toBuf(keyData, "importKey spki");
945
+ _preValidateSpki(spkiBuf, name);
932
946
  var pub = _nodeKey(function () { return nodeCrypto.createPublicKey({ key: spkiBuf, format: "der", type: "spki" }); }, "importKey spki");
933
947
  return new CryptoKey("public", true, _algFromImport(name, alg, pub), usages, pub);
934
948
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.7.44",
3
+ "version": "0.8.0",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:1e4a3531-f6e9-4a8b-940e-561dc73fa1ee",
5
+ "serialNumber": "urn:uuid:3474a203-558f-4bf4-8d04-ca4aec66e1f7",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-13T00:13:11.235Z",
8
+ "timestamp": "2026-09-19T19:43:19.653Z",
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.7.44",
22
+ "bom-ref": "@blamejs/pki@0.8.0",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.7.44",
25
+ "version": "0.8.0",
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.7.44",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.0",
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.7.44",
57
+ "ref": "@blamejs/pki@0.8.0",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]