@blamejs/pki 0.2.26 → 0.2.28

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,24 @@ 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.2.28 — 2026-07-16
8
+
9
+ The Certificate Transparency log-list trust surface arrives as pki.ct.parseLogList and pki.ct.verifySctWithLogList: resolve a trusted CT log's key from an SCT's log id and verify it in one step, ingesting the CT log-list JSON into state- and temporal-interval-constrained trusted logs.
10
+
11
+ ### Added
12
+
13
+ - pki.ct.parseLogList(json, opts) ingests a Certificate Transparency log-list JSON document into { logs, byLogId } -- constraint-carrying trusted logs keyed by log id. Each log's key is base64-decoded to its DER SubjectPublicKeyInfo and validated as on-profile; the log id is recomputed as SHA-256 of the key and must equal the stated log_id (RFC 6962 section 3.2, ct/log-id-mismatch), so a swapped key or a flipped id is refused. The state (exactly one of pending/qualified/usable/readonly/retired/rejected) and temporal_interval decode into trust constraints, and two entries for one recomputed id must agree or the list is rejected (ct/duplicate-log). Parsing is offline and routes through the bounded, duplicate-member-rejecting JSON reader; every malformed input is a typed CtError.
14
+ - pki.ct.verifySctWithLogList(entry, sct, logList, opts) resolves the trusted CT log for an SCT by its log id and verifies it in one step. The log's state gates trust (usable/qualified/readonly proceed; a retired log only for an SCT timestamped before its retirement; pending/rejected are ct/log-untrusted) and its temporal_interval gates the covered certificate (the cert's notAfter -- from the leaf certificate for a precert-free entry, or opts.certNotAfter -- must fall in the log's window, and a windowed log with no resolvable notAfter is ct/temporal-interval, never silently skipped). The signature check is delegated to the shipped pki.ct.verifySct. Resolves true for a valid signature from a trusted, in-window log, false on a cryptographic mismatch, and throws a typed CtError on any structural or trust failure.
15
+
16
+ ## v0.2.27 — 2026-07-16
17
+
18
+ CMS CompressedData arrives as pki.cms.compress / pki.cms.decompress, with the matching S/MIME pki.smime.compress / pki.smime.decompress: compress and recover RFC 3274 messages, with a bounded inflate that defends the decompression-bomb class.
19
+
20
+ ### Added
21
+
22
+ - pki.cms.compress(content, opts) and pki.cms.decompress(input, opts) produce and consume a CMS CompressedData (RFC 3274): ZLIB (RFC 1950 / RFC 1951) compression, version 0, id-alg-zlibCompress with omitted parameters. opts.contentType sets the inner content type (default id-data), opts.level the DEFLATE level, opts.pem returns PEM. Decompress requires version 0, id-alg-zlibCompress with absent-or-NULL parameters, and a present encapsulated content; it bounds the decompressed output at C.LIMITS.COMPRESS_MAX_BYTES (16 MiB, tightened downward via opts.maxOutputBytes) and stops before the output is materialized, so a decompression bomb throws cms/decompress-too-large rather than exhausting memory. Every malformed or truncated stream fails closed as cms/decompress-failed. Fail-closed with typed CmsError.
23
+ - pki.smime.compress(content, opts) and pki.smime.decompress(message, opts) assemble and open a compressed S/MIME message (RFC 8551 section 3.6): an opaque application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z entity whose base64 body is a CMS CompressedData. The MIME entity is canonicalized before compression and recovered exactly; the recovered content -- which may itself be a signed or enveloped S/MIME message -- is returned for the caller to feed back to pki.smime.verify / pki.smime.decrypt. Compression is a size transform with no integrity, confidentiality, or authentication (RFC 8551 section 2.4.5). Receive-tolerant: OpenSSL's legacy application/x-pkcs7-mime and a missing smime-type are both accepted.
24
+
7
25
  ## v0.2.26 — 2026-07-16
8
26
 
9
27
  S/MIME encryption arrives as pki.smime.encrypt / pki.smime.decrypt: envelope and open RFC 8551 encrypted messages over the CMS layer, with AES-GCM authenticated enveloping as the default and bidirectional OpenSSL interoperability.
package/README.md CHANGED
@@ -221,11 +221,11 @@ is callable today; nothing below is a stub.
221
221
  | `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
222
222
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
223
223
  | `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
224
- | `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt` |
225
- | `pki.smime` | RFC 8551 S/MIME message assembly, verification, and encryption over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt` |
224
+ | `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
225
+ | `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
226
226
  | `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
227
227
  | `pki.ocsp` | RFC 6960 Online Certificate Status Protocol — the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs (CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile, or SHA-2; optional RFC 9654 nonce, optional requestor signature). `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER — the issuing CA directly or a delegated responder, any `pki.cms.sign` key including the post-quantum ML-DSA / SLH-DSA sets, with `good` / `revoked` (reason + time) / `unknown` per-certificate status, and `buildErrorResponse(status)` the unsigned §2.3 error (`tryLater` / `unauthorized` / …). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA or a CA-issued delegate bearing id-kp-OCSPSigning **and** id-pkix-ocsp-nocheck, passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency (`thisUpdate`/`nextUpdate`), and the request-nonce echo — returning `{ status: "good" / "revoked" / "unknown", … }`, never a silent accept. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify` |
228
- | `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. The producing side: `encodeSctList` builds the extension value byte-for-byte (the exact inverse of `parseSctList`) and `signSct` performs a log's signing step (rebuilding the same signed-data preimage the verifier hashes and signing it with the log's ECDSA-P-256 / RSA key). Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct` |
228
+ | `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. The producing side: `encodeSctList` builds the extension value byte-for-byte (the exact inverse of `parseSctList`) and `signSct` performs a log's signing step (rebuilding the same signed-data preimage the verifier hashes and signing it with the log's ECDSA-P-256 / RSA key). The trust surface: `parseLogList` ingests the CT log-list JSON into constraint-carrying trusted logs — recomputing each log's id as SHA-256 of its key and refusing a disagreeing id (a swapped key, §3.2), decoding the state + temporal-interval constraints — and `verifySctWithLogList` resolves the log key from an SCT's log id, enforces the state (usable/qualified/readonly trusted; retired only before retirement; pending/rejected refused) and the temporal-interval window, then delegates the signature check to `verifySct`. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct`, `parseLogList`, `verifySctWithLogList` |
229
229
  | `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
230
230
  | `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
