@blamejs/pki 0.8.0 → 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,31 @@ 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
+
7
32
  ## v0.8.0 — 2026-09-19
8
33
 
9
34
  Verification is held to the rules signing already applied.
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/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;
@@ -105,7 +106,7 @@ var SIG_SCHEME = intrinsic.assign(intrinsic.create(null), {
105
106
  ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
106
107
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
107
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 }; });
108
- 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); }
109
110
  function _algParamsOk(shape, p) { return shape === "null" ? _isDerNull(p) : (p === null || p === undefined); }
110
111
  var EC_CURVE = intrinsic.create(null);
111
112
  EC_CURVE[oid.byName("prime256v1")] = { curve: "P-256", coordLen: 32 };
@@ -1120,4 +1121,4 @@ function isCertsOnly(input) {
1120
1121
  }
1121
1122
 
1122
1123
  // @internal
1123
- 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/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
+ });
package/lib/oid.js CHANGED
@@ -265,12 +265,19 @@ var FAMILIES = Object.assign(Object.create(null), {
265
265
  asymmDecryptKeyID: 54, certificationRequestInfoTemplate: 61, extensionReqTemplate: 62 } },
266
266
 
267
267
  ansiX962: { base: [1, 2, 840, 10045], of: {
268
- ecPublicKey: [2, 1], prime256v1: [3, 1, 7],
268
+ ecPublicKey: [2, 1], prime192v1: [3, 1, 1], prime256v1: [3, 1, 7],
269
269
  ecdsaWithSHA256: [4, 3, 2], ecdsaWithSHA384: [4, 3, 3], ecdsaWithSHA512: [4, 3, 4] } },
270
270
 
271
- 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 } },
272
275
 
273
- 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 } },
274
281
 
275
282
  edwards: { base: [1, 3, 101], of: { X25519: 110, X448: 111, Ed25519: 112, Ed448: 113 } },
276
283
 
@@ -220,7 +220,7 @@ function resolveRsaPss(paramsBytes) {
220
220
  return { verify: { name: "RSA-PSS", saltLength: saltLength }, imp: { name: "RSA-PSS", hash: hash } };
221
221
  }
222
222
 
223
- function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
223
+ function isDerNull(p) { return guard.der.isNull(p); }
224
224
 
225
225
  function resolveDescriptor(sigAlg) {
226
226
  if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
@@ -827,7 +827,7 @@ function addChild(state, parent, validPolicy, qualifiers, expectedPolicySet, che
827
827
  }
828
828
 
829
829
  function isNullOrAbsentParams(p) {
830
- return p === null || p === undefined || (p.length === 2 && p[0] === 0x05 && p[1] === 0x00);
830
+ return guard.der.paramsAbsentOrNull(p);
831
831
  }
832
832
 
833
833
  function spliceSpkiParameters(spki, algOid, paramsBytes) {
@@ -1017,8 +1017,13 @@ function validateCriticalExtensionStructure(cert) {
1017
1017
  * optional `requiredEku` (key purposes the target's extendedKeyUsage must
1018
1018
  * assert, given as registered OID names or dotted OID strings; an absent
1019
1019
  * extension is unrestricted, RFC 5280 4.2.1.12), and an optional
1020
- * `revocationChecker`. The value-carrying options (`time`, `maxPathCerts`,
1021
- * `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`)
1022
1027
  * are validated at the entry point. A mis-shaped value throws `path/bad-input`,
1023
1028
  * so it cannot silently go unapplied. Returns `{ valid, revocationChecked, anchorConstraints,
1024
1029
  * path, results, workingPublicKey, workingPublicKeyAlgorithm,
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");
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.0",
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:3474a203-558f-4bf4-8d04-ca4aec66e1f7",
5
+ "serialNumber": "urn:uuid:e0552083-e17a-4a74-b1fc-ea82fcb57a1e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-19T19:43:19.653Z",
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.8.0",
22
+ "bom-ref": "@blamejs/pki@0.8.1",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.0",
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.8.0",
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.8.0",
57
+ "ref": "@blamejs/pki@0.8.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]