@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 +38 -0
- package/README.md +1 -1
- package/lib/asn1-der.js +5 -1
- package/lib/cmp-build.js +23 -0
- package/lib/cms-verify.js +36 -10
- package/lib/constants.js +3 -0
- package/lib/framework-error.js +17 -5
- package/lib/guard-bytes.js +4 -1
- package/lib/guard-identifier.js +5 -1
- package/lib/lint.js +12 -6
- package/lib/oid.js +1 -0
- package/lib/path-validate.js +62 -3
- package/lib/schema-crl.js +12 -6
- package/lib/smime.js +2 -1
- package/lib/tsp-sign.js +2 -2
- package/lib/webcrypto.js +14 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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),
|
package/lib/framework-error.js
CHANGED
|
@@ -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.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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 =
|
|
94
|
+
this.permanent = !_TRANSIENT_REASONS[_reasonOf(c)];
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
package/lib/guard-bytes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/lib/guard-identifier.js
CHANGED
|
@@ -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: "
|
|
372
|
-
message: "a CA certificate
|
|
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: "
|
|
378
|
-
message: "a non-self-issued certificate
|
|
379
|
-
appliesTo: function (cert) { return !!(cert.issuer && cert.subject && cert.issuer
|
|
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: "
|
|
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
|
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:3474a203-558f-4bf4-8d04-ca4aec66e1f7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.0",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.0",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|