231
231
  | `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
package/lib/acme.js CHANGED
@@ -41,6 +41,7 @@ var x509 = require("./schema-x509");
41
41
  var csr = require("./schema-csr");
42
42
  var pkix = require("./schema-pkix");
43
43
  var constants = require("./constants");
44
+ var rfc3339 = require("./rfc3339");
44
45
  var subtle = require("./webcrypto").webcrypto.subtle;
45
46
  var frameworkError = require("./framework-error");
46
47
 
@@ -51,27 +52,10 @@ function E(code, message, cause) { return new AcmeError(code, message, cause); }
51
52
 
52
53
  function _isObject(v) { return v && typeof v === "object" && !Array.isArray(v); }
53
54
  function _isString(v) { return typeof v === "string"; }
54
- // RFC 3339 date-time (date "T" time with a zone). Beyond the grammar this checks
55
- // CALENDAR validity -- a syntactically well-formed but impossible instant (month 13,
56
- // February 30, hour 25, a +25:00 offset) is rejected, so a downstream expiry / window
57
- // comparison never runs on a value JS Date would silently roll over.
58
- var RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
59
- function _isRfc3339(v) {
60
- if (!_isString(v)) return false;
61
- var m = RFC3339_RE.exec(v);
62
- if (!m) return false;
63
- var year = +m[1], month = +m[2], day = +m[3], hour = +m[4], min = +m[5], sec = +m[6];
64
- if (month < 1 || month > 12) return false;
65
- // Reject a :60 leap second: Node's Date.parse returns NaN for it, so an expiry /
66
- // renewal-window comparison on such a value would silently pass (NaN <= x is
67
- // false). This toolkit only handles instants it can compare, so it fails closed.
68
- if (hour > 23 || min > 59 || sec > 59) return false;
69
- var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
70
- var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
71
- if (day < 1 || day > daysInMonth[month - 1]) return false;
72
- if (m[7]) { if (+m[8] > 23 || +m[9] > 59) return false; } // numeric zone offset
73
- return true;
74
- }
55
+ // RFC 3339 date-time validity (grammar + calendar) lives in the shared lib/rfc3339.js primitive so a
56
+ // downstream expiry / renewal-window comparison never runs on an impossible instant (month 13, a :60
57
+ // leap second, a rolled-over value). pki.ct's log-list window parse composes the same primitive.
58
+ function _isRfc3339(v) { return rfc3339.isValid(v); }
75
59
  // A URL string: an absolute http(s) URI with a real host (RFC 3986). ACME URLs are
76
60
  // server-provided endpoints downstream transport will trust, so they are PARSED
77
61
  // (not prefix-matched) -- a malformed value like "https://[" or a hostless
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the pki.cms.compress / pki.cms.decompress implementation. The operator-facing
6
+ // @module pki.cms + the @primitive blocks live in cms-verify.js, which re-exports these (the
7
+ // cms-encrypt.js / cms-decrypt.js model).
8
+ //
9
+ // CMS CompressedData (RFC 3274) -- the size-transform content type. compress ZLIB-compresses
10
+ // (RFC 1950 / RFC 1951) the content and wraps it as a CompressedData ContentInfo; decompress parses
11
+ // one (over the shipped strict schema-cms parser), enforces version 0 + id-alg-zlibCompress +
12
+ // absent-or-NULL params (RFC 3274 sec. 2), and BOUNDED-inflates the eContent. There is NO crypto
13
+ // here -- CompressedData provides no integrity / confidentiality / authentication (RFC 8551 sec.
14
+ // 2.4.5); it is purely a size transform. The one load-bearing defense is the decompression-bomb
15
+ // cap: inflate stops at C.LIMITS.COMPRESS_MAX_BYTES BEFORE the output is materialized (a
16
+ // resource-exhaustion defense, CWE-409), and every zlib fault collapses to a uniform typed error.
17
+
18
+ var zlib = require("zlib");
19
+ var oid = require("./oid");
20
+ var schemaCms = require("./schema-cms");
21
+ var frameworkError = require("./framework-error");
22
+ var guard = require("./guard-all");
23
+ var C = require("./constants");
24
+ var asn1 = require("./asn1-der");
25
+ var b = asn1.build;
26
+ var CmsError = frameworkError.CmsError;
27
+
28
+ function O(n) { return oid.byName(n); }
29
+ function _err(code, message, cause) { return new CmsError(code, message, cause); }
30
+
31
+ var OID_ZLIB = O("id-alg-zlibCompress");
32
+ // The NULL params DER (0x05 0x00) -- the one non-absent encoding RFC 3274 sec. 2 permits (MAY be NULL).
33
+ var NULL_PARAMS_DER = Buffer.from([0x05, 0x00]);
34
+
35
+ // Compress `content` and wrap it as a CMS CompressedData ContentInfo (RFC 3274 sec. 1.1): version 0,
36
+ // compressionAlgorithm = id-alg-zlibCompress with ABSENT parameters, encapContentInfo = { eContentType
37
+ // (default id-data), eContent = the RFC 1950 ZLIB stream }.
38
+ async function compress(content, opts) {
39
+ opts = opts || {};
40
+ var raw = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
41
+ var level = opts.level;
42
+ if (level !== undefined && (typeof level !== "number" || !isFinite(level) || Math.floor(level) !== level)) throw _err("cms/bad-input", "opts.level must be an integer");
43
+ var ctName = opts.contentType || "data";
44
+ var ctOid = O(ctName);
45
+ if (!ctOid) throw _err("cms/bad-input", "opts.contentType is not a known OID name: " + ctName);
46
+ var stream;
47
+ try { stream = zlib.deflateSync(raw, level !== undefined ? { level: level } : undefined); }
48
+ catch (e) { throw _err("cms/bad-input", "the content could not be compressed (check opts.level)", e); }
49
+ var cd = b.sequence([
50
+ b.integer(0),
51
+ b.sequence([b.oid(OID_ZLIB)]), // compressionAlgorithm, params ABSENT (RFC 3274 sec. 2)
52
+ b.sequence([b.oid(ctOid), b.explicit(0, b.octetString(stream))]),
53
+ ]);
54
+ var ci = b.sequence([b.oid(O("compressedData")), b.explicit(0, cd)]);
55
+ return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
56
+ }
57
+
58
+ // Parse a CMS CompressedData and recover its content. Requires contentTypeName == compressedData,
59
+ // version 0 (enforced by the parser), compressionAlgorithm == id-alg-zlibCompress with absent-or-NULL
60
+ // params, and a present eContent; then BOUNDED-inflates. Every failure is a typed CmsError.
61
+ async function decompress(input, opts) {
62
+ opts = opts || {};
63
+ var cap = C.LIMITS.COMPRESS_MAX_BYTES;
64
+ if (opts.maxOutputBytes !== undefined) {
65
+ var mo = opts.maxOutputBytes;
66
+ if (typeof mo !== "number" || !isFinite(mo) || mo <= 0 || Math.floor(mo) !== mo) throw _err("cms/bad-input", "opts.maxOutputBytes must be a positive integer");
67
+ if (mo < cap) cap = mo; // a caller may tighten the cap DOWNWARD only, never loosen it
68
+ }
69
+ var parsed = schemaCms.parse(_toDer(input));
70
+ if (parsed.contentTypeName !== "compressedData") throw _err("cms/unsupported-content-type", "input is not a CMS CompressedData (got " + parsed.contentTypeName + ")");
71
+ var alg = parsed.compressionAlgorithm;
72
+ if (alg.oid !== OID_ZLIB) throw _err("cms/unsupported-algorithm", "unsupported compressionAlgorithm " + (alg.name || alg.oid) + " (only id-alg-zlibCompress, RFC 3274 sec. 2)");
73
+ // RFC 3274 sec. 2: the parameters SHOULD be omitted but MAY be NULL -- reject any other encoding.
74
+ if (alg.parameters != null && Buffer.compare(alg.parameters, NULL_PARAMS_DER) !== 0) throw _err("cms/bad-algorithm-parameters", "id-alg-zlibCompress parameters must be absent or NULL (RFC 3274 sec. 2)");
75
+ var eci = parsed.encapContentInfo;
76
+ if (eci.eContent == null) throw _err("cms/no-encapsulated-content", "the CompressedData carries no encapsulated content (a detached CompressedData cannot be decompressed)");
77
+ var content = _inflateBounded(eci.eContent, cap);
78
+ return {
79
+ content: content,
80
+ contentType: eci.eContentType,
81
+ contentTypeName: oid.name(eci.eContentType) || eci.eContentType,
82
+ // Coverage residual: reaching here requires alg.oid === id-alg-zlibCompress (line above), which is
83
+ // always a registered name, so the `|| alg.oid` fallback is a defensive belt, never selected.
84
+ compressionAlgorithm: alg.name || alg.oid,
85
+ };
86
+ }
87
+
88
+ function _toDer(input) {
89
+ if (Buffer.isBuffer(input)) return input;
90
+ if (input instanceof Uint8Array) return Buffer.from(input);
91
+ if (typeof input === "string") { try { return schemaCms.pemDecode(input); } catch (e) { throw _err("cms/bad-input", "the CMS PEM could not be decoded", e); } }
92
+ throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
93
+ }
94
+
95
+ // Inflate an RFC 1950 ZLIB stream, bounding the output at `cap` bytes BEFORE it is materialized:
96
+ // Node's maxOutputLength throws ERR_BUFFER_TOO_LARGE the moment the output would exceed the bound
97
+ // (it does not first allocate the whole output), so a decompression bomb cannot exhaust memory
98
+ // (CWE-409). The cap breach maps to cms/decompress-too-large; every other zlib fault (bad RFC-1950
99
+ // header, truncated stream, corrupt DEFLATE, trailing garbage) collapses to the uniform
100
+ // cms/decompress-failed -- a per-errno surface would be needless attack telemetry.
101
+ function _inflateBounded(stream, cap) {
102
+ var view = guard.bytes.view(stream, CmsError, "cms/decompress-failed", "the compressed content");
103
+ try {
104
+ return zlib.inflateSync(view, { maxOutputLength: cap });
105
+ } catch (e) {
106
+ if (e && e.code === "ERR_BUFFER_TOO_LARGE") throw _err("cms/decompress-too-large", "the decompressed output exceeds the " + cap + "-byte cap (a decompression-bomb defense)", e);
107
+ throw _err("cms/decompress-failed", "the compressed content could not be decompressed", e);
108
+ }
109
+ }
110
+
111
+ module.exports = { compress: compress, decompress: decompress };
package/lib/cms-verify.js CHANGED
@@ -37,6 +37,7 @@ var edwardsPoint = require("./edwards-point");
37
37
  var cmsSign = require("./cms-sign");
38
38
  var cmsEncrypt = require("./cms-encrypt");
39
39
  var cmsDecrypt = require("./cms-decrypt");
40
+ var cmsCompress = require("./cms-compress");
40
41
  var signScheme = require("./sign-scheme");
41
42
  var MLDSA_SUITABLE_DIGEST = signScheme.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
42
43
  var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
@@ -658,4 +659,51 @@ var encrypt = cmsEncrypt.encrypt;
658
659
  */
659
660
  var decrypt = cmsDecrypt.decrypt;
660
661
 
661
- module.exports = { verify: verify, sign: sign, encrypt: encrypt, decrypt: decrypt };
662
+ /**
663
+ * @primitive pki.cms.compress
664
+ * @signature pki.cms.compress(content, opts?) -> Promise<Buffer | string>
665
+ * @since 0.2.27
666
+ * @status experimental
667
+ * @spec RFC 3274, RFC 1950, RFC 1951
668
+ * @related pki.cms.decompress, pki.schema.cms.parse
669
+ *
670
+ * ZLIB-compress `content` and wrap it as a CMS `CompressedData` ContentInfo (RFC 3274): version 0,
671
+ * `compressionAlgorithm` = id-alg-zlibCompress (parameters omitted), `encapContentInfo` = the inner
672
+ * content type plus the RFC 1950 ZLIB stream (RFC 1951 DEFLATE) as the eContent. There is exactly
673
+ * ONE compression algorithm in RFC 3274 -- ZLIB -- so there is no algorithm selector. CompressedData
674
+ * is a size transform with NO integrity, confidentiality, or authentication (RFC 8551 sec. 2.4.5);
675
+ * compress it, then sign or encrypt it if you need protection. Malformed input throws a typed `CmsError`.
676
+ *
677
+ * @opts contentType The inner eContentType (an OID name). Default `data`.
678
+ * @opts level The DEFLATE compression level (an integer) forwarded to zlib. Default zlib's default.
679
+ * @opts pem Return a PEM string instead of a DER Buffer.
680
+ * @example
681
+ * var z = await pki.cms.compress(Buffer.from("compress me"));
682
+ */
683
+ var compress = cmsCompress.compress;
684
+
685
+ /**
686
+ * @primitive pki.cms.decompress
687
+ * @signature pki.cms.decompress(input, opts?) -> Promise<{ content, contentType, contentTypeName, compressionAlgorithm }>
688
+ * @since 0.2.27
689
+ * @status experimental
690
+ * @spec RFC 3274, RFC 1950, RFC 1951
691
+ * @related pki.cms.compress, pki.schema.cms.parse
692
+ *
693
+ * Parse a CMS `CompressedData` (DER Buffer or PEM) and recover its content. It requires version 0,
694
+ * `compressionAlgorithm` = id-alg-zlibCompress with absent-or-NULL parameters (RFC 3274 sec. 2), and a
695
+ * present eContent, then inflates the RFC 1950 ZLIB stream. The inflate is BOUNDED at
696
+ * `C.LIMITS.COMPRESS_MAX_BYTES` (16 MiB) and stops before the output is materialized -- a
697
+ * decompression-bomb / resource-exhaustion defense (CWE-409): a cap breach throws
698
+ * `cms/decompress-too-large`, and every malformed / truncated / corrupt stream collapses to the uniform
699
+ * `cms/decompress-failed`. `opts.maxOutputBytes` tightens the cap DOWNWARD only. The verdict carries NO
700
+ * `authenticated` / `valid` field -- CompressedData is not a security assertion (RFC 8551 sec. 2.4.5).
701
+ *
702
+ * @opts maxOutputBytes Lower the decompressed-output cap (a DoS bound; downward only).
703
+ * @example
704
+ * var res = await pki.cms.decompress(compressedDer);
705
+ * res.content; // the recovered plaintext Buffer
706
+ */
707
+ var decompress = cmsCompress.decompress;
708
+
709
+ module.exports = { verify: verify, sign: sign, encrypt: encrypt, decrypt: decrypt, compress: compress, decompress: decompress };
package/lib/constants.js CHANGED
@@ -227,6 +227,9 @@ var LIMITS = {
227
227
  // enforced BEFORE parsing so a hostile document is refused up front.
228
228
  JSON_MAX_BYTES: BYTES.mib(1),
229
229
  JSON_MAX_DEPTH: 32,
230
+ // The CT log-list JSON ceiling -- the full "all logs" variant (with historical logs) exceeds the
231
+ // 1 MiB JSON_MAX_BYTES used for small ACME resource objects; 4 MiB gives headroom while staying bounded.
232
+ CT_LOG_LIST_MAX_BYTES: BYTES.mib(4),
230
233
  // PBKDF2 iteration ceiling on the DECRYPT side (RFC 8018 pwri / PBES2). An attacker-supplied
231
234
  // iterationCount is attacker-controlled work; a message above this is refused before any
232
235
  // derivation (typed cms/iteration-limit), and a caller may cap it lower via opts.maxIterations.
@@ -235,6 +238,10 @@ var LIMITS = {
235
238
  PBKDF2_MAX_SALT: 1024,
236
239
  // A MIME entity / S/MIME message size ceiling -- bounds the header + body decode before a copy.
237
240
  MIME_MAX_BYTES: BYTES.mib(16),
241
+ // The decompressed-output ceiling for CMS CompressedData (RFC 3274) -- bounds the inflate BEFORE the
242
+ // output is materialized (a decompression-bomb / resource-exhaustion defense, CWE-409); a caller may
243
+ // tighten it DOWNWARD via opts.maxOutputBytes, never loosen it.
244
+ COMPRESS_MAX_BYTES: BYTES.mib(16),
238
245
  // ACME challenge token entropy floor (RFC 8555 sec. 8, errata 6950): >= 128
239
246
  // bits of base64url is >= 22 characters. A shorter token is refused before use.
240
247
  ACME_TOKEN_MIN_CHARS: 22,
package/lib/ct.js CHANGED
@@ -48,6 +48,7 @@ var ByteWriter = require("./byte-writer.js");
48
48
  var oid = require("./oid.js");
49
49
  var webcrypto = require("./webcrypto.js");
50
50
  var validator = require("./validator-all.js");
51
+ var rfc3339 = require("./rfc3339.js");
51
52
  var subtle = webcrypto.webcrypto.subtle;
52
53
 
53
54
  var CtError = frameworkError.CtError;
@@ -592,12 +593,208 @@ async function signSct(entry, logKey, opts) {
592
593
  };
593
594
  }
594
595
 
596
+ // ---- CT log-list trust surface (RFC 6962 sec. 3.2 + the CT log-list v3 JSON schema) --------------
597
+
598
+ // The recognized CT log states (the schema `state` oneOf) mapped to a trust decision (the deployed
599
+ // Chrome/Apple CT policy): usable/qualified/readonly are trusted; retired is CONDITIONALLY trusted
600
+ // (only for an SCT timestamped before the retirement instant, checked at verify); pending/rejected
601
+ // are not trusted.
602
+ var LOG_STATE_TRUST = { pending: "no", qualified: "yes", usable: "yes", readonly: "yes", retired: "conditional", rejected: "no" };
603
+
604
+ // Decode a log's `state` oneOf: EXACTLY ONE recognized member, whose timestamp is an RFC 3339 instant.
605
+ // Zero, multiple, or an unrecognized member -> ct/bad-state (fail-closed, never a silent default-trust).
606
+ function _parseLogState(state) {
607
+ if (state == null || typeof state !== "object") throw _ctErr("ct/bad-state", "a CT log entry is missing its state");
608
+ var keys = Object.keys(state);
609
+ if (keys.length !== 1 || !Object.prototype.hasOwnProperty.call(LOG_STATE_TRUST, keys[0])) {
610
+ throw _ctErr("ct/bad-state", "a CT log state must carry exactly one recognized member (pending/qualified/usable/readonly/retired/rejected)");
611
+ }
612
+ var name = keys[0], member = state[name];
613
+ if (member == null || typeof member !== "object") throw _ctErr("ct/bad-state", "the CT log state " + name + " is malformed");
614
+ var since = rfc3339.parse(member.timestamp, _ctErr, "ct/bad-date", "the CT log state timestamp");
615
+ var trust = LOG_STATE_TRUST[name];
616
+ return { name: name, since: since, trusted: trust === "yes", conditional: trust === "conditional" };
617
+ }
618
+
619
+ // Decode a log's `temporal_interval` (both bounds RFC 3339, start strictly before end) or null.
620
+ function _parseTemporalInterval(ti) {
621
+ if (ti == null) return null;
622
+ if (typeof ti !== "object") throw _ctErr("ct/bad-log-list", "a CT log temporal_interval must be an object");
623
+ var start = rfc3339.parse(ti.start_inclusive, _ctErr, "ct/bad-date", "temporal_interval.start_inclusive");
624
+ var end = rfc3339.parse(ti.end_exclusive, _ctErr, "ct/bad-date", "temporal_interval.end_exclusive");
625
+ if (start.getTime() >= end.getTime()) throw _ctErr("ct/bad-log-list", "a CT log temporal_interval start_inclusive must be strictly before end_exclusive");
626
+ return { startInclusive: start, endExclusive: end };
627
+ }
628
+
629
+ // One log-list entry -> a constraint-carrying trusted-log record. The load-bearing M-BIND step: the
630
+ // log-id is RECOMPUTED as SHA-256(SPKI) and MUST equal the stated log_id (RFC 6962 sec. 3.2), so the
631
+ // trusted set is keyed by the authoritative binding, never the document's self-assertion -- a swapped
632
+ // key or a flipped id is refused. The key is also validated as a well-formed, on-profile SPKI at ingest.
633
+ function _parseLog(log, operatorName) {
634
+ if (log == null || typeof log !== "object") throw _ctErr("ct/bad-log-list", "a CT log entry is not an object");
635
+ if (typeof log.key !== "string" || typeof log.log_id !== "string") throw _ctErr("ct/bad-log-list", "a CT log entry is missing its key or log_id");
636
+ var spki = guard.encoding.base64(log.key, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/bad-log-list", "the CT log key");
637
+ var statedId = guard.encoding.base64(log.log_id, 64, _ctErr, "ct/bad-log-list", "the CT log id");
638
+ if (statedId.length !== 32) throw _ctErr("ct/bad-log-list", "a CT log_id must be 32 bytes (SHA-256), got " + statedId.length);
639
+ _spkiAlg(spki); // fail-fast: a non-SPKI / malformed key (or a forgeable RSA e < 3) throws ct/bad-input; the full EC-curve / RSA-size profile is enforced by verifySct at verify time
640
+ var logId = nodeCrypto.createHash("sha256").update(spki).digest();
641
+ if (!logId.equals(statedId)) throw _ctErr("ct/log-id-mismatch", "the CT log_id does not match SHA-256 of the log key (RFC 6962 sec. 3.2)");
642
+ return {
643
+ logId: logId, logIdHex: logId.toString("hex"), key: spki,
644
+ description: typeof log.description === "string" ? log.description : null,
645
+ url: typeof log.url === "string" ? log.url : (typeof log.submission_url === "string" ? log.submission_url : null),
646
+ mmd: typeof log.mmd === "number" ? log.mmd : null,
647
+ operator: operatorName, state: _parseLogState(log.state), temporalInterval: _parseTemporalInterval(log.temporal_interval),
648
+ trusted: false, // set below from the state (usable/qualified/readonly), so a consumer reads a plain bool
649
+ };
650
+ }
651
+
652
+ // Two entries for one recomputed log-id must be byte-identical (collapse) or the list is inconsistent.
653
+ function _sameTemporal(a, b) {
654
+ if (a == null && b == null) return true;
655
+ // Coverage residual: the || guard is exercised across null/interval, interval/null, and both-interval
656
+ // inputs; the remaining c8 sub-arm is a short-circuit path (when `a == null` is true, `b == null` is
657
+ // never evaluated) with no additional reachable input.
658
+ if (a == null || b == null) return false;
659
+ return a.startInclusive.getTime() === b.startInclusive.getTime() && a.endExclusive.getTime() === b.endExclusive.getTime();
660
+ }
661
+ function _logsAgree(a, b) {
662
+ return a.state.name === b.state.name && a.state.since.getTime() === b.state.since.getTime() && _sameTemporal(a.temporalInterval, b.temporalInterval);
663
+ }
664
+
665
+ /**
666
+ * @primitive pki.ct.parseLogList
667
+ * @signature pki.ct.parseLogList(json, opts?) -> { logs, byLogId }
668
+ * @since 0.2.28
669
+ * @status experimental
670
+ * @spec RFC 6962
671
+ * @related pki.ct.verifySctWithLogList, pki.ct.verifySct
672
+ *
673
+ * Ingest a Certificate Transparency log-list JSON document (the `log_list.json` browsers consume) into a
674
+ * set of constraint-carrying trusted logs, keyed by log-id. `json` is a Buffer or string -- the caller
675
+ * supplies the already-fetched, already-authenticated bytes (offline, no network fetch). Parsing routes
676
+ * through the bounded, duplicate-member-rejecting JSON reader; for each log it base64-decodes the `key`
677
+ * to its DER SubjectPublicKeyInfo, validates it as a well-formed on-profile key, **recomputes**
678
+ * `SHA-256(SPKI)` and fail-closed **requires** it equal the stated `log_id` (RFC 6962 sec. 3.2 -- a log
679
+ * whose stated id disagrees with its key is refused as `ct/log-id-mismatch`), and decodes the `state`
680
+ * (exactly one of pending/qualified/usable/readonly/retired/rejected) and `temporal_interval`. Returns
681
+ * `{ logs, byLogId }` where each log is `{ logId, logIdHex, key, description, url, mmd, operator, state:
682
+ * { name, since, trusted, conditional }, temporalInterval, trusted }` and `byLogId` is a null-proto
683
+ * `{ logIdHex: log }` map. Every malformed / oversized / mis-bound input is a typed `CtError`.
684
+ *
685
+ * @example
686
+ * var logList = pki.ct.parseLogList(logListJsonBytes);
687
+ * logList.logs[0].trusted; // was the first log trusted (usable/qualified/readonly)?
688
+ */
689
+ function parseLogList(json, opts) {
690
+ void opts;
691
+ var doc = guard.json.parse(json, _ctErr, {
692
+ maxBytes: C.LIMITS.CT_LOG_LIST_MAX_BYTES, maxDepth: C.LIMITS.JSON_MAX_DEPTH,
693
+ badJson: "ct/bad-json", tooDeep: "ct/too-deep", duplicateMember: "ct/duplicate-member",
694
+ tooLarge: "ct/too-large", badInput: "ct/bad-input", label: "the CT log list",
695
+ });
696
+ if (doc == null || typeof doc !== "object" || !Array.isArray(doc.operators)) throw _ctErr("ct/bad-log-list", "the CT log list must be a JSON object with an operators array");
697
+ var logs = [], byLogId = Object.create(null);
698
+ for (var i = 0; i < doc.operators.length; i++) {
699
+ var op = doc.operators[i];
700
+ if (op == null || typeof op !== "object" || typeof op.name !== "string") throw _ctErr("ct/bad-log-list", "a CT log-list operator is missing its name");
701
+ var arrays = [op.logs, op.tiled_logs];
702
+ for (var a = 0; a < arrays.length; a++) {
703
+ var arr = arrays[a];
704
+ if (arr == null) continue;
705
+ if (!Array.isArray(arr)) throw _ctErr("ct/bad-log-list", "a CT log-list operator's logs / tiled_logs must be an array");
706
+ for (var j = 0; j < arr.length; j++) {
707
+ var rec = _parseLog(arr[j], op.name);
708
+ rec.trusted = rec.state.trusted; // usable/qualified/readonly; retired stays false (conditional at verify)
709
+ var prev = byLogId[rec.logIdHex];
710
+ if (prev) {
711
+ if (!_logsAgree(prev, rec)) throw _ctErr("ct/duplicate-log", "two CT log entries share log-id " + rec.logIdHex + " but disagree");
712
+ continue; // a byte-identical duplicate collapses to the one already recorded
713
+ }
714
+ byLogId[rec.logIdHex] = rec;
715
+ logs.push(rec);
716
+ }
717
+ }
718
+ }
719
+ return { logs: logs, byLogId: byLogId };
720
+ }
721
+
722
+ // The covered certificate's notAfter for the temporal gate: an explicit opts.certNotAfter, else derived
723
+ // from a leafCert (entryType 0) via the shipped x509 parser, else null (a precert TBS can't be parsed).
724
+ function _resolveNotAfter(entry, opts) {
725
+ if (opts.certNotAfter instanceof Date) return opts.certNotAfter;
726
+ if (entry && entry.entryType === 0 && entry.leafCert != null) {
727
+ // Inline require (circular-load): ct.js is loaded before schema-x509 finishes initializing in the
728
+ // index chain (schema-x509 -> schema-pkix is mid-init when ct.js is first required), so a top-level
729
+ // require would see a half-built pkix. Deferred to first use, when every module is initialized.
730
+ var x509 = require("./schema-x509.js"); // allow:inline-require -- circular load with schema-x509 -> schema-pkix (see note above)
731
+ try {
732
+ return x509.parse(_toBuffer(entry.leafCert, "entry.leafCert")).validity.notAfter;
733
+ } catch (_e) {
734
+ return null; // a malformed leafCert -> no resolvable notAfter -> fail-closed ct/temporal-interval at the caller
735
+ }
736
+ }
737
+ return null;
738
+ }
739
+
740
+ /**
741
+ * @primitive pki.ct.verifySctWithLogList
742
+ * @signature pki.ct.verifySctWithLogList(entry, sct, logList, opts?) -> Promise<boolean>
743
+ * @since 0.2.28
744
+ * @status experimental
745
+ * @spec RFC 6962
746
+ * @related pki.ct.parseLogList, pki.ct.verifySct
747
+ *
748
+ * Resolve the trusted CT log for an SCT and verify it in one step. `logList` is a `parseLogList` result;
749
+ * the log is resolved by `sct.logIdHex` (an unknown log is `ct/log-not-found`). The log's **state** gates
750
+ * trust (usable/qualified/readonly proceed; a retired log proceeds only for an SCT timestamped before its
751
+ * retirement instant; pending/rejected are `ct/log-untrusted`); its **temporal_interval** gates the
752
+ * covered certificate (the cert's `notAfter` -- from `entry.leafCert` when `entryType` is 0, or
753
+ * `opts.certNotAfter` -- must fall in `[start_inclusive, end_exclusive)`, and a windowed log with no
754
+ * resolvable notAfter is `ct/temporal-interval`, never silently skipped). Then the crypto is delegated to
755
+ * the shipped `verifySct(entry, sct, log.key)` (which independently re-checks `logId == SHA-256(key)`).
756
+ * Resolves `true` for a valid signature from a trusted, in-window log; `false` on a cryptographic
757
+ * mismatch (a verdict); throws a typed `CtError` on any structural / trust failure.
758
+ *
759
+ * @opts certNotAfter A `Date` -- the covered certificate's notAfter for the temporal-interval gate (required for a precert entry).
760
+ * @example
761
+ * var ok = await pki.ct.verifySctWithLogList(sctEntry, embeddedSct, logList);
762
+ */
763
+ async function verifySctWithLogList(entry, sct, logList, opts) {
764
+ opts = opts || {};
765
+ if (logList == null || typeof logList !== "object" || logList.byLogId == null) throw _ctErr("ct/bad-input", "logList must be a pki.ct.parseLogList result");
766
+ if (sct == null || typeof sct !== "object" || typeof sct.logIdHex !== "string") throw _ctErr("ct/bad-input", "the SCT is missing its logIdHex");
767
+ var log = logList.byLogId[sct.logIdHex];
768
+ if (log == null) throw _ctErr("ct/log-not-found", "no trusted CT log matches the SCT's logId " + sct.logIdHex);
769
+ // State gate (fail-closed): trusted proceed; retired only before its retirement; else untrusted.
770
+ if (!log.state.trusted) {
771
+ if (!log.state.conditional) throw _ctErr("ct/log-untrusted", "the CT log state '" + log.state.name + "' is not trusted");
772
+ var ts = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp");
773
+ if (ts >= BigInt(log.state.since.getTime())) throw _ctErr("ct/log-untrusted", "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")");
774
+ }
775
+ // Temporal gate (fail-closed): the covered cert's notAfter must be in [start_inclusive, end_exclusive).
776
+ if (log.temporalInterval) {
777
+ var notAfter = _resolveNotAfter(entry, opts);
778
+ // An Invalid Date (getTime() === NaN) is still `instanceof Date`, and NaN < x / NaN >= x are both
779
+ // false, so it would silently BYPASS the window containment -- reject it fail-closed, exactly like
780
+ // the codec's readTime rejects a NaN instant (a caller may pass a lenient `new Date(badString)`).
781
+ if (!(notAfter instanceof Date) || isNaN(notAfter.getTime())) throw _ctErr("ct/temporal-interval", "the CT log has a temporal_interval but the covered certificate's notAfter is not available or not a valid date (pass a valid opts.certNotAfter)");
782
+ var t = notAfter.getTime();
783
+ if (t < log.temporalInterval.startInclusive.getTime() || t >= log.temporalInterval.endExclusive.getTime()) {
784
+ throw _ctErr("ct/temporal-interval", "the covered certificate's notAfter is outside the CT log's temporal_interval");
785
+ }
786
+ }
787
+ return verifySct(entry, sct, log.key); // the shipped crypto verdict (re-checks the logId binding)
788
+ }
789
+
595
790
  module.exports = {
596
791
  parseSctList: parseSctList,
597
792
  reconstructSignedData: reconstructSignedData,
598
793
  verifySct: verifySct,
599
794
  encodeSctList: encodeSctList,
600
795
  signSct: signSct,
796
+ parseLogList: parseLogList,
797
+ verifySctWithLogList: verifySctWithLogList,
601
798
  HASH_ALGORITHMS: HASH_ALGORITHMS,
602
799
  SIGNATURE_ALGORITHMS: SIGNATURE_ALGORITHMS,
603
800
  };
package/lib/oid.js CHANGED
@@ -204,8 +204,9 @@ var FAMILIES = {
204
204
  hmacWithSHA384: 10, hmacWithSHA512: 11 } },
205
205
 
206
206
  // S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
207
- // authData is RFC 5652 sec. 9 AuthenticatedData; authEnvelopedData is RFC 5083.
208
- smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4, authEnvelopedData: 23, encKeyWithID: 21 } },
207
+ // authData is RFC 5652 sec. 9 AuthenticatedData; authEnvelopedData is RFC 5083;
208
+ // compressedData is RFC 3274 CompressedData.
209
+ smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4, compressedData: 9, encKeyWithID: 21, authEnvelopedData: 23 } },
209
210
 
210
211
  // S/MIME other-recipient-info types (id-ori, RFC 5652 sec. 6.2.5). id-ori-kem
211
212
  // carries a KEMRecipientInfo (RFC 9629) inside the ori [4] RecipientInfo arm.
@@ -215,7 +216,7 @@ var FAMILIES = {
215
216
  // CEK-HKDF content-encryption wrapper (RFC 9709) a KEMRecipientInfo names, plus
216
217
  // the RSA-KEM SPKI algorithm (RFC 9690). Parameters are absent for the HKDFs.
217
218
  smimeAlg: { base: [1, 2, 840, 113549, 1, 9, 16, 3], of: {
218
- "id-alg-PWRI-KEK": 9,
219
+ "id-alg-zlibCompress": 8, "id-alg-PWRI-KEK": 9,
219
220
  "id-rsa-kem": 14, "id-alg-hss-lms-hashsig": 17,
220
221
  // RFC 8418 X25519/X448 key-agreement schemes (HKDF-based).
221
222
  "dhSinglePass-stdDH-hkdf-sha256-scheme": 19, "dhSinglePass-stdDH-hkdf-sha384-scheme": 20,
package/lib/rfc3339.js ADDED
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @internal
6
+ * lib/rfc3339.js -- RFC 3339 date-time parsing + validation, a fail-closed engine primitive shared by
7
+ * pki.acme (a boolean field-validator over server-supplied JSON) and pki.ct (parse a CT log-list
8
+ * timestamp / temporal-interval bound to a comparable Date). Beyond the grammar it enforces CALENDAR
9
+ * validity -- a syntactically well-formed but impossible instant (month 13, February 30, hour 25, a
10
+ * :60 leap second, a +25:00 offset) is rejected, so a downstream expiry / window comparison never runs
11
+ * on a value JS `Date` would silently roll over or `Date.parse` would NaN. Like the byte-reader / guard
12
+ * family, `parse` takes the caller's `(code, message) -> error` factory so every consumer keeps its own
13
+ * domain fault code.
14
+ */
15
+
16
+ // date "T" time with a zone (Z or a numeric offset); an optional fractional second.
17
+ var RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
18
+
19
+ // isValid(v) -> boolean: v is a syntactically well-formed AND calendar-valid RFC 3339 date-time string.
20
+ function isValid(v) {
21
+ if (typeof v !== "string") return false;
22
+ var m = RFC3339_RE.exec(v);
23
+ if (!m) return false;
24
+ var year = +m[1], month = +m[2], day = +m[3], hour = +m[4], min = +m[5], sec = +m[6];
25
+ if (month < 1 || month > 12) return false;
26
+ // Reject a :60 leap second (Node's Date.parse returns NaN for it, so a comparison on such a value
27
+ // would silently pass) and any hour/minute/second out of range.
28
+ if (hour > 23 || min > 59 || sec > 59) return false;
29
+ var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
30
+ var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
31
+ if (day < 1 || day > daysInMonth[month - 1]) return false;
32
+ if (m[7]) { if (+m[8] > 23 || +m[9] > 59) return false; } // a numeric zone offset must itself be valid
33
+ return true;
34
+ }
35
+
36
+ // parse(v, E, code, label) -> Date: a calendar-valid RFC 3339 date-time as a comparable Date; otherwise
37
+ // throws E(code, message) (the caller's (code, message) factory). isValid guarantees `new Date(v)` is a
38
+ // real instant (never NaN), so a returned Date is always safe to compare.
39
+ function parse(v, E, code, label) {
40
+ if (!isValid(v)) throw E(code, (label || "the value") + " is not a valid RFC 3339 date-time");
41
+ return new Date(v);
42
+ }
43
+
44
+ module.exports = { isValid: isValid, parse: parse };
package/lib/schema-cms.js CHANGED
@@ -76,6 +76,7 @@ var PWRI_VERSION = pkix.versionReader(NS, { "0": 0 });
76
76
  // AuthEnvelopedData version (RFC 5083 sec. 2.1) and the KEMRecipientInfo version
77
77
  // (RFC 9629 sec. 3) are both a fixed 0.
78
78
  var AUTHDATA_VERSION = pkix.versionReader(NS, { "0": 0, "1": 1, "3": 3 });
79
+ var COMPRESSED_DATA_VERSION = pkix.versionReader(NS, { "0": 0 }); // RFC 3274 sec. 1.1 -- version MUST be 0
79
80
  var AUTHENV_VERSION = pkix.versionReader(NS, { "0": 0 });
80
81
  var KEMRI_VERSION = pkix.versionReader(NS, { "0": 0 });
81
82
 
@@ -88,6 +89,7 @@ var OID_ENVELOPED_DATA = oid.byName("envelopedData");
88
89
  var OID_ENCRYPTED_DATA = oid.byName("encryptedData");
89
90
  var OID_AUTH_DATA = oid.byName("authData"); // RFC 5652 sec. 9
90
91
  var OID_AUTH_ENVELOPED_DATA = oid.byName("authEnvelopedData"); // RFC 5083
92
+ var OID_COMPRESSED_DATA = oid.byName("compressedData"); // RFC 3274
91
93
  var OID_DATA = oid.byName("data");
92
94
  var OID_ORI_KEM = oid.byName("kem"); // id-ori-kem (RFC 9629)
93
95
 
@@ -1047,6 +1049,28 @@ var AUTH_ENVELOPED_DATA = schema.seq([
1047
1049
  // SIGNED_DATA; id-envelopedData / id-encryptedData / id-ct-authData /
1048
1050
  // id-ct-authEnvelopedData walk their schemas; the other PKCS#7 types are
1049
1051
  // recognized-and-deferred; unknown OIDs are rejected.
1052
+ // CompressedData ::= SEQUENCE { version CMSVersion (0), compressionAlgorithm
1053
+ // CompressionAlgorithmIdentifier, encapContentInfo EncapsulatedContentInfo }
1054
+ // (RFC 3274 sec. 1.1). The eContent is an RFC 1950 ZLIB stream of the inner
1055
+ // content. Parse decodes the structure and surfaces the raw compressionAlgorithm
1056
+ // + eContent; the zlib-only algorithm check + the bounded inflate live in the
1057
+ // pki.cms.decompress consuming operation, not the parser (mirroring how the
1058
+ // content-encryption algorithm is validated by decrypt, not the envelope parse).
1059
+ var COMPRESSED_DATA = schema.seq([
1060
+ schema.field("version", COMPRESSED_DATA_VERSION),
1061
+ schema.field("compressionAlgorithm", ALGORITHM_IDENTIFIER),
1062
+ schema.field("encapContentInfo", ENCAP_CONTENT_INFO),
1063
+ ], {
1064
+ assert: "sequence", code: "cms/bad-compressed-data", what: "CompressedData",
1065
+ build: function (m) {
1066
+ return {
1067
+ version: m.fields.version.value,
1068
+ compressionAlgorithm: m.fields.compressionAlgorithm.value.result,
1069
+ encapContentInfo: m.fields.encapContentInfo.value.result,
1070
+ };
1071
+ },
1072
+ });
1073
+
1050
1074
  var CONTENT_INFO = schema.seq([
1051
1075
  schema.field("contentType", schema.oidLeaf()),
1052
1076
  schema.field("content", schema.explicit(0, schema.any(), { code: "cms/not-a-content-info" })),
@@ -1060,6 +1084,7 @@ var CONTENT_INFO = schema.seq([
1060
1084
  else if (ct === OID_ENCRYPTED_DATA) inner = ENCRYPTED_DATA;
1061
1085
  else if (ct === OID_AUTH_DATA) inner = AUTHENTICATED_DATA;
1062
1086
  else if (ct === OID_AUTH_ENVELOPED_DATA) inner = AUTH_ENVELOPED_DATA;
1087
+ else if (ct === OID_COMPRESSED_DATA) inner = COMPRESSED_DATA;
1063
1088
  if (inner === null) {
1064
1089
  if (DEFERRED.has(ct)) {
1065
1090
  // Coverage residual -- reached only when the OID is a registry entry (the guard above
package/lib/smime.js CHANGED
@@ -240,9 +240,10 @@ async function verify(message, opts) {
240
240
  throw _err("smime/unsupported-type", "not a signed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
241
241
  }
242
242
 
243
- // The application/pkcs7-mime header for an enveloped S/MIME message (RFC 8551 sec. 3.2 / sec. 3.2.2).
244
- function _pkcs7MimeHead(smimeType) {
245
- return Buffer.from("Content-Type: application/pkcs7-mime; smime-type=" + smimeType + "; name=smime.p7m\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=smime.p7m\r\n\r\n", "latin1");
243
+ // The application/pkcs7-mime header for an opaque S/MIME message (RFC 8551 sec. 3.2 / sec. 3.2.2). `name`
244
+ // is the smime.p7m (enveloped) / smime.p7z (compressed) file name for the media type + Content-Disposition.
245
+ function _pkcs7MimeHead(smimeType, name) {
246
+ return Buffer.from("Content-Type: application/pkcs7-mime; smime-type=" + smimeType + "; name=" + name + "\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=" + name + "\r\n\r\n", "latin1");
246
247
  }
247
248
 
248
249
  // The smime-type for a CMS enveloped body, DERIVED from its OUTER content type (RFC 8551 sec. 3.2.2) --
@@ -307,7 +308,7 @@ async function encrypt(content, recipients, opts) {
307
308
  // RecipientInfos), never bare EncryptedData -- which is not an S/MIME construct (no smime-type maps to it).
308
309
  var recips = Array.isArray(recipients) ? recipients : [recipients];
309
310
  var der = _toBuf(await cms.encrypt(entity, recips, _cmsEncryptOpts(opts)));
310
- var head = _pkcs7MimeHead(_envelopedTypeOf(der, "smime/bad-mime"));
311
+ var head = _pkcs7MimeHead(_envelopedTypeOf(der, "smime/bad-mime"), "smime.p7m");
311
312
  return _capped(Buffer.concat([head, _base64Body(der), mime.CRLF]));
312
313
  }
313
314
 
@@ -362,6 +363,75 @@ async function decrypt(message, keyMaterial, opts) {
362
363
  };
363
364
  }
364
365
 
366
+ /**
367
+ * @primitive pki.smime.compress
368
+ * @signature pki.smime.compress(content, opts?) -> Promise<Buffer>
369
+ * @since 0.2.27
370
+ * @status experimental
371
+ * @spec RFC 8551, RFC 3274
372
+ * @related pki.smime.decompress, pki.cms.compress
373
+ *
374
+ * Compress a MIME entity as an opaque compressed S/MIME message (RFC 8551 sec. 3.6). `content` is the
375
+ * payload -- a raw body wrapped as a `text/plain` entity by default, or the caller's own complete MIME
376
+ * entity when `opts.entity` is set. The entity is canonicalized (RFC 8551 sec. 3.1) and ZLIB-compressed
377
+ * into a CMS `CompressedData` (`pki.cms.compress`), carried opaque in one `application/pkcs7-mime;
378
+ * smime-type=compressed-data; name=smime.p7z` entity (base64). Compression is a size transform with NO
379
+ * integrity, confidentiality, or authentication (RFC 8551 sec. 2.4.5) -- sign or encrypt the result if
380
+ * you need protection. Returns the assembled message bytes; fail-closed with `SmimeError`.
381
+ *
382
+ * @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
383
+ * @opts contentType the wrapped entity's MIME Content-Type (default `text/plain; charset=utf-8`).
384
+ * @opts level forwarded to cms.compress: the DEFLATE compression level (an integer).
385
+ * @example
386
+ * var z = await pki.smime.compress(Buffer.from("compress this message"));
387
+ */
388
+ async function compress(content, opts) {
389
+ opts = opts || {};
390
+ var entity = _entityBytes(content, opts);
391
+ var cOpts = {};
392
+ if (opts.level !== undefined) cOpts.level = opts.level;
393
+ var der = _toBuf(await cms.compress(entity, cOpts));
394
+ var head = _pkcs7MimeHead("compressed-data", "smime.p7z");
395
+ return _capped(Buffer.concat([head, _base64Body(der), mime.CRLF]));
396
+ }
397
+
398
+ /**
399
+ * @primitive pki.smime.decompress
400
+ * @signature pki.smime.decompress(message, opts?) -> Promise<{ content, contentType, contentTypeName, compressionAlgorithm }>
401
+ * @since 0.2.27
402
+ * @status experimental
403
+ * @spec RFC 8551, RFC 3274
404
+ * @related pki.smime.compress, pki.cms.decompress, pki.smime.verify, pki.smime.decrypt
405
+ *
406
+ * Decompress a compressed S/MIME message (RFC 8551 sec. 3.6) -- an `application/pkcs7-mime` entity whose
407
+ * base64 body is a CMS `CompressedData`. Returns the recovered inner MIME entity as `content` plus the
408
+ * inner `contentType` / `contentTypeName` and the `compressionAlgorithm`. The inflate is BOUNDED (a
409
+ * decompression-bomb defense, `cms/decompress-too-large`; `opts.maxOutputBytes` tightens it downward).
410
+ * The verdict carries NO `authenticated` / `valid` field -- CompressedData is not a security assertion
411
+ * (RFC 8551 sec. 2.4.5). A recovered content that is itself a signed or enveloped S/MIME message is
412
+ * returned as-is for the caller to feed back to `pki.smime.verify` / `pki.smime.decrypt` (no
413
+ * auto-recursion). Accepts OpenSSL's legacy `application/x-pkcs7-mime` and a missing `smime-type`.
414
+ *
415
+ * @opts maxOutputBytes forwarded to cms.decompress: lower the decompressed-output cap (a DoS bound; downward only).
416
+ * @example
417
+ * var res = await pki.smime.decompress(compressedSmimeBytes);
418
+ * res.content; // the recovered inner MIME entity
419
+ */
420
+ async function decompress(message, opts) {
421
+ opts = opts || {};
422
+ var ent = mime.parse(message, SmimeError, "smime/bad-mime");
423
+ var ct = ent.contentType;
424
+ if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
425
+ // The smime-type header is advisory (the CMS body is authoritative) -- compare case-insensitively.
426
+ var st = ct.params["smime-type"];
427
+ if (st && st.toLowerCase() !== "compressed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only compressed-data)");
428
+ var der = _decodeCms(ent);
429
+ var dOpts = {};
430
+ if (opts.maxOutputBytes !== undefined) dOpts.maxOutputBytes = opts.maxOutputBytes;
431
+ var res = await cms.decompress(der, dOpts);
432
+ return { content: res.content, contentType: res.contentType, contentTypeName: res.contentTypeName, compressionAlgorithm: res.compressionAlgorithm };
433
+ }
434
+
365
435
  // Decode a CMS body per its Content-Transfer-Encoding (base64 is the S/MIME norm; 7bit/8bit/binary pass through).
366
436
  function _decodeCms(ent) {
367
437
  // guard.encoding.base64 is strict (no whitespace) and rejects via a (code,msg) FACTORY -- strip the
@@ -372,4 +442,4 @@ function _decodeCms(ent) {
372
442
 
373
443
  function _toBuf(v) { return Buffer.isBuffer(v) ? v : Buffer.from(v); }
374
444
 
375
- module.exports = { sign: sign, verify: verify, encrypt: encrypt, decrypt: decrypt };
445
+ module.exports = { sign: sign, verify: verify, encrypt: encrypt, decrypt: decrypt, compress: compress, decompress: decompress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
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:f346b9f1-fcfc-449a-bd4f-4e1dfeb3209c",
5
+ "serialNumber": "urn:uuid:517b8e6e-813e-4f65-9d63-651831e57f82",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-16T12:38:31.773Z",
8
+ "timestamp": "2026-07-16T14:06:47.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.2.26",
22
+ "bom-ref": "@blamejs/pki@0.2.28",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.26",
25
+ "version": "0.2.28",
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.2.26",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.28",
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.2.26",
57
+ "ref": "@blamejs/pki@0.2.28",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]