@blamejs/pki 0.1.14 → 0.1.15

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,7 +4,27 @@ 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.1.14 — 2026-07-06
7
+ ## v0.1.15 — 2026-07-06
8
+
9
+ CMS EnvelopedData and EncryptedData join the parser, and every documentation example is now executed as a test.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.cms.parse now decodes CMS EnvelopedData (RFC 5652 §6) and EncryptedData (§8). An EnvelopedData returns { version, originatorInfo, recipientInfos, encryptedContentInfo, unprotectedAttrs } with all five RecipientInfo kinds decoded — KeyTransRecipientInfo (§6.2.1, with the issuerAndSerialNumber/subjectKeyIdentifier version coupling enforced), KeyAgreeRecipientInfo (§6.2.2 + RFC 5753 §3.1), KEKRecipientInfo (§6.2.3), PasswordRecipientInfo (§6.2.4), and OtherRecipientInfo (§6.2.5). An EncryptedData returns { version, encryptedContentInfo, unprotectedAttrs }. The wrapped keys, the ciphertext, and all AlgorithmIdentifier parameters are surfaced raw — every recipient carries the keyEncryptionAlgorithm its encryptedKey must be unwrapped with, and a kekid / rKeyId OtherKeyAttribute is surfaced as raw DER — decryption and key-unwrap are a separate layer. The CMSVersion is recomputed and enforced per structure and per recipient, recipientInfos is required non-empty, and the encryptedContent [0] IMPLICIT OCTET STRING is read as the ciphertext directly.
14
+ - The schema engine gains an implicitTag option on pki.schema.engine.seq() and on pki.schema.pkix.algorithmIdentifier(ns, { implicitTag }) — a [tag] IMPLICIT SEQUENCE / AlgorithmIdentifier reader (used by the PasswordRecipientInfo keyDerivationAlgorithm [0]). A call with no option is byte-identical to before.
15
+ - Every @example in the documentation comment blocks is now executed end-to-end as a test (test/layer-0-primitives/doc-examples.test.js, in the smoke gate), not merely parse-checked: an example must run to completion or throw a typed PkiError, and every documented @primitive path must resolve to a real export — so a documented example can no longer drift from the shipped API. A new @originated comment tag records a callable's original availability version when its documented path is later corrected, enforced alongside the @since version gate.
16
+
17
+ ### Changed
18
+
19
+ - The W3C WebCrypto constructor classes (CryptoKey, Crypto, SubtleCrypto, WebCryptoError) are now reachable under pki.webcrypto (e.g. pki.webcrypto.CryptoKey) alongside the ready Crypto instance, matching their documented path; the previously-separate pki.WebCrypto holder is removed.
20
+ - Repository tooling now installs npm packages exclusively through integrity-verified lockfiles: the fuzz build installs the jazzer.js engine via npm ci against the committed fuzz/package-lock.json, and the vendoring script resolves a package to an integrity-pinned lockfile in an isolated staging workspace — no install script runs and the repo's own node_modules is never touched — before bundling. Tooling child processes that need a shell (the Windows npm shim) now receive one explicitly-quoted command string instead of an unescaped argument array.
21
+
22
+ ### Fixed
23
+
24
+ - Two documented API paths that did not resolve at runtime are corrected: pki.webcrypto.CryptoKey (previously reachable only via pki.WebCrypto.CryptoKey) and pki.asn1.read.oid (its comment block labeled the path pki.asn1.readOid, which never existed). Both are now reachable at the documented path.
25
+ - A documentation example for pki.webcrypto.subtle.exportKey referenced an undefined variable; it now generates the key pair it exports.
26
+
27
+ ## v0.1.14 — 2026-07-05
8
28
 
9
29
  An RFC 5755 attribute-certificate parser joins the pki.schema family.
10
30
 
package/README.md CHANGED
@@ -195,7 +195,7 @@ is callable today; nothing below is a stub.
195
195
  | `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode` |
196
196
  | `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
197
197
  | `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
