@blamejs/pki 0.7.45 → 0.8.1

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,61 @@ 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.1 — 2026-09-19
8
+
9
+ CMS gains its last RFC 5652 content type, and the OID registry names every curve a certificate can declare.
10
+
11
+ ### Added
12
+
13
+ - `pki.cms.digest(content, opts)` builds an RFC 5652 DigestedData under SHA-256, SHA-384 or SHA-512, attached or detached, and returns DER or PEM. `pki.cms.verifyDigest(message, opts)` recomputes the digest and compares it in constant time, returning `{ valid, digestAlgorithm, content, contentType }` with `code` set to `cms/digest-mismatch` when the content does not match. The verdict carries no signer and no trusted field: a DigestedData establishes that content matches a digest and nothing about who produced either, so its worth is the worth of the channel the two traveled over. Use `pki.cms.sign` to establish origin, or `pki.cms.authenticate` to establish it under a shared key.
14
+ - `pki.schema.cms.parse` decodes a DigestedData ContentInfo instead of reporting it unparsed, and enforces the RFC 5652 section 7 version rule across fields: version 0 when the encapsulated content type is id-data, version 2 otherwise. A version that disagrees with the content type is `cms/bad-version`.
15
+ - The OID registry names thirty-three elliptic curves, covering RFC 5480 section 2.1.1.1, RFC 5639 section 4.1 and secp256k1. `pki.oid.name` and `pki.oid.byName` resolve each in both directions, under the name `node:crypto` knows the curve by.
16
+
17
+ ### Changed
18
+
19
+ - A `lint/cabf-tls/weak-key` finding raised against a non-approved elliptic curve names that curve in its context; the field was null for every curve outside the approved three. `pki.inspect.certificate` prints an `ASN1 OID:` line for those curves as well. Neither changes what the crypto engine accepts: importing a key on a curve the engine does not carry still fails with `webcrypto/not-supported`.
20
+
21
+ ### Fixed
22
+
23
+ - A digest AlgorithmIdentifier carrying parameters that are neither absent nor DER NULL is refused with `cms/unsupported-algorithm` on the two paths that read one without checking it: the `digestAlgorithm` of a DigestedData, and the `digestAlgorithm` beside the `macAlgorithm` of an AuthenticatedData, which `pki.cms.decrypt` reads to check the message-digest attribute. RFC 5754 section 2 allows only those two forms, so a parameter value that is neither does not name the algorithm the message claims; both paths would otherwise have reported the content verified or authenticated under it. The `macAlgorithm` beside it, and every SignerInfo digest, were already held to the rule.
24
+
25
+ ### Documentation
26
+
27
+ - `pki.path.validate` documents `opts.verifier`, which replaces the built-in signature check with `verifier.verify({ cert, workingPublicKey, workingPublicKeyAlgorithm, workingPublicKeyParameters })`. It is how a chain on a curve the engine does not carry, such as a brainpool one, is validated. It replaces the signature step alone: the RSA key-strength floor and every other RFC 5280 check still apply.
28
+ - `pki.hpke` is described as carrying the three ML-KEM suites alongside the four DHKEM suites, which is what shipped.
29
+ - The reference site at pkijs.com renders all 256 documented primitives; 46 of them reached no page. A page is identified by its URL, and a page built from two source files kept only one of them, so `pki.x509.sign`, the seven `pki.smime` verbs, `pki.tsp.sign` and every `pki.schema.*` parser were documented in the source and absent from the site. The symbol search and the API index resolve a primitive the same way the page does, so a search result and the section it links to agree.
30
+ - Roadmap rows that claimed capability the code does not have are corrected. RFC 6961 multi-stapling, RFC 6979 deterministic (EC)DSA and the RFC 5915 `ECPrivateKey` structure are not implemented. A distinguished name is emitted as an RFC 4514 string and is not read back from one. AES-CCM content encryption parses but is not produced. The OpenSSL interop statement names the seven algorithm arms it covers rather than claiming all of them. No registry rows are reserved for FN-DSA or HQC. The CMC rows record that RFC 5272, RFC 5273 and RFC 5274 were obsoleted by RFC 10002, RFC 10003 and RFC 10004 in July 2026, which the shipped surface predates. The CVE-2022-0778 row states that a named curve is required when a key is imported to verify with, not when a certificate is parsed, since parsing stays deliberately permissive so a non-conforming certificate can be inspected and linted.
31
+
32
+ ## v0.8.0 — 2026-09-19
33
+
34
+ Verification is held to the rules signing already applied.
35
+
36
+ ### Added
37
+
38
+ - `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.
39
+ - `pki.C.LIMITS.RSA_MIN_MODULUS_BITS` (2048) and `pki.path.validate` option `minRsaModulusBits`, which may only raise the floor.
40
+ - `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.
41
+
42
+ ### Changed
43
+
44
+ - `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.
45
+ - `pki.path.validate` refuses an RSA issuer key below 2048 bits with `path/weak-key`. Raise the floor with `minRsaModulusBits`; it cannot be lowered.
46
+ - 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.
47
+ - `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.
48
+ - `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.
49
+ - 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.
50
+
51
+ ### Fixed
52
+
53
+ - 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.
54
+ - `pki.cms.sign` and `pki.cms.countersign` over more than 16 MiB of buffered content threw an untyped `RangeError` instead of a `PkiError`.
55
+ - 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.
56
+ - 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.
57
+
58
+ ### Documentation
59
+
60
+ - 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.
61
+
7
62
  ## v0.7.45 — 2026-09-13
8
63
 
9
64
  A signature-protected CMP message fills an omitted sender with the signer's subject.
package/index.js CHANGED
@@ -99,6 +99,7 @@ module.exports = {
99
99
  // and exporting the module wholesale would put it on the public surface.
100
100
  cms: { verify: cms.verify, sign: cms.sign, countersign: cms.countersign, encrypt: cms.encrypt,
101
101
  authenticate: cms.authenticate, decrypt: cms.decrypt, compress: cms.compress, decompress: cms.decompress,
102
+ digest: cms.digest, verifyDigest: cms.verifyDigest,
102
103
  certsOnly: cms.certsOnly, parseCertsOnly: cms.parseCertsOnly, isCertsOnly: cms.isCertsOnly },
103
104
  smime: smime,
104
105
  // `cmc` interprets an RFC 5272 Full PKI Response into one terminal verdict;
@@ -160,7 +161,8 @@ module.exports = {
160
161
  shbs: shbs,
161
162
  // `hpke` is RFC 9180 Hybrid Public Key Encryption -- the KEM + HKDF key
162
163
  // schedule + AEAD context construction behind TLS ECH / MLS / OHTTP. Pure
163
- // composition over node:crypto; the classical DHKEM suites, all four modes.
164
+ // composition over node:crypto; the four DHKEM suites and the three ML-KEM
165
+ // suites, all four modes.
164
166
  hpke: hpke,
165
167
  // `kem` is composite ML-KEM key establishment (draft-ietf-lamps-pq-composite-kem):
166
168
  // pki.kem.encapsulate / decapsulate over a post-quantum ML-KEM hybridized with a
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/cmc-build.js CHANGED
@@ -199,8 +199,7 @@ async function _assertTcrPops(requests, popProvenRequest) {
199
199
  /** @internal The parameters an algorithm identifier carries, which for these four are absent or NULL. */
200
200
  function _assertNoAlgorithmParameters(algId, what) {
201
201
  var params = algId.parameters;
202
- if (params == null) return;
203
- if (params.length === 2 && params[0] === 0x05 && params[1] === 0x00) return;
202
+ if (guard.der.paramsAbsentOrNull(params)) return;
204
203
  throw E("cmc/bad-pop-challenge",
205
204
  "the challenge's " + what + " carries a parameter value, and the algorithms RFC 5274 sec. 4.2 " +
206
205
  "names take none: they are encoded with the parameters absent or NULL");
@@ -569,7 +569,7 @@ function _explicitSetOf(implicitBytes) {
569
569
  var MAC_HASH = intrinsic.assign(intrinsic.create(null), { hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" });
570
570
  var DIGEST_WC = intrinsic.assign(intrinsic.create(null), { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" });
571
571
  var MAC_KEY_MIN_OCTETS = 16;
572
- function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
572
+ function _isDerNull(p) { return guard.der.isNull(p); }
573
573
 
574
574
  async function _verifyAuthenticatedData(parsed, km, opts) {
575
575
  var macAlg = parsed.macAlgorithm;
@@ -583,6 +583,8 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
583
583
  if (parsed.authAttrsBytes) {
584
584
  var dHash = DIGEST_WC[parsed.digestAlgorithm && parsed.digestAlgorithm.name];
585
585
  if (!dHash) throw _err("cms/unsupported-algorithm", "unsupported digestAlgorithm " + JSON.stringify(parsed.digestAlgorithm && parsed.digestAlgorithm.name));
586
+ guard.der.assertParams(parsed.digestAlgorithm.parameters, "absentOrNull", _err,
587
+ "cms/unsupported-algorithm", parsed.digestAlgorithm.name + " digestAlgorithm (RFC 5754 sec. 2)");
586
588
  preimage = _explicitSetOf(parsed.authAttrsBytes);
587
589
  var mdAttr = (parsed.authAttrs || []).filter(function (a) { return a.type === O("messageDigest"); })[0];
588
590
  mdCheck = { hash: dHash, declared: asn1.read.octetString(asn1.decode(mdAttr.values[0])) };
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.cms
6
+ * @nav Messaging
7
+ * @title CMS DigestedData
8
+ * @order 95
9
+ * @slug cms-digest
10
+ *
11
+ * @intro
12
+ * RFC 5652 sec. 7 DigestedData, the integrity-only CMS content type. It carries the
13
+ * content, the digest algorithm, and the digest over the content octets. There is no
14
+ * signer and no key, so it establishes that content matches a digest and nothing about
15
+ * who produced either: its worth is the worth of the channel the message and the digest
16
+ * traveled over. `pki.cms.sign` establishes origin, `pki.cms.authenticate` establishes
17
+ * origin under a shared key.
18
+ *
19
+ * The version follows the encapsulated type, 0 for `id-data` and 2 for anything else.
20
+ * The verifying half recomputes the digest and compares it in constant time, and its
21
+ * verdict carries no signer and no trust field, so it cannot be read as a signature.
22
+ *
23
+ * @spec RFC 5652 sec. 7
24
+ * @card
25
+ * Build and verify RFC 5652 DigestedData: SHA-256/384/512 over the content octets,
26
+ * attached or detached, constant-time comparison, fail-closed.
27
+ */
28
+
29
+ var nodeCrypto = require("node:crypto");
30
+ var oid = require("./oid");
31
+ var schemaCms = require("./schema-cms");
32
+ var frameworkError = require("./framework-error");
33
+ var guard = require("./guard-all");
34
+ var intrinsic = require("./guard-intrinsic");
35
+ var asn1 = require("./asn1-der");
36
+ var b = asn1.build;
37
+
38
+ var CmsError = frameworkError.CmsError;
39
+ function _err(code, message, cause) { return new CmsError(code, message, cause); }
40
+ function O(name) { return oid.byName(name); }
41
+
42
+ var OID_DATA = O("data");
43
+ var OID_DIGESTED_DATA = O("digestedData");
44
+
45
+ var NODE_DIGEST = intrinsic.assign(intrinsic.create(null), {
46
+ sha256: "sha256", sha384: "sha384", sha512: "sha512",
47
+ });
48
+
49
+ var _DIGEST_OPTS = intrinsic.assign(intrinsic.create(null), {
50
+ digestAlgorithm: 1, contentType: 1, detached: 1, pem: 1,
51
+ });
52
+ var _VERIFY_DIGEST_OPTS = intrinsic.assign(intrinsic.create(null), { content: 1 });
53
+
54
+ function _digestName(opts) {
55
+ var name = opts.digestAlgorithm === undefined ? "sha256" : opts.digestAlgorithm;
56
+ var key = guard.text.keyOf(name);
57
+ if (!NODE_DIGEST[key]) {
58
+ throw _err("cms/unsupported-algorithm", "unsupported digest algorithm " + guard.text.showValue(name) +
59
+ " (sha256 / sha384 / sha512)");
60
+ }
61
+ return key;
62
+ }
63
+
64
+ /**
65
+ * @primitive pki.cms.digest
66
+ * @signature pki.cms.digest(content, opts?) -> Promise<Buffer|string>
67
+ * @since 0.8.1
68
+ * @status stable
69
+ * @spec RFC 5652 sec. 7
70
+ * @related pki.cms.verifyDigest, pki.cms.sign, pki.cms.authenticate
71
+ *
72
+ * Build an RFC 5652 `DigestedData`: the content, the digest algorithm, and the digest over
73
+ * the content octets. It carries no signer and no key, so it establishes that content
74
+ * matches a digest and nothing about who produced either. Use `pki.cms.sign` for origin,
75
+ * or `pki.cms.authenticate` for origin under a shared key.
76
+ *
77
+ * The version follows the encapsulated type, 0 for `id-data` and 2 for anything else.
78
+ *
79
+ * @opts
80
+ * - `digestAlgorithm` (string) -- `sha256` (default), `sha384` or `sha512`.
81
+ * - `contentType` (string) -- the encapsulated content type by OID name, default `data`.
82
+ * - `detached` (boolean) -- omit the content, leaving the digest to travel alone.
83
+ * - `pem` (boolean) -- return a PEM `CMS` block instead of DER.
84
+ * @example
85
+ * var der = await pki.cms.digest(Buffer.from("hello"));
86
+ * (await pki.cms.verifyDigest(der)).valid; // -> true
87
+ */
88
+ function digest(content, opts) {
89
+ return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
90
+ [content, "content"], [opts, "pki.cms.digest options"],
91
+ ], _digest);
92
+ }
93
+
94
+ async function _digest(content, opts) {
95
+ opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.digest options");
96
+ guard.identifier.assertKnownKeys(opts, _DIGEST_OPTS, _err, "cms/bad-input",
97
+ "pki.cms.digest has an unknown option ");
98
+ var raw = guard.bytes.source(content, CmsError, "cms/bad-input", "content");
99
+ var alg = _digestName(opts);
100
+ var ctName = opts.contentType === undefined ? "data" : opts.contentType;
101
+ var ctOid = O(ctName);
102
+ if (!ctOid) throw _err("cms/bad-input", "opts.contentType is not a known OID name: " + guard.text.showValue(ctName));
103
+
104
+ var value = nodeCrypto.createHash(NODE_DIGEST[alg]).update(raw).digest();
105
+ var eciChildren = opts.detached === true
106
+ ? [b.oid(ctOid)]
107
+ : [b.oid(ctOid), b.explicit(0, b.octetString(raw))];
108
+ var dd = b.sequence([
109
+ b.integer(ctOid === OID_DATA ? 0 : 2),
110
+ b.sequence([b.oid(O(alg))]),
111
+ b.sequence(eciChildren),
112
+ b.octetString(value),
113
+ ]);
114
+ var ci = b.sequence([b.oid(OID_DIGESTED_DATA), b.explicit(0, dd)]);
115
+ return opts.pem === true ? schemaCms.pemEncode(ci, "CMS") : ci;
116
+ }
117
+
118
+ /**
119
+ * @primitive pki.cms.verifyDigest
120
+ * @signature pki.cms.verifyDigest(input, opts?) -> Promise<verdict>
121
+ * @since 0.8.1
122
+ * @status stable
123
+ * @spec RFC 5652 sec. 7
124
+ * @related pki.cms.digest, pki.cms.verify
125
+ *
126
+ * Recompute the digest of a `DigestedData`'s content and compare it with the digest the
127
+ * message carries, in constant time. Returns `{ valid, digestAlgorithm, content, contentType }`,
128
+ * with `code` set to `cms/digest-mismatch` when the content does not match.
129
+ *
130
+ * A DigestedData proves neither origin nor authority. A caller that needs either verifies a
131
+ * `SignedData` instead; this verdict carries no signer and no trust field, so it cannot be
132
+ * mistaken for one.
133
+ *
134
+ * @opts
135
+ * - `content` (BufferSource) -- the detached content the digest covers. Required when the
136
+ * message omits its own content, refused as redundant when it carries it.
137
+ * @example
138
+ * var der = await pki.cms.digest(Buffer.from("hello"));
139
+ * var v = await pki.cms.verifyDigest(der);
140
+ * v.valid; // -> true
141
+ * v.digestAlgorithm; // -> "sha256"
142
+ */
143
+ function verifyDigest(input, opts) {
144
+ return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
145
+ [input, "the CMS message"], [opts, "pki.cms.verifyDigest options"],
146
+ ], _verifyDigest);
147
+ }
148
+
149
+ async function _verifyDigest(input, opts) {
150
+ opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.verifyDigest options");
151
+ guard.identifier.assertKnownKeys(opts, _VERIFY_DIGEST_OPTS, _err, "cms/bad-input",
152
+ "pki.cms.verifyDigest has an unknown option ");
153
+ var parsed = guard.parsed.acceptDerived(input, "cms", function (bytes) {
154
+ return schemaCms.parse(bytes);
155
+ }, _err, "cms/bad-input", "the DigestedData");
156
+ if (parsed.contentType !== OID_DIGESTED_DATA) {
157
+ throw _err("cms/not-a-digested-data", "pki.cms.verifyDigest reads a DigestedData (RFC 5652 sec. 7) and this is " +
158
+ (oid.name(parsed.contentType) || parsed.contentType));
159
+ }
160
+ var alg = guard.text.keyOf(parsed.digestAlgorithm.name || "");
161
+ if (!NODE_DIGEST[alg]) {
162
+ throw _err("cms/unsupported-algorithm", "unsupported digest algorithm " +
163
+ guard.text.showValue(parsed.digestAlgorithm.name || parsed.digestAlgorithm.oid));
164
+ }
165
+ guard.der.assertParams(parsed.digestAlgorithm.parameters, "absentOrNull", _err,
166
+ "cms/unsupported-algorithm", parsed.digestAlgorithm.name + " digest algorithm (RFC 5754 sec. 2)");
167
+ var embedded = parsed.encapContentInfo.eContent;
168
+ var supplied = opts.content === undefined ? null
169
+ : guard.bytes.snapshotSource(opts.content, CmsError, "cms/bad-input", "opts.content");
170
+ if (embedded != null && supplied != null) {
171
+ throw _err("cms/bad-input", "the DigestedData carries its own content, so opts.content is redundant");
172
+ }
173
+ if (embedded == null && supplied == null) {
174
+ throw _err("cms/bad-input", "the DigestedData is detached, so opts.content must supply the content it covers");
175
+ }
176
+ var covered = embedded != null
177
+ ? guard.bytes.snapshotSource(embedded, CmsError, "cms/bad-input", "the encapsulated content")
178
+ : supplied;
179
+ var recomputed = nodeCrypto.createHash(NODE_DIGEST[alg]).update(covered).digest();
180
+ var matched = guard.crypto.constantTimeEqual(recomputed, parsed.digest);
181
+ return guard.verdict.of({
182
+ valid: matched,
183
+ code: matched ? undefined : "cms/digest-mismatch",
184
+ digestAlgorithm: alg,
185
+ contentType: parsed.encapContentInfo.eContentType,
186
+ contentTypeName: oid.name(parsed.encapContentInfo.eContentType) || null,
187
+ content: covered,
188
+ });
189
+ }
190
+
191
+ module.exports = { digest: digest, verifyDigest: verifyDigest };
package/lib/cms-verify.js CHANGED
@@ -38,6 +38,7 @@ var cmsSign = require("./cms-sign");
38
38
  var cmsEncrypt = require("./cms-encrypt");
39
39
  var cmsDecrypt = require("./cms-decrypt");
40
40
  var cmsCompress = require("./cms-compress");
41
+ var cmsDigest = require("./cms-digest");
41
42
  var signScheme = require("./sign-scheme");
42
43
  var _mldsaDigestSuitable = signScheme.mldsaDigestSuitable;
43
44
  var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID;
@@ -69,6 +70,17 @@ var DIGEST_HASH = intrinsic.assign(intrinsic.create(null), {
69
70
  shake128: "SHAKE128", shake256: "SHAKE256",
70
71
  });
71
72
  var SIG_HASH = intrinsic.assign(intrinsic.create(null), { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" });
73
+ function _digestOidTable(named) {
74
+ var t = intrinsic.create(null);
75
+ intrinsic.forEach(named, function (pair) {
76
+ var dotted = oid.byName(pair[0]);
77
+ if (dotted) t[dotted] = pair[1];
78
+ });
79
+ return t;
80
+ }
81
+ var WEAK_DIGEST_OIDS = _digestOidTable([["sha1", "SHA-1"]]);
82
+ var UNVERIFIABLE_DIGEST_OIDS = _digestOidTable([["md5", "MD5"], ["md4", "MD4"], ["md2", "MD2"]]);
83
+ var WEAK_SIGNATURE_HASHES = intrinsic.assign(intrinsic.create(null), { "SHA-1": 1 });
72
84
  function _supportedDigest(name) { return intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
73
85
  async function _computeDigest(name, content) {
74
86
  return intrinsic.bufferFrom(await subtle.digest(DIGEST_HASH[name], content));
@@ -94,7 +106,7 @@ var SIG_SCHEME = intrinsic.assign(intrinsic.create(null), {
94
106
  ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
95
107
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
96
108
  ].forEach(function (s) { SIG_SCHEME["id-slh-dsa-" + s] = { kind: "slhdsa", name: "SLH-DSA-" + s.toUpperCase(), params: "absent", sameKeyOid: true, digest: SLHDSA_BY_OID[oid.byName("id-slh-dsa-" + s)].digest }; });
97
- function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
109
+ function _isDerNull(p) { return guard.der.isNull(p); }
98
110
  function _algParamsOk(shape, p) { return shape === "null" ? _isDerNull(p) : (p === null || p === undefined); }
99
111
  var EC_CURVE = intrinsic.create(null);
100
112
  EC_CURVE[oid.byName("prime256v1")] = { curve: "P-256", coordLen: 32 };
@@ -247,11 +259,21 @@ function _decodeSignedAttrs(setOfBytes) {
247
259
  });
248
260
  }
249
261
 
250
- async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, digestByName) {
262
+ async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, digestByName, allowWeakDigests) {
251
263
  var composite = compositeSig.COMPOSITE_ALGS[si.signatureAlgorithm.oid];
252
264
  if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts, csTarget, digestByName);
253
265
  var scheme = SIG_SCHEME[si.signatureAlgorithm.name];
254
266
  if (!scheme) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported signature algorithm " + JSON.stringify(si.signatureAlgorithm.name) });
267
+ var unverifiableDigest = UNVERIFIABLE_DIGEST_OIDS[si.digestAlgorithm.oid];
268
+ if (unverifiableDigest) {
269
+ return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid,
270
+ message: "the " + unverifiableDigest + " message digest is broken and this toolkit does not verify a signature under it" });
271
+ }
272
+ var weakDigest = WEAK_DIGEST_OIDS[si.digestAlgorithm.oid];
273
+ if (!allowWeakDigests && weakDigest) {
274
+ return _pResolve({ ok: false, code: "cms/weak-digest", sid: si.sid,
275
+ message: "the " + weakDigest + " message digest is not collision resistant; pass allowWeakDigests to verify an archived signature under it" });
276
+ }
255
277
  var digestHash = SIG_HASH[si.digestAlgorithm.name];
256
278
  var pss = scheme.kind === "rsapss" ? _resolvePss(si.signatureAlgorithm.parameters) : null;
257
279
  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 +283,10 @@ async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, dige
261
283
  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
284
  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
285
  var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
286
+ if (!allowWeakDigests && WEAK_SIGNATURE_HASHES[sigHash]) {
287
+ return _pResolve({ ok: false, code: "cms/weak-digest", sid: si.sid,
288
+ message: "the signature algorithm selects the " + sigHash + " digest, which is not collision resistant; pass allowWeakDigests to verify an archived signature under it" });
289
+ }
264
290
  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
291
  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
292
  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 +412,7 @@ function _surfaceSignedAttrs(si) {
386
412
  });
387
413
  }
388
414
 
389
- async function _verifyCountersignatures(si, parsedCerts) {
415
+ async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
390
416
  var targetSig = _toBuf(si.signature, "the countersigned signature");
391
417
  var values = [];
392
418
  intrinsic.forEach(si.unsignedAttrs || [], function (a) {
@@ -394,16 +420,16 @@ async function _verifyCountersignatures(si, parsedCerts) {
394
420
  intrinsic.forEach(a.values, function (v) { guard.list.append(values, v && v.bytes ? v.bytes : v); });
395
421
  });
396
422
  var out = [];
397
- for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts));
423
+ for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts, allowWeakDigests));
398
424
  return out;
399
425
  }
400
426
 
401
- async function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
427
+ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests) {
402
428
  var csSi;
403
429
  try { csSi = cms.walkCountersignature(asn1.decode(intrinsic.isBuffer(vDer) ? vDer : intrinsic.bufferFrom(vDer))); }
404
430
  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);
431
+ var verdict = await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig, undefined, allowWeakDigests);
432
+ var nested = await _verifyCountersignatures(csSi, parsedCerts, allowWeakDigests);
407
433
  var head = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name };
408
434
  return guard.verdict.of(
409
435
  verdict.code ? guard.verdict.of(head, { code: verdict.code }) : head,
@@ -478,7 +504,7 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
478
504
  * var cert = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
479
505
  * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
480
506
  * var detachedBytes = Buffer.from("hello");
481
- * var p7sDer = await pki.cms.sign(detachedBytes, { cert: cert, key: key, detached: true });
507
+ * var p7sDer = await pki.cms.sign(detachedBytes, { cert: cert, key: key }, { detached: true });
482
508
  * var res = await pki.cms.verify(p7sDer, { content: detachedBytes });
483
509
  * res.valid; // boolean
484
510
  * res.signers[0].ok; // per-signer verdict
@@ -509,7 +535,7 @@ async function _resolveContentDigests(parsed, streamSource) {
509
535
  return byName;
510
536
  }
511
537
 
512
- var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1 });
538
+ var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, allowWeakDigests: 1 });
513
539
 
514
540
  function verify(input, opts) {
515
541
  return guard.async.deferred(function () { return _verify(input, opts); });
@@ -523,6 +549,7 @@ async function _verify(input, opts) {
523
549
  }, _err, "cms/bad-input", "the SignedData");
524
550
  if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
525
551
  var trustCfg = _snapshotTrust(opts);
552
+ var allowWeakDigests = opts.allowWeakDigests === true;
526
553
  var parsedCerts = [];
527
554
  (parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
528
555
  (opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
@@ -544,8 +571,8 @@ async function _verify(input, opts) {
544
571
  var signers = [];
545
572
  for (var _sp = 0; _sp < parsed.signerInfos.length; _sp++) {
546
573
  var siOne = parsed.signerInfos[_sp];
547
- var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName);
548
- var counters = await _verifyCountersignatures(siOne, parsedCerts);
574
+ var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName, allowWeakDigests);
575
+ var counters = await _verifyCountersignatures(siOne, parsedCerts, allowWeakDigests);
549
576
  guard.list.append(signers, guard.verdict.of(verdict, {
550
577
  countersignatures: counters,
551
578
  unsignedAttrs: _surfaceUnsignedAttrs(siOne),
@@ -1094,4 +1121,4 @@ function isCertsOnly(input) {
1094
1121
  }
1095
1122
 
1096
1123
  // @internal
1097
- module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress, certsOnly: certsOnly, parseCertsOnly: parseCertsOnly, isCertsOnly: isCertsOnly, setEngine: setEngine };
1124
+ module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress, digest: cmsDigest.digest, verifyDigest: cmsDigest.verifyDigest, certsOnly: certsOnly, parseCertsOnly: parseCertsOnly, isCertsOnly: isCertsOnly, setEngine: setEngine };
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
package/lib/guard-der.js CHANGED
@@ -7,6 +7,7 @@ var _freeze = require("./guard-intrinsic").freeze;
7
7
  var _forEach = require("./guard-intrinsic").forEach;
8
8
  var _sizeOf = require("./guard-intrinsic").sizeOf;
9
9
  var _create = require("./guard-intrinsic").create;
10
+ var _isBuffer = require("./guard-intrinsic").isBuffer;
10
11
 
11
12
  var asn1 = null;
12
13
  function _asn1() { if (asn1 === null) asn1 = require("./asn1-der"); return asn1; }
@@ -84,4 +85,28 @@ function tlv(content, E, code, label) {
84
85
  return content;
85
86
  }
86
87
 
87
- module.exports = _freeze({ element: element, tlv: tlv });
88
+ // @enforced-by guard-shape-reinlined
89
+ // @guard-shape \[0\] === 0x05
90
+ function isNull(params) {
91
+ return _isBuffer(params) && _sizeOf(params) === 2 && params[0] === 0x05 && params[1] === 0x00;
92
+ }
93
+
94
+ // @enforced-by behavioral -- absence has no rename-proof shape; the RED vector per verb is the guard
95
+ function paramsAbsentOrNull(params) {
96
+ return params === null || params === undefined || isNull(params);
97
+ }
98
+
99
+ // @enforced-by behavioral -- see paramsAbsentOrNull.
100
+ function assertParams(params, shape, E, code, label) {
101
+ var ok = shape === "null" ? isNull(params) : (shape === "absent"
102
+ ? (params === null || params === undefined)
103
+ : paramsAbsentOrNull(params));
104
+ if (ok) return params;
105
+ throw E(code, "the " + label + " parameters must be " + (shape === "null"
106
+ ? "DER NULL" : (shape === "absent" ? "absent" : "absent or DER NULL")));
107
+ }
108
+
109
+ module.exports = _freeze({
110
+ element: element, tlv: tlv,
111
+ isNull: isNull, paramsAbsentOrNull: paramsAbsentOrNull, assertParams: assertParams,
112
+ });
@@ -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
 
@@ -264,12 +265,19 @@ var FAMILIES = Object.assign(Object.create(null), {
264
265
  asymmDecryptKeyID: 54, certificationRequestInfoTemplate: 61, extensionReqTemplate: 62 } },
265
266
 
266
267
  ansiX962: { base: [1, 2, 840, 10045], of: {
267
- ecPublicKey: [2, 1], prime256v1: [3, 1, 7],
268
+ ecPublicKey: [2, 1], prime192v1: [3, 1, 1], prime256v1: [3, 1, 7],
268
269
  ecdsaWithSHA256: [4, 3, 2], ecdsaWithSHA384: [4, 3, 3], ecdsaWithSHA512: [4, 3, 4] } },
269
270
 
270
- secg: { base: [1, 3, 132, 0], of: { secp384r1: 34, secp521r1: 35 } },
271
+ secg: { base: [1, 3, 132, 0], of: {
272
+ sect163k1: 1, sect163r1: 2, secp256k1: 10, sect163r2: 15, sect283k1: 16, sect283r1: 17,
273
+ sect233k1: 26, sect233r1: 27, secp192k1: 31, secp224k1: 32, secp224r1: 33,
274
+ secp384r1: 34, secp521r1: 35, sect409k1: 36, sect409r1: 37, sect571k1: 38, sect571r1: 39 } },
271
275
 
272
- brainpool: { base: [1, 3, 36, 3, 3, 2, 8, 1, 1], of: { brainpoolP256r1: 7, brainpoolP384r1: 11 } },
276
+ brainpool: { base: [1, 3, 36, 3, 3, 2, 8, 1, 1], of: {
277
+ brainpoolP160r1: 1, brainpoolP160t1: 2, brainpoolP192r1: 3, brainpoolP192t1: 4,
278
+ brainpoolP224r1: 5, brainpoolP224t1: 6, brainpoolP256r1: 7, brainpoolP256t1: 8,
279
+ brainpoolP320r1: 9, brainpoolP320t1: 10, brainpoolP384r1: 11, brainpoolP384t1: 12,
280
+ brainpoolP512r1: 13, brainpoolP512t1: 14 } },
273
281
 
274
282
  edwards: { base: [1, 3, 101], of: { X25519: 110, X448: 111, Ed25519: 112, Ed448: 113 } },
275
283
 
@@ -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";
@@ -216,7 +220,7 @@ function resolveRsaPss(paramsBytes) {
216
220
  return { verify: { name: "RSA-PSS", saltLength: saltLength }, imp: { name: "RSA-PSS", hash: hash } };
217
221
  }
218
222
 
219
- function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
223
+ function isDerNull(p) { return guard.der.isNull(p); }
220
224
 
221
225
  function resolveDescriptor(sigAlg) {
222
226
  if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
@@ -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,
@@ -779,7 +827,7 @@ function addChild(state, parent, validPolicy, qualifiers, expectedPolicySet, che
779
827
  }
780
828
 
781
829
  function isNullOrAbsentParams(p) {
782
- return p === null || p === undefined || (p.length === 2 && p[0] === 0x05 && p[1] === 0x00);
830
+ return guard.der.paramsAbsentOrNull(p);
783
831
  }
784
832
 
785
833
  function spliceSpkiParameters(spki, algOid, paramsBytes) {
@@ -969,8 +1017,13 @@ function validateCriticalExtensionStructure(cert) {
969
1017
  * optional `requiredEku` (key purposes the target's extendedKeyUsage must
970
1018
  * assert, given as registered OID names or dotted OID strings; an absent
971
1019
  * extension is unrestricted, RFC 5280 4.2.1.12), and an optional
972
- * `revocationChecker`. The value-carrying options (`time`, `maxPathCerts`,
973
- * `maxPolicyNodes`, the subtree seeds, `userInitialPolicySet`, `requiredEku`)
1020
+ * `revocationChecker`. An optional `verifier` replaces the built-in signature
1021
+ * check with `verifier.verify({ cert, workingPublicKey, workingPublicKeyAlgorithm,
1022
+ * workingPublicKeyParameters })`, which is how a chain on a curve the engine does
1023
+ * not carry, such as a brainpool one, is validated. It replaces the signature step
1024
+ * only: the key-strength floor and every other RFC 5280 check still apply.
1025
+ * The value-carrying options (`time`, `maxPathCerts`,
1026
+ * `maxPolicyNodes`, `minRsaModulusBits`, the subtree seeds, `userInitialPolicySet`, `requiredEku`)
974
1027
  * are validated at the entry point. A mis-shaped value throws `path/bad-input`,
975
1028
  * so it cannot silently go unapplied. Returns `{ valid, revocationChecked, anchorConstraints,
976
1029
  * path, results, workingPublicKey, workingPublicKeyAlgorithm,
@@ -1054,7 +1107,7 @@ function validateCriticalExtensionStructure(cert) {
1054
1107
  var _VALIDATE_OPTS = intrinsic.assign(intrinsic.create(null), {
1055
1108
  checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
1056
1109
  initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
1057
- initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, requireRevocation: 1,
1110
+ initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, minRsaModulusBits: 1, requireRevocation: 1,
1058
1111
  requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchors: 1,
1059
1112
  userInitialPolicySet: 1, verifier: 1
1060
1113
  });
@@ -1100,6 +1153,14 @@ async function validate(path, opts) {
1100
1153
  if (!intrinsic.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
1101
1154
  var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
1102
1155
  if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
1156
+ if (opts.minRsaModulusBits != null) {
1157
+ if (typeof opts.minRsaModulusBits !== "number" || !intrinsic.isInteger(opts.minRsaModulusBits)) {
1158
+ throw E("path/bad-input", "validate: opts.minRsaModulusBits must be an integer");
1159
+ }
1160
+ if (opts.minRsaModulusBits < constants.LIMITS.RSA_MIN_MODULUS_BITS) {
1161
+ throw E("path/bad-input", "validate: opts.minRsaModulusBits may only raise the " + constants.LIMITS.RSA_MIN_MODULUS_BITS + "-bit minimum, not lower it");
1162
+ }
1163
+ }
1103
1164
  var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
1104
1165
  var n = certs.length;
1105
1166
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
@@ -1178,7 +1239,10 @@ async function validate(path, opts) {
1178
1239
  var checks = [];
1179
1240
 
1180
1241
  var sigRes;
1181
- if (verifier) {
1242
+ var weak = _weakIssuerKey(state);
1243
+ if (weak) {
1244
+ sigRes = { ok: false, code: "path/weak-key", error: weak };
1245
+ } else if (verifier) {
1182
1246
  var vv;
1183
1247
  try {
1184
1248
  vv = await verifier.verify({
package/lib/schema-cms.js CHANGED
@@ -100,8 +100,8 @@ var AEAD_ALGS = Object.create(null);
100
100
 
101
101
  var DEFERRED = new Set([
102
102
  oid.byName("data"), oid.byName("signedAndEnvelopedData"),
103
- oid.byName("digestedData"),
104
103
  ]);
104
+ var OID_DIGESTED_DATA = oid.byName("digestedData");
105
105
  var OID_CONTENT_TYPE = oid.byName("contentType");
106
106
  var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
107
107
  var OID_SIGNING_TIME = oid.byName("signingTime");
@@ -878,6 +878,32 @@ var COMPRESSED_DATA = schema.seq([
878
878
  },
879
879
  });
880
880
 
881
+ var DIGESTED_DATA_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2 });
882
+
883
+ var DIGESTED_DATA = schema.seq([
884
+ schema.field("version", DIGESTED_DATA_VERSION),
885
+ schema.field("digestAlgorithm", ALGORITHM_IDENTIFIER),
886
+ schema.field("encapContentInfo", ENCAP_CONTENT_INFO),
887
+ schema.field("digest", schema.octetString()),
888
+ ], {
889
+ assert: "sequence", arity: { exact: 4 }, code: "cms/bad-digested-data", what: "DigestedData",
890
+ build: function (m, ctx) {
891
+ var version = m.fields.version.value;
892
+ var eci = m.fields.encapContentInfo.value.result;
893
+ var expected = eci.eContentType === OID_DATA ? 0 : 2;
894
+ if (version !== expected) {
895
+ throw ctx.E("cms/bad-version", "a DigestedData over " + (ctx.oid.name(eci.eContentType) || eci.eContentType) +
896
+ " is version " + expected + " (RFC 5652 sec. 7)");
897
+ }
898
+ return {
899
+ version: version,
900
+ digestAlgorithm: m.fields.digestAlgorithm.value.result,
901
+ encapContentInfo: eci,
902
+ digest: m.fields.digest.value,
903
+ };
904
+ },
905
+ });
906
+
881
907
  var CONTENT_INFO = schema.seq([
882
908
  schema.field("contentType", schema.oidLeaf()),
883
909
  schema.field("content", schema.explicit(0, schema.any(), { code: "cms/not-a-content-info" })),
@@ -892,6 +918,7 @@ var CONTENT_INFO = schema.seq([
892
918
  else if (ct === OID_AUTH_DATA) inner = AUTHENTICATED_DATA;
893
919
  else if (ct === OID_AUTH_ENVELOPED_DATA) inner = AUTH_ENVELOPED_DATA;
894
920
  else if (ct === OID_COMPRESSED_DATA) inner = COMPRESSED_DATA;
921
+ else if (ct === OID_DIGESTED_DATA) inner = DIGESTED_DATA;
895
922
  if (inner === null) {
896
923
  if (DEFERRED.has(ct)) {
897
924
  throw NS.E("cms/unsupported-content-type", (ctx.oid.name(ct) || ct) + " is recognized but not parsed by this build");
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
  },
@@ -280,10 +280,10 @@ function pbkdf2Params(ns) {
280
280
  var keyLength = guard.range.positiveInt31(m.fields.keyLength.value, ctx.E, ctx.prefix + "/bad-mac-data", "PBKDF2 keyLength");
281
281
  var prf = m.fields.prf.present ? m.fields.prf.value.result : null;
282
282
  var pp = prf ? prf.parameters : null;
283
- if (prf && prf.oid === hmacSha1 && pp !== null && pp.length === 2 && pp[0] === 0x05 && pp[1] === 0x00) {
283
+ if (prf && prf.oid === hmacSha1 && pp !== null && guard.der.isNull(pp)) {
284
284
  throw ctx.E(ctx.prefix + "/bad-mac-data", "a PBKDF2 prf equal to its DEFAULT algid-hmacWithSHA1 must be omitted (X.690 sec. 11.5, RFC 8018 sec. 5.2)");
285
285
  }
286
- if (prf && pp !== null && !(pp.length === 2 && pp[0] === 0x05 && pp[1] === 0x00)) {
286
+ if (prf && pp !== null && !guard.der.isNull(pp)) {
287
287
  throw ctx.E(ctx.prefix + "/bad-mac-data", "the PBKDF2 prf parameters must be absent or NULL (RFC 8018 App. B.1)");
288
288
  }
289
289
  return {
@@ -313,7 +313,7 @@ function pbmac1Params(ns) {
313
313
  }
314
314
  var scheme = m.fields.messageAuthScheme.value.result;
315
315
  var sp = scheme.parameters;
316
- if (sp !== null && !(sp.length === 2 && sp[0] === 0x05 && sp[1] === 0x00)) {
316
+ if (sp !== null && !guard.der.isNull(sp)) {
317
317
  throw ctx.E(ctx.prefix + "/bad-mac-data", "the PBMAC1 messageAuthScheme parameters must be absent or NULL (RFC 8018 App. B.1)");
318
318
  }
319
319
  return {
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.45",
3
+ "version": "0.8.1",
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:c95b91b1-2249-469b-b641-900d56a9cf3d",
5
+ "serialNumber": "urn:uuid:e0552083-e17a-4a74-b1fc-ea82fcb57a1e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-13T00:41:14.691Z",
8
+ "timestamp": "2026-09-19T23:58:45.411Z",
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.45",
22
+ "bom-ref": "@blamejs/pki@0.8.1",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.7.45",
25
+ "version": "0.8.1",
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.45",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.1",
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.45",
57
+ "ref": "@blamejs/pki@0.8.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]