@blamejs/pki 0.2.20 → 0.2.22
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 +24 -0
- package/README.md +2 -1
- package/index.js +2 -0
- package/lib/cms-sign.js +7 -218
- package/lib/cms-verify.js +3 -2
- package/lib/lint.js +43 -2
- package/lib/ocsp-verify.js +226 -0
- package/lib/ocsp.js +378 -0
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +131 -197
- package/lib/schema-pkix.js +8 -0
- package/lib/sign-scheme.js +206 -0
- package/lib/webcrypto.js +112 -10
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ 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.22 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
The RFC 6960 OCSP producer and relying-party surface arrives as pki.ocsp: build and sign OCSP requests and responses, mint unsigned error responses, and verify a response end to end against the same hardened responder-authorization, signature, CertID, currency, and nonce gates the certification-path revocation checker runs.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.ocsp is the RFC 6960 OCSP request/response surface. pki.ocsp.buildRequest(query, opts) builds an OCSPRequest (one or many { cert, issuer } queries; CertID under SHA-1 or SHA-2; optional RFC 9654 nonce; optional requestor signature; the RFC 5019 lightweight profile). pki.ocsp.sign(responseData, responder, opts) signs a BasicOCSPResponse over the exact ResponseData DER for the issuing CA or a delegated responder, with good / revoked / unknown per-certificate status and any pki.cms.sign signature algorithm (RSA, RSASSA-PSS, ECDSA, EdDSA, the post-quantum ML-DSA and SLH-DSA sets). pki.ocsp.buildErrorResponse(status) mints the unsigned section 2.3 error response. Transport-free; malformed input throws a typed OcspError.
|
|
14
|
+
- pki.ocsp.verify(response, opts) verifies an OCSP response as a relying party, fail-closed: it binds the supplied issuer certificate to the target certificate (the target's issuer name must equal the issuer's subject and the target's signature must verify under the issuer's key), recomputes the CertID under its own hash algorithm to bind the checked certificate to its issuer, requires an authorized responder (the issuing CA, or a CA-issued delegate bearing id-kp-OCSPSigning and id-pkix-ocsp-nocheck that passes the full out-of-path certificate gates), verifies the signature over tbsResponseDataBytes, enforces thisUpdate / nextUpdate currency, and binds the request nonce (RFC 9654) under a constant-time comparison. A revoked status shadows good within a response; an unbound issuer, an unauthorized responder, a mismatched CertID, or a stale or unverifiable response returns { status: "unknown" } with granular responderAuthorized / signatureValid / matched flags -- never a silent accept. It runs the exact responder-authorization, signature, and currency gates pki.path.ocspChecker runs, through one shared core.
|
|
15
|
+
- pki.path.verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts) is the lower-level primitive pki.ocsp.verify composes: it verifies a single already-parsed OCSP response for one certificate against its already-parsed issuer, returning the same fail-closed granular verdict, for callers that have already decoded their inputs.
|
|
16
|
+
|
|
17
|
+
## v0.2.21 — 2026-07-15
|
|
18
|
+
|
|
19
|
+
ML-KEM public keys in X.509 certificates and PKCS#8 private keys (RFC 9935 / FIPS 203) become a first-class, fail-closed surface: certification-path validation enforces the keyEncipherment-only key usage, key import validates the private-key CHOICE and rejects an inconsistent key with a typed error, and pki.lint gains the RFC 9935 certificate rows.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- pki.path.validate enforces the RFC 9935 section 5 key-usage rule for ML-KEM (id-ml-kem-512/768/1024) certificates: a present keyUsage MUST assert keyEncipherment as the only bit. A leaf with digitalSignature, an ML-KEM key with keyCertSign / cRLSign / keyAgreement / dataEncipherment / nonRepudiation, or keyEncipherment set alongside any reserved bit, fails closed with the frozen code path/kem-key-usage -- for the target and every intermediate whose own subject key is ML-KEM. An absent keyUsage places no restriction (RFC 5280 section 4.2.1.3).
|
|
24
|
+
- pki.lint gains an rfc9935 profile: lint/rfc9935/kem-key-usage (the section 5 keyEncipherment-only rule) and lint/rfc9935/kem-key-length (the SPKI encapsulation key must be exactly 800 / 1184 / 1568 octets for the id-ml-kem-512 / 768 / 1024 OID -- the OID is the sole authority for the parameter set, so an OCTET-STRING-wrapped or wrong-set key is flagged). Both run by default and are silent on a non-ML-KEM certificate.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- pki.webcrypto.subtle.importKey no longer surfaces a raw engine exception when handed a malformed or inconsistent key: a bad SPKI, a bad JWK, or an ML-KEM private key whose seed and expanded halves are inconsistent (FIPS 203 section 7.3) now fails closed with a typed webcrypto/data error. For an ML-KEM PKCS#8, the RFC 9935 section 6 private-key CHOICE is validated by its DER tag and exact size for the algorithm OID before the engine imports it -- the OpenSSL-legacy bare-seed, bare-expanded-key, and concatenated layouts the engine would otherwise accept are rejected, so a non-conformant private key cannot be imported under an ML-KEM name.
|
|
29
|
+
- pki.webcrypto.subtle.importKey now fails closed on two further malformed-input classes: a JWK import whose key data is not an object (null, a primitive, or an array -- including the JSON null an unwrap over non-authenticating ciphertext can yield) returns a typed webcrypto/data error instead of a raw TypeError; and an SPKI or PKCS#8 import requested under a secret-key or key-derivation algorithm name (AES-GCM/CBC/CTR/KW, HMAC, HKDF, PBKDF2) is rejected as webcrypto/not-supported rather than importing a mislabeled key handle.
|
|
30
|
+
|
|
7
31
|
## v0.2.20 — 2026-07-15
|
|
8
32
|
|
|
9
33
|
A WebAuthn attestation object whose attestation statement is not a CBOR map is now rejected with a typed webauthn/bad-attestation-object at parse instead of surfacing an untyped error from a format verifier, and the strict CBOR codec gains pki.cbor.read.mapGet -- a keyed map lookup that asserts the map's major type inside the accessor.
|
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ is callable today; nothing below is a stub.
|
|
|
201
201
|
| `pki.asn1` | Strict, bounded DER codec — `decode` (zero-copy node tree), `encode`, `build.*` canonical-DER value builders, `read.*` typed readers, `TAGS`, OID-content encode/decode |
|
|
202
202
|
| `pki.cbor` | Strict, bounded RFC 8949 deterministic CBOR codec — `decode` (zero-copy node tree) + `read.*` typed leaf readers incl. the keyed map lookup `read.mapGet` (text or COSE-label integer key, the map's major type asserted in the accessor), fail-closed on every non-canonical shape (indefinite length, non-minimal argument, unsorted / duplicate map keys, non-shortest float, trailing bytes) |
|
|
203
203
|
| `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
|
|
204
|
-
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation (KEM encapsulation
|
|
204
|
+
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
|
|
205
205
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
206
206
|
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
|
|
207
207
|
| `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`, `pemEncode` |
|
|
@@ -223,6 +223,7 @@ is callable today; nothing below is a stub.
|
|
|
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
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`. Fail-closed with typed `cms/*` errors — `sign`, `verify` |
|
|
225
225
|
| `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` |
|
|
226
|
+
| `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` |
|
|
226
227
|
| `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. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct` |
|
|
227
228
|
| `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` |
|
|
228
229
|
| `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` |
|
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var path = require("./lib/path-validate");
|
|
|
38
38
|
var ct = require("./lib/ct");
|
|
39
39
|
var cms = require("./lib/cms-verify");
|
|
40
40
|
var tsp = require("./lib/tsp-sign");
|
|
41
|
+
var ocsp = require("./lib/ocsp");
|
|
41
42
|
var merkle = require("./lib/merkle");
|
|
42
43
|
var shbs = require("./lib/shbs");
|
|
43
44
|
var hpke = require("./lib/hpke");
|
|
@@ -77,6 +78,7 @@ module.exports = {
|
|
|
77
78
|
ct: ct,
|
|
78
79
|
cms: cms,
|
|
79
80
|
tsp: tsp,
|
|
81
|
+
ocsp: ocsp,
|
|
80
82
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
81
83
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
82
84
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/cms-sign.js
CHANGED
|
@@ -17,101 +17,25 @@ var nodeCrypto = require("crypto");
|
|
|
17
17
|
var asn1 = require("./asn1-der");
|
|
18
18
|
var oid = require("./oid");
|
|
19
19
|
var x509 = require("./schema-x509");
|
|
20
|
-
var pkcs8 = require("./schema-pkcs8");
|
|
21
20
|
var pkix = require("./schema-pkix");
|
|
22
|
-
var webcrypto = require("./webcrypto");
|
|
23
|
-
var subtle = webcrypto.webcrypto.subtle;
|
|
24
|
-
var validator = require("./validator-all");
|
|
25
|
-
var compositeSig = require("./composite-sig");
|
|
26
21
|
var frameworkError = require("./framework-error");
|
|
27
22
|
|
|
23
|
+
var signScheme = require("./sign-scheme");
|
|
28
24
|
var CmsError = frameworkError.CmsError;
|
|
29
25
|
var b = asn1.build;
|
|
30
26
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
27
|
+
// The domain error factory the shared sign-scheme resolver/signer throws through (kind ->
|
|
28
|
+
// cms/<kind>), so its faults keep the cms/* codes.
|
|
29
|
+
function _signE(kind, message, cause) { return new CmsError("cms/" + kind, message, cause); }
|
|
31
30
|
function O(name) { return oid.byName(name); }
|
|
32
31
|
|
|
33
|
-
// A digest-algorithm name -> the WebCrypto hash (sign path). SHAKE256 has no WebCrypto hash, so
|
|
34
|
-
// the message digest is always computed with node:crypto (below), uniform across the family.
|
|
35
|
-
var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
36
32
|
var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" };
|
|
37
33
|
var SHAKE_OUT = { shake128: 32, shake256: 64 }; // SHAKE output lengths: SHAKE128 256-bit (RFC 9814 sec. 4), SHAKE256 512-bit (RFC 8419 sec. 2.3 / RFC 9814)
|
|
38
|
-
var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
39
|
-
var ECDSA_ALG = { sha256: "ecdsaWithSHA256", sha384: "ecdsaWithSHA384", sha512: "ecdsaWithSHA512" };
|
|
40
|
-
var HASH_NAME_BY_OID = {};
|
|
41
|
-
HASH_NAME_BY_OID[O("sha256")] = "sha256";
|
|
42
|
-
HASH_NAME_BY_OID[O("sha384")] = "sha384";
|
|
43
|
-
HASH_NAME_BY_OID[O("sha512")] = "sha512";
|
|
44
|
-
var EC_BY_CURVE_OID = {};
|
|
45
|
-
EC_BY_CURVE_OID[O("prime256v1")] = { curve: "P-256", coordLen: 32 };
|
|
46
|
-
EC_BY_CURVE_OID[O("secp384r1")] = { curve: "P-384", coordLen: 48 };
|
|
47
|
-
EC_BY_CURVE_OID[O("secp521r1")] = { curve: "P-521", coordLen: 66 };
|
|
48
|
-
// ML-DSA (RFC 9882): the signer cert SPKI algorithm OID -> its WebCrypto name. Registry-keyed
|
|
49
|
-
// (Hard rule #2, no dotted-decimal literal). The same OID identifies both the key and the signature.
|
|
50
|
-
var MLDSA_BY_OID = {};
|
|
51
|
-
MLDSA_BY_OID[O("id-ml-dsa-44")] = "ML-DSA-44";
|
|
52
|
-
MLDSA_BY_OID[O("id-ml-dsa-65")] = "ML-DSA-65";
|
|
53
|
-
MLDSA_BY_OID[O("id-ml-dsa-87")] = "ML-DSA-87";
|
|
54
|
-
// The message-digest algorithms suitable for each ML-DSA parameter set (RFC 9882 sec. 3.3 /
|
|
55
|
-
// Table 1): a digest MUST offer at least the parameter set's collision strength (lambda) and at
|
|
56
|
-
// least 2*lambda bits of output. SHA-512 is suitable for all sets; SHAKE256 (512-bit output) too.
|
|
57
|
-
// Below-strength digests downgrade the signature to the weaker of the two (the whichever-is-lower
|
|
58
|
-
// rule), so they are refused fail-closed on sign and verify -- the RFC's "verifiers MAY reject".
|
|
59
|
-
var MLDSA_SUITABLE_DIGEST = {
|
|
60
|
-
"ML-DSA-44": { sha256: 1, sha384: 1, sha512: 1, shake256: 1 },
|
|
61
|
-
"ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
|
|
62
|
-
"ML-DSA-87": { sha512: 1, shake256: 1 },
|
|
63
|
-
};
|
|
64
|
-
// SLH-DSA (RFC 9814): the twelve pure FIPS 205 sets. The signer cert SPKI algorithm OID -> its
|
|
65
|
-
// WebCrypto set name ("SLH-DSA-"+SET, mirroring webcrypto's SLH_DSA_NODE / path-validate) + the
|
|
66
|
-
// RFC 9814 sec. 4 pinned message digest for that set. The sig and key share one OID; the digest is
|
|
67
|
-
// FIXED per set (no caller choice, unlike ML-DSA's suitable-set). Registry-keyed (Hard rule #2).
|
|
68
|
-
var SLHDSA_BY_OID = {};
|
|
69
|
-
[["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
|
|
70
|
-
["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
|
|
71
|
-
["shake-192s", "shake256"], ["shake-192f", "shake256"], ["shake-256s", "shake256"], ["shake-256f", "shake256"]
|
|
72
|
-
].forEach(function (r) { SLHDSA_BY_OID[O("id-slh-dsa-" + r[0])] = { wc: "SLH-DSA-" + r[0].toUpperCase(), digest: r[1] }; });
|
|
73
34
|
|
|
74
35
|
var OID_DATA = O("data");
|
|
75
36
|
var OID_SIGNED_DATA = O("signedData");
|
|
76
37
|
var OID_SKI = O("subjectKeyIdentifier");
|
|
77
38
|
|
|
78
|
-
// An AlgorithmIdentifier { OID } (absent parameters) or { OID, NULL }.
|
|
79
|
-
function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
|
|
80
|
-
// The RSASSA-PSS AlgorithmIdentifier with the params SEQUENCE cms.verify's _resolvePss accepts:
|
|
81
|
-
// an explicit SHA-2 hashAlgorithm, MGF1 keyed to the same hash, the hash-length saltLength, and
|
|
82
|
-
// the default trailerField (omitted). RFC 4055.
|
|
83
|
-
function _pssAlgId(digestName) {
|
|
84
|
-
var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
|
|
85
|
-
var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
|
|
86
|
-
var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
|
|
87
|
-
return b.sequence([b.oid(O("rsassaPss")), params]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// An id-RSASSA-PSS SubjectPublicKeyInfo MAY pin the permitted hash in its RSASSA-PSS-params
|
|
91
|
-
// (RFC 4055 sec. 1.2 / sec. 3.1): a key generated for SHA-384 cannot sign under SHA-256. Read the
|
|
92
|
-
// pinned digest name from the SPKI algorithm parameters so signing honors the key's restriction
|
|
93
|
-
// instead of hard-coding SHA-256. Absent params (an unrestricted key) or an unrecognized hash
|
|
94
|
-
// returns null -- the caller then falls back to its opts.digestAlgorithm or the SHA-256 default.
|
|
95
|
-
function _pssHashFromSpki(cert) {
|
|
96
|
-
var params = cert.subjectPublicKeyInfo.algorithm.parameters;
|
|
97
|
-
if (params == null) return null; // an unrestricted id-RSASSA-PSS key
|
|
98
|
-
// params is the raw parameters TLV the strict codec already validated when it parsed the cert,
|
|
99
|
-
// so re-decoding a single valid TLV cannot throw. Read the pinned hash structurally, defaulting
|
|
100
|
-
// to null (caller falls back) whenever the shape is not the expected RSASSA-PSS-params.
|
|
101
|
-
var node = asn1.decode(params);
|
|
102
|
-
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) return null;
|
|
103
|
-
var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
|
|
104
|
-
if (!hashField || !hashField.children || !hashField.children[0] || !hashField.children[0].children) return null;
|
|
105
|
-
var oidNode = hashField.children[0].children[0]; // [0] EXPLICIT AlgorithmIdentifier { hash OID, ... }
|
|
106
|
-
if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) return null;
|
|
107
|
-
var pinnedOid = asn1.read.oid(oidNode);
|
|
108
|
-
var name = HASH_NAME_BY_OID[pinnedOid];
|
|
109
|
-
// A recognized SHA-2 pin resolves to its name; an unsupported pinned hash (e.g. SHA-3) fails
|
|
110
|
-
// closed -- we cannot honor the key's restriction, so we never silently sign under a different
|
|
111
|
-
// digest the key forbids.
|
|
112
|
-
if (!name) throw _err("cms/unsupported-algorithm", "the id-RSASSA-PSS signer key pins an unsupported hash algorithm (" + pinnedOid + ")");
|
|
113
|
-
return name;
|
|
114
|
-
}
|
|
115
39
|
|
|
116
40
|
// The message digest of the content under the digest algorithm (SHA-2 or SHAKE256).
|
|
117
41
|
function _digest(digestName, content) {
|
|
@@ -121,83 +45,6 @@ function _digest(digestName, content) {
|
|
|
121
45
|
return h.update(content).digest();
|
|
122
46
|
}
|
|
123
47
|
|
|
124
|
-
// Resolve the sign scheme from the signer certificate's public-key algorithm + per-signer opts:
|
|
125
|
-
// the digest, the digestAlgorithm and signatureAlgorithm AlgorithmIdentifiers (with the exact
|
|
126
|
-
// parameter shape cms.verify requires), and the WebCrypto import + sign algorithms.
|
|
127
|
-
function _scheme(cert, so, noSignedAttrs) {
|
|
128
|
-
var alg = cert.subjectPublicKeyInfo.algorithm;
|
|
129
|
-
var keyOid = alg.oid;
|
|
130
|
-
var comp = compositeSig.COMPOSITE_ALGS[keyOid];
|
|
131
|
-
if (comp) {
|
|
132
|
-
// Composite ML-DSA (draft-ietf-lamps-cms-composite-sigs): the arm DICTATES the CMS
|
|
133
|
-
// digestAlgorithm (its pre-hash, Table 1) -- unlike RSA/ECDSA the digest is not a caller
|
|
134
|
-
// choice, so a conflicting so.digestAlgorithm is rejected. The 3 arms Node's WebCrypto surface
|
|
135
|
-
// cannot sign (brainpool curves; the SHAKE256/64 pre-hash) fail closed at config time -- never
|
|
136
|
-
// a partial single-component signature.
|
|
137
|
-
if (comp.trad.unsupported) throw _err("cms/unsupported-algorithm", "composite " + comp.name + ": " + comp.trad.unsupported);
|
|
138
|
-
if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw _err("cms/bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " + JSON.stringify(so.digestAlgorithm) + " conflicts");
|
|
139
|
-
return { composite: comp, digest: comp.phCms, digestAlgId: _algId(comp.phCms, "absent"), sigAlgId: _algId(comp.name, "absent") };
|
|
140
|
-
}
|
|
141
|
-
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
142
|
-
var isPssKey = keyOid === O("rsassaPss");
|
|
143
|
-
// An id-RSASSA-PSS key MAY pin its permitted hash in the SPKI params (RFC 4055 sec. 1.2): honor
|
|
144
|
-
// that over the SHA-256 default so Node/OpenSSL does not reject the otherwise-valid signer with
|
|
145
|
-
// ERR_OSSL_DIGEST_NOT_ALLOWED. An explicit digestAlgorithm that contradicts the pin is rejected
|
|
146
|
-
// fail-closed rather than silently signed under a digest the key forbids.
|
|
147
|
-
var pinned = isPssKey ? _pssHashFromSpki(cert) : null;
|
|
148
|
-
if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw _err("cms/bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " was requested");
|
|
149
|
-
var d = so.digestAlgorithm || pinned || "sha256";
|
|
150
|
-
if (!HASH[d]) throw _err("cms/unsupported-algorithm", "unsupported RSA digest algorithm " + JSON.stringify(d));
|
|
151
|
-
// A general rsaEncryption key signs PKCS#1 v1.5 by default, or RSASSA-PSS when opts.pss is set.
|
|
152
|
-
if (so.pss || isPssKey) return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _pssAlgId(d), imp: { name: "RSA-PSS", hash: HASH[d] }, sign: { name: "RSA-PSS", saltLength: PSS_SALT[HASH[d]] }, ecdsaDer: false };
|
|
153
|
-
return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _algId("rsaEncryption", "null"), imp: { name: "RSASSA-PKCS1-v1_5", hash: HASH[d] }, sign: { name: "RSASSA-PKCS1-v1_5" }, ecdsaDer: false };
|
|
154
|
-
}
|
|
155
|
-
if (keyOid === O("ecPublicKey")) {
|
|
156
|
-
var curveOid;
|
|
157
|
-
try { curveOid = asn1.read.oid(asn1.decode(alg.parameters)); }
|
|
158
|
-
catch (e) { throw _err("cms/unsupported-algorithm", "the signer EC key parameters are not a named-curve OID", e); }
|
|
159
|
-
var ec = EC_BY_CURVE_OID[curveOid];
|
|
160
|
-
if (!ec) throw _err("cms/unsupported-algorithm", "the signer key is on an unsupported EC curve");
|
|
161
|
-
var de = so.digestAlgorithm || "sha256";
|
|
162
|
-
if (!HASH[de]) throw _err("cms/unsupported-algorithm", "unsupported ECDSA digest algorithm " + JSON.stringify(de));
|
|
163
|
-
return { digest: de, digestAlgId: _algId(de, "absent"), sigAlgId: _algId(ECDSA_ALG[de], "absent"), imp: { name: "ECDSA", namedCurve: ec.curve }, sign: { name: "ECDSA", hash: HASH[de] }, ecdsaDer: true, coordLen: ec.coordLen };
|
|
164
|
-
}
|
|
165
|
-
if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
|
|
166
|
-
var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
|
|
167
|
-
var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
|
|
168
|
-
if (!NODE_DIGEST[dd]) throw _err("cms/unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
|
|
169
|
-
return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
|
|
170
|
-
}
|
|
171
|
-
if (MLDSA_BY_OID[keyOid]) {
|
|
172
|
-
// ML-DSA (RFC 9882): a one-shot post-quantum signature, pure mode, empty context. The signature
|
|
173
|
-
// covers the same sec. 5.4 preimage as every other scheme; digestAlgorithm names the message-
|
|
174
|
-
// digest algorithm (SHA-512 by default -- the sec. 3.3 MUST / interop default), which must be
|
|
175
|
-
// suitable for the parameter set (Q1 -- enforced on sign, refusing a below-strength digest).
|
|
176
|
-
var mlName = MLDSA_BY_OID[keyOid];
|
|
177
|
-
// RFC 9882 sec. 3.3: without signed attributes the digestAlgorithm has no meaning, but the
|
|
178
|
-
// signer MUST still emit SHA-512 to avoid an interoperability failure -- so force it (ignoring
|
|
179
|
-
// any caller digestAlgorithm) rather than carry a value a strict peer would reject. With signed
|
|
180
|
-
// attributes the digest must be suitable for the parameter set (Q1 -- enforced on sign).
|
|
181
|
-
var md;
|
|
182
|
-
if (noSignedAttrs) {
|
|
183
|
-
md = "sha512";
|
|
184
|
-
} else {
|
|
185
|
-
md = so.digestAlgorithm || "sha512";
|
|
186
|
-
if (!NODE_DIGEST[md]) throw _err("cms/unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
|
|
187
|
-
if (!MLDSA_SUITABLE_DIGEST[mlName][md]) throw _err("cms/unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
|
|
188
|
-
}
|
|
189
|
-
return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
|
|
190
|
-
}
|
|
191
|
-
if (SLHDSA_BY_OID[keyOid]) {
|
|
192
|
-
// SLH-DSA (RFC 9814): a one-shot post-quantum signature, pure mode, empty context. The message
|
|
193
|
-
// digest is FIXED per parameter set (sec. 4) -- emit it, and reject a contradicting caller
|
|
194
|
-
// digestAlgorithm fail-closed rather than carry a non-conformant digest.
|
|
195
|
-
var slh = SLHDSA_BY_OID[keyOid];
|
|
196
|
-
if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw _err("cms/bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " conflicts");
|
|
197
|
-
return { digest: slh.digest, digestAlgId: _algId(slh.digest, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: slh.wc }, sign: { name: slh.wc }, ecdsaDer: false };
|
|
198
|
-
}
|
|
199
|
-
throw _err("cms/unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
|
|
200
|
-
}
|
|
201
48
|
|
|
202
49
|
// The raw issuer Name TLV from a parsed certificate (byte-identical to the cert, so the sid the
|
|
203
50
|
// verifier canonically compares matches exactly). The issuer is the Name after the optional
|
|
@@ -215,30 +62,6 @@ function _skiValue(cert) {
|
|
|
215
62
|
catch (e) { throw _err("cms/no-ski", "the signer certificate's subjectKeyIdentifier extension value is not an OCTET STRING", e); }
|
|
216
63
|
}
|
|
217
64
|
|
|
218
|
-
// A caller-supplied CryptoKey carries its OWN algorithm (name, and for RSA the baked-in hash,
|
|
219
|
-
// for ECDSA the curve). A PKCS#8 key is imported under the resolved scheme, so it always agrees;
|
|
220
|
-
// but a pre-imported CryptoKey whose algorithm disagrees with the certificate's scheme would
|
|
221
|
-
// silently sign under a different hash than the digestAlgorithm advertises -- an inconsistent,
|
|
222
|
-
// non-verifiable CMS. Reject the mismatch fail-closed.
|
|
223
|
-
function _assertKeyMatchesScheme(key, imp) {
|
|
224
|
-
var ka = key.algorithm || {};
|
|
225
|
-
if (ka.name !== imp.name) throw _err("cms/bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
|
|
226
|
-
if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw _err("cms/bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
|
|
227
|
-
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw _err("cms/bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
228
|
-
}
|
|
229
|
-
// Import the signer private key: a CryptoKey passed through, or a PKCS#8 DER Buffer / PEM string.
|
|
230
|
-
function _importKey(key, imp) {
|
|
231
|
-
if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
|
|
232
|
-
_assertKeyMatchesScheme(key, imp);
|
|
233
|
-
return Promise.resolve(key);
|
|
234
|
-
}
|
|
235
|
-
var der;
|
|
236
|
-
if (Buffer.isBuffer(key)) der = key;
|
|
237
|
-
else if (key instanceof Uint8Array) der = Buffer.from(key);
|
|
238
|
-
else if (typeof key === "string") { try { der = pkcs8.pemDecode(key); } catch (e) { throw _err("cms/bad-input", "the signer PEM private key could not be decoded", e); } }
|
|
239
|
-
else throw _err("cms/bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
|
|
240
|
-
return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
|
|
241
|
-
}
|
|
242
65
|
|
|
243
66
|
// Build one SignerInfo (RFC 5652 sec. 5.3) and sign it. Resolves the SignerInfo (as a build
|
|
244
67
|
// node) plus its digestAlgorithm AlgorithmIdentifier and the signer certificate DER (for the
|
|
@@ -247,7 +70,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
247
70
|
var so = signer || {};
|
|
248
71
|
var certDer = _normCertDer(so.cert);
|
|
249
72
|
var cert = x509.parse(certDer);
|
|
250
|
-
var scheme =
|
|
73
|
+
var scheme = signScheme.resolveSignScheme(cert, so, opts.signedAttributes === false, _signE);
|
|
251
74
|
var useSki = opts.sid === "ski";
|
|
252
75
|
var sid = useSki
|
|
253
76
|
? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
|
|
@@ -282,7 +105,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
282
105
|
return { setOf: setOf, wire: wire };
|
|
283
106
|
}).then(function (toSign) {
|
|
284
107
|
var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
|
|
285
|
-
return
|
|
108
|
+
return signScheme.signOverTbs(scheme, so.key, signedBytes, _signE).then(function (sig) {
|
|
286
109
|
var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
|
|
287
110
|
if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
|
|
288
111
|
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
@@ -291,38 +114,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
291
114
|
});
|
|
292
115
|
}
|
|
293
116
|
|
|
294
|
-
// Sign the sec. 5.4 preimage under the resolved scheme -> the raw signature bytes for the
|
|
295
|
-
// SignerInfo signature OCTET STRING. The classical path imports the single signer key and signs
|
|
296
|
-
// (re-encoding ECDSA to canonical DER); the composite path signs BOTH component keys over the
|
|
297
|
-
// domain-separated M' and returns mldsaSig || tradSig (composite-sig.js owns the construction).
|
|
298
|
-
function _signScheme(scheme, so, signedBytes) {
|
|
299
|
-
if (scheme.composite) {
|
|
300
|
-
return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(so.key, scheme.composite), signedBytes).then(function (sig) { return Buffer.from(sig); });
|
|
301
|
-
}
|
|
302
|
-
return _importKey(so.key, scheme.imp).then(function (priv) {
|
|
303
|
-
return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
|
|
304
|
-
var sig = Buffer.from(sigRaw);
|
|
305
|
-
if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
|
|
306
|
-
return sig;
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
117
|
|
|
311
|
-
// A composite signer key is the two component private keys { mldsa, trad } as PKCS#8 (Node has no
|
|
312
|
-
// single composite key type; the CMS draft is silent on key import). A missing/wrong-typed
|
|
313
|
-
// component fails closed at config time -- never a partial single-component signature.
|
|
314
|
-
function _normCompositeKeys(key, comp) {
|
|
315
|
-
if (!key || typeof key !== "object" || Buffer.isBuffer(key) || key instanceof Uint8Array || key.mldsa == null || key.trad == null) {
|
|
316
|
-
throw _err("cms/bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
|
|
317
|
-
}
|
|
318
|
-
return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key"), trad: _normPkcs8(key.trad, "the composite traditional component key") };
|
|
319
|
-
}
|
|
320
|
-
function _normPkcs8(k, label) {
|
|
321
|
-
if (Buffer.isBuffer(k)) return k;
|
|
322
|
-
if (k instanceof Uint8Array) return Buffer.from(k);
|
|
323
|
-
if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw _err("cms/bad-input", label + " PEM could not be decoded", e); } }
|
|
324
|
-
throw _err("cms/bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
325
|
-
}
|
|
326
118
|
|
|
327
119
|
// A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
|
|
328
120
|
// RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
|
|
@@ -415,7 +207,4 @@ function _toBuf(v, what) {
|
|
|
415
207
|
// * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
|
|
416
208
|
// scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
|
|
417
209
|
// check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
|
|
418
|
-
|
|
419
|
-
// parameter-set digest-strength policy has ONE home -- a digest accepted on sign is exactly the
|
|
420
|
-
// set accepted on verify, and neither side can drift from the other (RFC 9882 sec. 3.3).
|
|
421
|
-
module.exports = { sign: sign, MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST, SLHDSA_BY_OID: SLHDSA_BY_OID };
|
|
210
|
+
module.exports = { sign: sign };
|
package/lib/cms-verify.js
CHANGED
|
@@ -35,8 +35,9 @@ var webcrypto = require("./webcrypto");
|
|
|
35
35
|
var subtle = webcrypto.webcrypto.subtle;
|
|
36
36
|
var edwardsPoint = require("./edwards-point");
|
|
37
37
|
var cmsSign = require("./cms-sign");
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var signScheme = require("./sign-scheme");
|
|
39
|
+
var MLDSA_SUITABLE_DIGEST = signScheme.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
|
|
40
|
+
var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
|
|
40
41
|
var validator = require("./validator-all");
|
|
41
42
|
var compositeSig = require("./composite-sig");
|
|
42
43
|
var guard = require("./guard-all");
|
package/lib/lint.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
var frameworkError = require("./framework-error");
|
|
27
27
|
var asn1 = require("./asn1-der");
|
|
28
|
+
var guard = require("./guard-all");
|
|
28
29
|
var oid = require("./oid");
|
|
29
30
|
var x509 = require("./schema-x509");
|
|
30
31
|
var pkix = require("./schema-pkix");
|
|
@@ -458,11 +459,51 @@ var CABF_TLS_RULES = [
|
|
|
458
459
|
},
|
|
459
460
|
];
|
|
460
461
|
|
|
462
|
+
// RFC 9935 -- ML-KEM public keys in X.509 certificates. The OID is the sole authority for the
|
|
463
|
+
// parameter set; the SPKI BIT STRING is the raw ek, exactly 384k+32 octets for that OID.
|
|
464
|
+
var ML_KEM_EK_LEN = {};
|
|
465
|
+
["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n, i) { ML_KEM_EK_LEN[n] = [800, 1184, 1568][i]; });
|
|
466
|
+
function _isMlKem(cert) {
|
|
467
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
468
|
+
return !!(spki && spki.algorithm && ML_KEM_EK_LEN[spki.algorithm.name] !== undefined);
|
|
469
|
+
}
|
|
470
|
+
var RFC9935_RULES = [
|
|
471
|
+
{
|
|
472
|
+
id: "lint/rfc9935/kem-key-usage", severity: "error", source: "rfc9935", citation: "RFC 9935 5",
|
|
473
|
+
message: "an ML-KEM certificate's keyUsage, if present, must assert keyEncipherment as the only key usage set",
|
|
474
|
+
appliesTo: _isMlKem,
|
|
475
|
+
check: function (cert, ctx) {
|
|
476
|
+
var ku = ctx.decode("keyUsage");
|
|
477
|
+
if (!ku || !ku.value) return null; // absent keyUsage is unconstrained (RFC 5280 4.2.1.3)
|
|
478
|
+
var v = ku.value;
|
|
479
|
+
var others = v.digitalSignature || v.nonRepudiation || v.dataEncipherment || v.keyAgreement ||
|
|
480
|
+
v.keyCertSign || v.cRLSign || v.encipherOnly || v.decipherOnly || v.reservedBitsSet === true;
|
|
481
|
+
return (v.keyEncipherment && !others) ? null : true;
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: "lint/rfc9935/kem-key-length", severity: "error", source: "rfc9935", citation: "RFC 9935 4 / FIPS 203",
|
|
486
|
+
message: "an ML-KEM subjectPublicKey must be the raw encapsulation key at its exact FIPS 203 size for the OID, in a byte-aligned BIT STRING",
|
|
487
|
+
appliesTo: _isMlKem,
|
|
488
|
+
check: function (cert) {
|
|
489
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
490
|
+
var want = ML_KEM_EK_LEN[spki.algorithm.name];
|
|
491
|
+
var pub = spki.publicKey; // a parsed SPKI always surfaces the BIT STRING as { bytes, unusedBits }
|
|
492
|
+
// The ek is a raw byte string carried in the BIT STRING: it must be the exact size AND
|
|
493
|
+
// byte-aligned. A right-length but non-octet-aligned BIT STRING (unusedBits != 0) is not the
|
|
494
|
+
// raw ek (RFC 9935 sec. 4); route the alignment test through the shared guard primitive.
|
|
495
|
+
return (guard.crypto.isOctetAligned(pub) && pub.bytes.length === want) ? null
|
|
496
|
+
: { context: { expected: want, got: pub.bytes.length, unusedBits: pub.unusedBits } };
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
|
|
461
501
|
var PROFILES = {
|
|
462
502
|
"rfc5280": RFC5280_RULES,
|
|
503
|
+
"rfc9935": RFC9935_RULES,
|
|
463
504
|
"cabf-tls": CABF_TLS_RULES,
|
|
464
505
|
};
|
|
465
|
-
var ALL_RULES = RFC5280_RULES.concat(CABF_TLS_RULES);
|
|
506
|
+
var ALL_RULES = RFC5280_RULES.concat(RFC9935_RULES).concat(CABF_TLS_RULES);
|
|
466
507
|
|
|
467
508
|
function _selectRules(profile) {
|
|
468
509
|
if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
|
|
@@ -557,7 +598,7 @@ function rules(profile) {
|
|
|
557
598
|
*
|
|
558
599
|
* List the known lint-profile names.
|
|
559
600
|
* @example
|
|
560
|
-
* pki.lint.profiles(); // -> ["rfc5280", "cabf-tls"]
|
|
601
|
+
* pki.lint.profiles(); // -> ["rfc5280", "rfc9935", "cabf-tls"]
|
|
561
602
|
*/
|
|
562
603
|
function profiles() { return Object.keys(PROFILES); }
|
|
563
604
|
|