198
- | `pki.schema.cms` | Parse DER / PEM CMS SignedData per RFC 5652 — encapsulated content, signer infos, raw signed-attribute bytes for external verification, certificates / CRLs kept raw, fail-closed; non-SignedData content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
198
+ | `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement, with the wrapped keys / ciphertext surfaced raw), and EncryptedData (§8); certificates / CRLs kept raw, fail-closed; remaining content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
199
199
  | `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode` |
200
200
  | `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode` |
201
201
  | `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode` |
package/index.js CHANGED
@@ -42,13 +42,17 @@ module.exports = {
42
42
  // `schema` is the family: the L2 structure-schema engine (schema.engine) and
43
43
  // the per-format parsers (schema.x509, …) with detect-and-route schema.parse.
44
44
  schema: schema,
45
- // A ready W3C Crypto instance (globalThis.crypto shape) + the classes
46
- // for constructing more. PQC-first, classical-capable, zero-dep.
47
- webcrypto: webcrypto.webcrypto,
48
- WebCrypto: {
49
- Crypto: webcrypto.Crypto,
50
- SubtleCrypto: webcrypto.SubtleCrypto,
51
- CryptoKey: webcrypto.CryptoKey,
52
- WebCryptoError: webcrypto.WebCryptoError,
53
- },
45
+ // A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
46
+ // constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
47
+ // .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
48
+ webcrypto: _webcryptoNamespace(),
54
49
  };
50
+
51
+ function _webcryptoNamespace() {
52
+ var wc = webcrypto.webcrypto; // the ready Crypto instance
53
+ wc.Crypto = webcrypto.Crypto;
54
+ wc.SubtleCrypto = webcrypto.SubtleCrypto;
55
+ wc.CryptoKey = webcrypto.CryptoKey;
56
+ wc.WebCryptoError = webcrypto.WebCryptoError;
57
+ return wc;
58
+ }
package/lib/asn1-der.js CHANGED
@@ -408,9 +408,10 @@ function readNullImplicit(node, tag) {
408
408
  }
409
409
 
410
410
  /**
411
- * @primitive pki.asn1.readOid
411
+ * @primitive pki.asn1.read.oid
412
412
  * @signature pki.asn1.read.oid(node) -> "1.2.840.113549.1.1.11"
413
- * @since 0.1.0
413
+ * @since 0.1.15
414
+ * @originated 0.1.0
414
415
  * @status stable
415
416
  * @spec X.690 §8.19
416
417
  * @related pki.oid.name
package/lib/schema-cms.js CHANGED
@@ -9,14 +9,16 @@
9
9
  * @slug cms
10
10
  *
11
11
  * @intro
12
- * CMS SignedData handling per RFC 5652 (§3 ContentInfo envelope, §5 SignedData).
13
- * `parse` turns a DER or PEM (`CMS`) message into a structured object: the
14
- * SignedData version, the digest algorithms, the encapsulated content, the
15
- * certificate / CRL sets, and the signer infos. It is an OID-dispatch envelope —
16
- * ContentInfo reads its `contentType` and structurally decodes only
17
- * `id-signedData`; the other PKCS#7 content types (EnvelopedData, EncryptedData,
18
- * …) are recognized and rejected with a precise `cms/unsupported-content-type`
19
- * rather than a generic unknown-format error.
12
+ * CMS handling per RFC 5652 (§3 ContentInfo envelope). `parse` turns a DER or PEM
13
+ * (`CMS`) message into a structured object and is an OID-dispatch envelope —
14
+ * ContentInfo reads its `contentType` and structurally decodes SignedData (§5),
15
+ * EnvelopedData (§6, with all five RecipientInfo kinds key-transport,
16
+ * key-agreement per RFC 5753, KEK, password, and other), and EncryptedData (§8);
17
+ * the remaining PKCS#7 content types are recognized and rejected with a precise
18
+ * `cms/unsupported-content-type` rather than a generic unknown-format error. A
19
+ * SignedData surfaces its version, digest algorithms, encapsulated content,
20
+ * certificate / CRL sets, and signer infos; an EnvelopedData its recipient infos
21
+ * and encrypted content info; an EncryptedData its encrypted content info.
20
22
  *
21
23
  * CMS is a signed container: the bytes an external verifier must hash are
22
24
  * surfaced RAW and never re-serialized. `encapContentInfo.eContent` is the raw
@@ -53,15 +55,26 @@ var NAME = pkix.name(NS);
53
55
  // version is {1,3} (RFC 5652 §5.1, §5.3). Wider accept maps than any other format.
54
56
  var SIGNED_DATA_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3, "4": 4, "5": 5 });
55
57
  var SIGNER_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3 });
58
+ // EnvelopedData §6.1 (0/2/3/4), EncryptedData §8 (0/2), and the per-RecipientInfo
59
+ // versions (RFC 5652 §6.2.1-§6.2.4): ktri {0,2}, kari {3}, kekri {4}, pwri {0}.
60
+ var ENVELOPED_DATA_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2, "3": 3, "4": 4 });
61
+ var ENCRYPTED_DATA_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2 });
62
+ var KTRI_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2 });
63
+ var KARI_VERSION = pkix.versionReader(NS, { "3": 3 });
64
+ var KEKRI_VERSION = pkix.versionReader(NS, { "4": 4 });
65
+ var PWRI_VERSION = pkix.versionReader(NS, { "0": 0 });
56
66
 
57
- // id-signedData is the one content type this build structurally decodes; the rest
58
- // are recognized-and-deferred (a precise diagnostic, not a silent unknown-format).
59
- // OIDs resolve from the registry (pkcs7 / smimeCt families), never dotted literals.
67
+ // id-signedData / id-envelopedData / id-encryptedData are the content types this
68
+ // build structurally decodes; the rest are recognized-and-deferred (a precise
69
+ // diagnostic, not a silent unknown-format). OIDs resolve from the registry (pkcs7 /
70
+ // smimeCt families), never dotted literals.
60
71
  var OID_SIGNED_DATA = oid.byName("signedData");
72
+ var OID_ENVELOPED_DATA = oid.byName("envelopedData");
73
+ var OID_ENCRYPTED_DATA = oid.byName("encryptedData");
61
74
  var OID_DATA = oid.byName("data");
62
75
  var DEFERRED = new Set([
63
- oid.byName("data"), oid.byName("envelopedData"), oid.byName("signedAndEnvelopedData"),
64
- oid.byName("digestedData"), oid.byName("encryptedData"), oid.byName("authData"),
76
+ oid.byName("data"), oid.byName("signedAndEnvelopedData"),
77
+ oid.byName("digestedData"), oid.byName("authData"),
65
78
  ]);
66
79
  // The two mandatory signed-attribute types (RFC 5652 §5.3, §11.1/§11.2).
67
80
  var OID_CONTENT_TYPE = oid.byName("contentType");
@@ -277,11 +290,313 @@ var SIGNED_DATA = schema.seq([
277
290
  },
278
291
  });
279
292
 
293
+ // ==== EnvelopedData / EncryptedData (RFC 5652 §6/§8, RFC 5753) ========
294
+ var T = asn1.TAGS;
295
+
296
+ // EncryptedContentInfo ::= SEQUENCE { contentType OID, contentEncryptionAlgorithm
297
+ // AlgorithmIdentifier, encryptedContent [0] IMPLICIT OCTET STRING OPTIONAL } (RFC
298
+ // 5652 §6.1). encryptedContent is [0] IMPLICIT (context PRIMITIVE) — its content
299
+ // octets ARE the ciphertext directly, so it reads through implicitOctetString(0),
300
+ // NOT the [0] EXPLICIT shape ENCAP_CONTENT_INFO uses (which would double-strip a
301
+ // length header). The ciphertext + algorithm parameters are surfaced RAW.
302
+ var ENCRYPTED_CONTENT_INFO = schema.seq([
303
+ schema.field("contentType", schema.oidLeaf()),
304
+ schema.field("contentEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
305
+ schema.optional("encryptedContent", schema.implicitOctetString(0), { tag: 0 }),
306
+ ], {
307
+ assert: "sequence", arity: { min: 2, max: 3 }, code: "cms/bad-encrypted-content-info", what: "EncryptedContentInfo",
308
+ build: function (m) {
309
+ return {
310
+ contentType: m.fields.contentType.value,
311
+ contentEncryptionAlgorithm: m.fields.contentEncryptionAlgorithm.value.result,
312
+ encryptedContent: m.fields.encryptedContent.present ? m.fields.encryptedContent.value : null,
313
+ };
314
+ },
315
+ });
316
+
317
+ // RecipientIdentifier ::= CHOICE { issuerAndSerialNumber, subjectKeyIdentifier [0]
318
+ // IMPLICIT OCTET STRING } (RFC 5652 §6.2.1) — structurally identical to
319
+ // SignerIdentifier; reuse ISSUER_AND_SERIAL + the implicitOctetString(0) leaf.
320
+ var RECIPIENT_IDENTIFIER = schema.choice([
321
+ { when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: ISSUER_AND_SERIAL },
322
+ { when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitOctetString(0) },
323
+ ], { code: "cms/bad-recipient-identifier", what: "RecipientIdentifier" });
324
+
325
+ // KeyTransRecipientInfo ::= SEQUENCE { version(0|2), rid RecipientIdentifier,
326
+ // keyEncryptionAlgorithm, encryptedKey OCTET STRING } (RFC 5652 §6.2.1).
327
+ var KEY_TRANS_RECIPIENT_INFO = schema.seq([
328
+ schema.field("version", KTRI_VERSION),
329
+ schema.field("rid", RECIPIENT_IDENTIFIER),
330
+ schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
331
+ schema.field("encryptedKey", schema.octetString()),
332
+ ], {
333
+ assert: "sequence", code: "cms/bad-ktri", what: "KeyTransRecipientInfo",
334
+ build: function (m) {
335
+ var version = m.fields.version.value;
336
+ var ridNode = m.fields.rid.node;
337
+ var isSkid = ridNode.tagClass === "context" && ridNode.tagNumber === 0;
338
+ // RFC 5652 §6.2.1 — rid ⇔ version: issuerAndSerialNumber ⇒ 0, subjectKeyIdentifier ⇒ 2.
339
+ if (isSkid && version !== 2) throw NS.E("cms/bad-recipient-version", "a subjectKeyIdentifier recipient identifier requires KeyTransRecipientInfo version 2 (RFC 5652 §6.2.1)");
340
+ if (!isSkid && version !== 0) throw NS.E("cms/bad-recipient-version", "an issuerAndSerialNumber recipient identifier requires KeyTransRecipientInfo version 0 (RFC 5652 §6.2.1)");
341
+ return {
342
+ type: "ktri", version: version,
343
+ rid: isSkid ? { subjectKeyIdentifier: m.fields.rid.value } : m.fields.rid.value.result,
344
+ ridType: isSkid ? "subjectKeyIdentifier" : "issuerAndSerialNumber",
345
+ keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
346
+ encryptedKey: m.fields.encryptedKey.value,
347
+ };
348
+ },
349
+ });
350
+
351
+ // OriginatorPublicKey ::= SEQUENCE { algorithm, publicKey BIT STRING } (RFC 5753
352
+ // §3.1.1), reached as originatorKey [1] IMPLICIT — SPKI-shaped but cannot reuse
353
+ // pkix.spki (which asserts a universal SEQUENCE), so assert:"constructed".
354
+ var ORIGINATOR_PUBLIC_KEY = schema.seq([
355
+ schema.field("algorithm", ALGORITHM_IDENTIFIER),
356
+ schema.field("publicKey", schema.bitString()),
357
+ ], {
358
+ assert: "constructed", code: "cms/bad-originator-public-key", what: "OriginatorPublicKey",
359
+ build: function (m) { return { algorithm: m.fields.algorithm.value.result, publicKey: m.fields.publicKey.value }; },
360
+ });
361
+
362
+ // OriginatorIdentifierOrKey ::= CHOICE { issuerAndSerialNumber, subjectKeyIdentifier
363
+ // [0] IMPLICIT OCTET STRING, originatorKey [1] IMPLICIT OriginatorPublicKey }.
364
+ var ORIGINATOR_IDENTIFIER_OR_KEY = schema.choice([
365
+ { when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: ISSUER_AND_SERIAL },
366
+ { when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitOctetString(0) },
367
+ { when: { tagClass: "context", tagNumber: 1 }, schema: ORIGINATOR_PUBLIC_KEY },
368
+ ], { code: "cms/bad-originator-identifier", what: "OriginatorIdentifierOrKey" });
369
+
370
+ // RecipientKeyIdentifier (RFC 5652 §6.2.2) and KEKIdentifier (§6.2.3) are one
371
+ // shape — { <keyId> OCTET STRING, date GeneralizedTime OPTIONAL, other
372
+ // OtherKeyAttribute OPTIONAL } — differing only in the key-id field's name and
373
+ // the enclosing tag form. One factory defines both so the OPTIONAL handling
374
+ // (date and the raw-surfaced OtherKeyAttribute) cannot diverge between them.
375
+ function keyIdentifierSchema(keyIdName, assert, code, what) {
376
+ return schema.seq([
377
+ schema.field(keyIdName, schema.octetString()),
378
+ schema.optional("date", schema.time(NS), { whenUniversal: [T.GENERALIZED_TIME] }),
379
+ schema.optional("other", schema.any(), { whenUniversal: [T.SEQUENCE] }),
380
+ ], {
381
+ assert: assert, code: code, what: what,
382
+ build: function (m) {
383
+ var out = {};
384
+ out[keyIdName] = m.fields[keyIdName].value;
385
+ out.date = m.fields.date.present ? m.fields.date.value : null;
386
+ out.other = m.fields.other.present ? m.fields.other.node.bytes : null;
387
+ return out;
388
+ },
389
+ });
390
+ }
391
+
392
+ // Reached as rKeyId [0] IMPLICIT (a SEQUENCE — constructed, unlike ktri's [0] leaf).
393
+ var RECIPIENT_KEY_IDENTIFIER = keyIdentifierSchema("subjectKeyIdentifier",
394
+ "constructed", "cms/bad-recipient-key-identifier", "RecipientKeyIdentifier");
395
+
396
+ // KeyAgreeRecipientIdentifier ::= CHOICE { issuerAndSerialNumber, rKeyId [0] IMPLICIT
397
+ // RecipientKeyIdentifier } (RFC 5652 §6.2.2).
398
+ var KEY_AGREE_RECIPIENT_IDENTIFIER = schema.choice([
399
+ { when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: ISSUER_AND_SERIAL },
400
+ { when: { tagClass: "context", tagNumber: 0 }, schema: RECIPIENT_KEY_IDENTIFIER },
401
+ ], { code: "cms/bad-kari-identifier", what: "KeyAgreeRecipientIdentifier" });
402
+
403
+ // RecipientEncryptedKey ::= SEQUENCE { rid KeyAgreeRecipientIdentifier, encryptedKey
404
+ // OCTET STRING } (RFC 5652 §6.2.2).
405
+ var RECIPIENT_ENCRYPTED_KEY = schema.seq([
406
+ schema.field("rid", KEY_AGREE_RECIPIENT_IDENTIFIER),
407
+ schema.field("encryptedKey", schema.octetString()),
408
+ ], {
409
+ assert: "sequence", code: "cms/bad-recipient-encrypted-key", what: "RecipientEncryptedKey",
410
+ build: function (m) {
411
+ var ridNode = m.fields.rid.node;
412
+ var isRkid = ridNode.tagClass === "context" && ridNode.tagNumber === 0;
413
+ return {
414
+ rid: isRkid ? m.fields.rid.value.result : m.fields.rid.value.result,
415
+ encryptedKey: m.fields.encryptedKey.value,
416
+ };
417
+ },
418
+ });
419
+
420
+ // KeyAgreeRecipientInfo ::= [1] IMPLICIT SEQUENCE { version(3), originator [0]
421
+ // EXPLICIT OriginatorIdentifierOrKey, ukm [1] EXPLICIT OPTIONAL,
422
+ // keyEncryptionAlgorithm, recipientEncryptedKeys SEQUENCE OF } (RFC 5652 §6.2.2 +
423
+ // RFC 5753 §3.1.1). originator [0] is EXPLICIT (wraps a CHOICE).
424
+ var KEY_AGREE_RECIPIENT_INFO = schema.seq([
425
+ schema.field("version", KARI_VERSION),
426
+ schema.field("originator", schema.explicit(0, ORIGINATOR_IDENTIFIER_OR_KEY, { code: "cms/bad-kari" })),
427
+ schema.optional("ukm", schema.octetString(), { tag: 1, explicit: true, emptyCode: "cms/bad-kari" }),
428
+ schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
429
+ schema.field("recipientEncryptedKeys", schema.seqOf(RECIPIENT_ENCRYPTED_KEY, { code: "cms/bad-recipient-encrypted-keys", what: "recipientEncryptedKeys" })),
430
+ ], {
431
+ assert: "constructed", code: "cms/bad-kari", what: "KeyAgreeRecipientInfo",
432
+ build: function (m) {
433
+ var origNode = m.fields.originator.node.children[0];
434
+ var origForm = origNode.tagClass === "context" ? (origNode.tagNumber === 0 ? "subjectKeyIdentifier" : "originatorKey") : "issuerAndSerialNumber";
435
+ var origVal = m.fields.originator.value;
436
+ return {
437
+ type: "kari", version: m.fields.version.value,
438
+ originator: { form: origForm, value: origForm === "subjectKeyIdentifier" ? origVal : origVal.result },
439
+ ukm: m.fields.ukm.present ? m.fields.ukm.value : null,
440
+ keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
441
+ recipientEncryptedKeys: m.fields.recipientEncryptedKeys.value.items.map(function (it) { return it.value.result; }),
442
+ };
443
+ },
444
+ });
445
+
446
+ // KEKRecipientInfo ::= [2] IMPLICIT SEQUENCE { version(4), kekid KEKIdentifier,
447
+ // keyEncryptionAlgorithm, encryptedKey } (RFC 5652 §6.2.3).
448
+ var KEK_IDENTIFIER = keyIdentifierSchema("keyIdentifier",
449
+ "sequence", "cms/bad-kek-identifier", "KEKIdentifier");
450
+ var KEK_RECIPIENT_INFO = schema.seq([
451
+ schema.field("version", KEKRI_VERSION),
452
+ schema.field("kekid", KEK_IDENTIFIER),
453
+ schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
454
+ schema.field("encryptedKey", schema.octetString()),
455
+ ], {
456
+ assert: "constructed", code: "cms/bad-kekri", what: "KEKRecipientInfo",
457
+ build: function (m) {
458
+ return {
459
+ type: "kekri", version: m.fields.version.value,
460
+ kekid: m.fields.kekid.value.result,
461
+ keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
462
+ encryptedKey: m.fields.encryptedKey.value,
463
+ };
464
+ },
465
+ });
466
+
467
+ // PasswordRecipientInfo ::= [3] IMPLICIT SEQUENCE { version(0), keyDerivationAlgorithm
468
+ // [0] IMPLICIT AlgorithmIdentifier OPTIONAL, keyEncryptionAlgorithm, encryptedKey }
469
+ // (RFC 5652 §6.2.4). keyDerivationAlgorithm [0] IMPLICIT is the one field needing
470
+ // the implicitTag AlgorithmIdentifier; present iff the first post-version node is [0].
471
+ var PASSWORD_RECIPIENT_INFO = schema.seq([
472
+ schema.field("version", PWRI_VERSION),
473
+ schema.optional("keyDerivationAlgorithm", pkix.algorithmIdentifier(NS, { implicitTag: 0 }), { tag: 0 }),
474
+ schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
475
+ schema.field("encryptedKey", schema.octetString()),
476
+ ], {
477
+ assert: "constructed", code: "cms/bad-pwri", what: "PasswordRecipientInfo",
478
+ build: function (m) {
479
+ return {
480
+ type: "pwri", version: m.fields.version.value,
481
+ keyDerivationAlgorithm: m.fields.keyDerivationAlgorithm.present ? m.fields.keyDerivationAlgorithm.value.result : null,
482
+ keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
483
+ encryptedKey: m.fields.encryptedKey.value,
484
+ };
485
+ },
486
+ });
487
+
488
+ // OtherRecipientInfo ::= [4] IMPLICIT SEQUENCE { oriType OID, oriValue ANY } (RFC
489
+ // 5652 §6.2.5) — oriValue opaque, surfaced RAW (the shipped encoder routes
490
+ // KEMRecipientInfo through here).
491
+ var OTHER_RECIPIENT_INFO = schema.seq([
492
+ schema.field("oriType", schema.oidLeaf()),
493
+ schema.field("oriValue", schema.any()),
494
+ ], {
495
+ assert: "constructed", code: "cms/bad-ori", what: "OtherRecipientInfo",
496
+ build: function (m) { return { type: "ori", oriType: m.fields.oriType.value, oriValue: m.fields.oriValue.node.bytes }; },
497
+ });
498
+
499
+ // RecipientInfo ::= CHOICE { ktri KeyTransRecipientInfo, kari [1], kekri [2], pwri
500
+ // [3], ori [4] } (RFC 5652 §6.2). A bare universal SEQUENCE is ktri (the untagged
501
+ // alternative). An unknown context tag → no arm → cms/bad-recipient-info.
502
+ var RECIPIENT_INFO = schema.choice([
503
+ { when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: KEY_TRANS_RECIPIENT_INFO },
504
+ { when: { tagClass: "context", tagNumber: 1 }, schema: KEY_AGREE_RECIPIENT_INFO },
505
+ { when: { tagClass: "context", tagNumber: 2 }, schema: KEK_RECIPIENT_INFO },
506
+ { when: { tagClass: "context", tagNumber: 3 }, schema: PASSWORD_RECIPIENT_INFO },
507
+ { when: { tagClass: "context", tagNumber: 4 }, schema: OTHER_RECIPIENT_INFO },
508
+ ], { code: "cms/bad-recipient-info", what: "RecipientInfo" });
509
+
510
+ // OriginatorInfo ::= [0] IMPLICIT SEQUENCE { certs [0] IMPLICIT OPTIONAL, crls [1]
511
+ // IMPLICIT OPTIONAL } (RFC 5652 §6.1). Members surfaced RAW (their outer tag feeds
512
+ // the version rule).
513
+ var ORIGINATOR_INFO = schema.seq([
514
+ schema.optional("certs", schema.implicitSetOf(0, schema.any(), { min: 1, code: "cms/bad-originator-certs", what: "certs" }), { tag: 0 }),
515
+ schema.optional("crls", schema.implicitSetOf(1, schema.any(), { min: 1, code: "cms/bad-originator-crls", what: "crls" }), { tag: 1 }),
516
+ ], {
517
+ assert: "constructed", code: "cms/bad-originator-info", what: "OriginatorInfo",
518
+ build: function (m) {
519
+ return {
520
+ certs: m.fields.certs.present ? m.fields.certs.value.items.map(rawElement) : [],
521
+ crls: m.fields.crls.present ? m.fields.crls.value.items.map(rawElement) : [],
522
+ };
523
+ },
524
+ });
525
+
526
+ // RFC 5652 §6.1 — the exact EnvelopedData CMSVersion, from originatorInfo's raw
527
+ // cert/crl outer tags and the recipient arms (a cert `other` is [3], a v2AttrCert
528
+ // [2]; a crl `other` is [1]; a pwri or ori forces v3; all-ktri-IAS with no
529
+ // originatorInfo/unprotectedAttrs is v0; everything else v2).
530
+ function _expectedEnvelopedDataVersion(originatorInfo, recipientInfos, hasUnprotectedAttrs) {
531
+ function ctx(el, n) { return el.tagClass === "context" && el.tagNumber === n; }
532
+ var hasOrig = !!originatorInfo;
533
+ var certs = hasOrig ? originatorInfo.certs : [];
534
+ var crls = hasOrig ? originatorInfo.crls : [];
535
+ if (hasOrig && (certs.some(function (c) { return ctx(c, 3); }) || crls.some(function (c) { return ctx(c, 1); }))) return 4;
536
+ if ((hasOrig && certs.some(function (c) { return ctx(c, 2); })) ||
537
+ recipientInfos.some(function (r) { return r.type === "pwri" || r.type === "ori"; })) return 3;
538
+ if (!hasOrig && !hasUnprotectedAttrs &&
539
+ recipientInfos.every(function (r) { return r.type === "ktri" && r.ridType === "issuerAndSerialNumber"; })) return 0;
540
+ return 2;
541
+ }
542
+
543
+ // EnvelopedData ::= SEQUENCE { version, originatorInfo [0] IMPLICIT OPTIONAL,
544
+ // recipientInfos RecipientInfos (SET SIZE 1..MAX), encryptedContentInfo,
545
+ // unprotectedAttrs [1] IMPLICIT OPTIONAL } (RFC 5652 §6.1). recipientInfos is
546
+ // min:1 (an empty SET is non-conformant — the INVERSE of SignedData's degenerate
547
+ // signerInfos).
548
+ var ENVELOPED_DATA = schema.seq([
549
+ schema.field("version", ENVELOPED_DATA_VERSION),
550
+ schema.optional("originatorInfo", ORIGINATOR_INFO, { tag: 0 }),
551
+ schema.field("recipientInfos", schema.setOf(RECIPIENT_INFO, { min: 1, code: "cms/bad-recipient-infos", what: "recipientInfos" })),
552
+ schema.field("encryptedContentInfo", ENCRYPTED_CONTENT_INFO),
553
+ schema.optional("unprotectedAttrs", schema.implicitSetOf(1, ATTRIBUTE, { min: 1, code: "cms/bad-unprotected-attrs", what: "unprotectedAttrs" }), { tag: 1 }),
554
+ ], {
555
+ assert: "sequence", code: "cms/bad-enveloped-data", what: "EnvelopedData",
556
+ build: function (m) {
557
+ var version = m.fields.version.value;
558
+ var originatorInfo = m.fields.originatorInfo.present ? m.fields.originatorInfo.value.result : null;
559
+ var recipientInfos = m.fields.recipientInfos.value.items.map(function (it) { return it.value.result; });
560
+ var hasUnprotectedAttrs = m.fields.unprotectedAttrs.present;
561
+ var expected = _expectedEnvelopedDataVersion(originatorInfo, recipientInfos, hasUnprotectedAttrs);
562
+ if (version !== expected) throw NS.E("cms/bad-version", "EnvelopedData version " + version + " does not match its contents (RFC 5652 §6.1 requires v" + expected + ")");
563
+ return {
564
+ version: version,
565
+ originatorInfo: originatorInfo,
566
+ recipientInfos: recipientInfos,
567
+ encryptedContentInfo: m.fields.encryptedContentInfo.value.result,
568
+ unprotectedAttrs: hasUnprotectedAttrs ? m.fields.unprotectedAttrs.value.items.map(function (it) { return it.value.result; }) : null,
569
+ };
570
+ },
571
+ });
572
+
573
+ // EncryptedData ::= SEQUENCE { version, encryptedContentInfo, unprotectedAttrs [1]
574
+ // IMPLICIT OPTIONAL } (RFC 5652 §8) — no recipients, no originatorInfo; the CEK is
575
+ // distributed out of band. version is 0, or 2 iff unprotectedAttrs are present.
576
+ var ENCRYPTED_DATA = schema.seq([
577
+ schema.field("version", ENCRYPTED_DATA_VERSION),
578
+ schema.field("encryptedContentInfo", ENCRYPTED_CONTENT_INFO),
579
+ schema.optional("unprotectedAttrs", schema.implicitSetOf(1, ATTRIBUTE, { min: 1, code: "cms/bad-unprotected-attrs", what: "unprotectedAttrs" }), { tag: 1 }),
580
+ ], {
581
+ assert: "sequence", code: "cms/bad-encrypted-data", what: "EncryptedData",
582
+ build: function (m) {
583
+ var version = m.fields.version.value;
584
+ var hasUnprotectedAttrs = m.fields.unprotectedAttrs.present;
585
+ var expected = hasUnprotectedAttrs ? 2 : 0;
586
+ if (version !== expected) throw NS.E("cms/bad-version", "EncryptedData version " + version + " does not match its contents (RFC 5652 §8 requires v" + expected + ")");
587
+ return {
588
+ version: version,
589
+ encryptedContentInfo: m.fields.encryptedContentInfo.value.result,
590
+ unprotectedAttrs: hasUnprotectedAttrs ? m.fields.unprotectedAttrs.value.items.map(function (it) { return it.value.result; }) : null,
591
+ };
592
+ },
593
+ });
594
+
280
595
  // ContentInfo ::= SEQUENCE { contentType OID, content [0] EXPLICIT ANY DEFINED BY
281
596
  // contentType } (RFC 5652 §3). The content is captured raw (explicit(0, any()))
282
597
  // and re-dispatched by contentType inside the build: id-signedData walks
283
- // SIGNED_DATA, the other PKCS#7 types are recognized-and-deferred, unknown OIDs
284
- // are rejected.
598
+ // SIGNED_DATA, id-envelopedData / id-encryptedData walk their schemas, the other
599
+ // PKCS#7 types are recognized-and-deferred, unknown OIDs are rejected.
285
600
  var CONTENT_INFO = schema.seq([
286
601
  schema.field("contentType", schema.oidLeaf()),
287
602
  schema.field("content", schema.explicit(0, schema.any(), { code: "cms/not-a-content-info" })),
@@ -290,6 +605,8 @@ var CONTENT_INFO = schema.seq([
290
605
  build: function (m, ctx) {
291
606
  var ct = m.fields.contentType.value;
292
607
  if (ct === OID_SIGNED_DATA) return schema.walk(SIGNED_DATA, m.fields.content.value, ctx).result;
608
+ if (ct === OID_ENVELOPED_DATA) return schema.walk(ENVELOPED_DATA, m.fields.content.value, ctx).result;
609
+ if (ct === OID_ENCRYPTED_DATA) return schema.walk(ENCRYPTED_DATA, m.fields.content.value, ctx).result;
293
610
  if (DEFERRED.has(ct)) {
294
611
  throw NS.E("cms/unsupported-content-type", (ctx.oid.name(ct) || ct) + " is recognized but not parsed by this build");
295
612
  }
package/lib/schema-crl.js CHANGED
@@ -121,6 +121,11 @@ var REVOKED_LIST = schema.seqOf(REVOKED_ENTRY, {
121
121
  // revokedCertificates=SEQUENCE) are disambiguated by their universal tag;
122
122
  // crlExtensions is modeled as a trailing [0]..[0] so a stray non-[0] trailing
123
123
  // context tag is REJECTED (crl/bad-tbs), not silently ignored.
124
+ // `signature` is consumed by the CERTIFICATE_LIST build (the §5.1.1.2
125
+ // outer==inner agreement check reads tbsMatch.fields.signature.node.bytes);
126
+ // the operator reads the surfaced outer signatureAlgorithm, which that
127
+ // check proves byte-identical.
128
+ // allow:schema-build-drops-parsed-field
124
129
  var TBS_CERTLIST = schema.seq([
125
130
  schema.optional("version", CRL_VERSION, { whenUniversal: [TAGS.INTEGER] }),
126
131
  schema.field("signature", ALGORITHM_IDENTIFIER),
@@ -63,11 +63,12 @@ function _assertShape(schema, node, ctx) {
63
63
  _fail(ctx, schema.code, (schema.what || "value") + " must be a constructed value");
64
64
  }
65
65
  } else if (mode === "implicit") {
66
- // IMPLICIT [tag] SET/SEQUENCE OF: the context tag replaces the universal
67
- // tag, so the node is a context-class constructed [tag] and its direct
68
- // children are the items (no inner universal SET, no EXPLICIT unwrap).
66
+ // IMPLICIT [tag] SET/SEQUENCE OF or a fixed-field IMPLICIT [tag] SEQUENCE body:
67
+ // the context tag replaces the universal tag, so the node is a context-class
68
+ // constructed [tag] and its direct children are the items / positional fields
69
+ // (no inner universal tag, no EXPLICIT unwrap).
69
70
  if (node.tagClass !== "context" || node.tagNumber !== schema.implicitTag || !node.children) {
70
- _fail(ctx, schema.code, (schema.what || "value") + " must be an IMPLICIT [" + schema.implicitTag + "] SET OF");
71
+ _fail(ctx, schema.code, (schema.what || "value") + " must be an IMPLICIT [" + schema.implicitTag + "] constructed value");
71
72
  }
72
73
  } else {
73
74
  _fail(ctx, schema.code, "unknown assert mode " + JSON.stringify(mode));
@@ -163,7 +164,7 @@ function trailing(members, opts) {
163
164
 
164
165
  function seq(fields, opts) {
165
166
  opts = opts || {};
166
- return { kind: "seq", fields: fields, assert: opts.assert || "sequence", arity: opts.arity,
167
+ return { kind: "seq", fields: fields, assert: opts.assert || "sequence", implicitTag: opts.implicitTag, arity: opts.arity,
167
168
  code: opts.code, what: opts.what, build: opts.build, checks: opts.checks || [] };
168
169
  }
169
170
 
@@ -129,12 +129,18 @@ function versionReader(ns, accept) {
129
129
  });
130
130
  }
131
131
 
132
- function algorithmIdentifier(ns) {
132
+ // opts.implicitTag (optional): read the AlgorithmIdentifier as a [tag] IMPLICIT
133
+ // SEQUENCE (a context-class constructed node whose children are algorithm + parameters),
134
+ // for the RFC 5652 §6.2.4 pwri.keyDerivationAlgorithm [0]. With no opts the shape is a
135
+ // universal SEQUENCE, byte-identical to every existing caller.
136
+ function algorithmIdentifier(ns, opts) {
137
+ opts = opts || {};
133
138
  return schema.seq([
134
139
  schema.field("algorithm", schema.oidLeaf()),
135
140
  schema.optional("parameters", schema.any(), { whenAny: true }),
136
141
  ], {
137
- assert: "sequence", arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
142
+ assert: opts.implicitTag != null ? "implicit" : "sequence", implicitTag: opts.implicitTag,
143
+ arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
138
144
  build: function (m, ctx) {
139
145
  var dotted = m.fields.algorithm.value;
140
146
  return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
package/lib/webcrypto.js CHANGED
@@ -531,6 +531,7 @@ function _curveFromKey(ko) {
531
531
  * point). Throws unless the key was created `extractable`.
532
532
  *
533
533
  * @example
534
+ * var keyPair = await pki.webcrypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
534
535
  * var spki = await pki.webcrypto.subtle.exportKey("spki", keyPair.publicKey);
535
536
  */
536
537
  SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
@@ -566,7 +567,7 @@ function _rawPublic(key) {
566
567
  *
567
568
  * A ready `Crypto` instance (the shape of `globalThis.crypto`) exposing
568
569
  * `getRandomValues`, `randomUUID`, and `subtle`. Construct additional
569
- * instances with `new pki.WebCrypto.Crypto()`.
570
+ * instances with `new pki.webcrypto.Crypto()`.
570
571
  *
571
572
  * @example
572
573
  * var iv = pki.webcrypto.getRandomValues(new Uint8Array(12));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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:7865e3f3-f43e-4cdb-818f-8a8fe2cf2d7a",
5
+ "serialNumber": "urn:uuid:d18de1ff-7ebd-406f-91cb-801da4286f4f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-06T02:35:05.577Z",
8
+ "timestamp": "2026-07-06T08:32:53.685Z",
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.1.14",
22
+ "bom-ref": "@blamejs/pki@0.1.15",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.14",
25
+ "version": "0.1.15",
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.1.14",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.15",
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.1.14",
57
+ "ref": "@blamejs/pki@0.1.15",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]