@blamejs/pki 0.2.28 → 0.2.30
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 +20 -0
- package/README.md +2 -1
- package/lib/acme.js +3 -0
- package/lib/cms-encrypt.js +1 -1
- package/lib/cms-sign.js +2 -3
- package/lib/cms-verify.js +3 -1
- package/lib/ct.js +69 -12
- package/lib/framework-error.js +9 -0
- package/lib/guard-all.js +4 -0
- package/lib/guard-limits.js +26 -1
- package/lib/guard-time.js +68 -0
- package/lib/lint.js +3 -0
- package/lib/ocsp-verify.js +1 -0
- package/lib/path-validate.js +12 -6
- package/lib/schema-all.js +4 -0
- package/lib/schema-c509.js +479 -0
- package/lib/smime.js +1 -1
- package/lib/tsp-sign.js +1 -3
- package/lib/validator-all.js +3 -2
- package/lib/validator-sig.js +18 -40
- package/lib/webauthn.js +4 -4
- package/lib/webcrypto.js +17 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.30 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
C509 CBOR-encoded certificates arrive as pki.schema.c509.parse: decode the compact CBOR profile of X.509 in both its natively-signed and X.509-re-encoded forms, reconstructing the original DER byte-for-byte so the original signature still verifies.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509.parse(bytes) decodes a C509 CBOR-encoded certificate (draft-ietf-cose-cbor-encoded-cert) into structured, validated fields. It reads both certificate forms -- natively-signed (c509CertificateType 2) and the CBOR re-encoding of a DER X.509 v3 certificate (type 3) -- over the strict deterministic-CBOR codec, fail-closed. For a type-3 certificate it reconstructs the original DER byte-for-byte (de-compressing the EC point, re-emitting each field as canonical DER, re-wrapping the ECDSA signature) so the original signature verifies and the certificate round-trips through pki.schema.x509.parse; a field it cannot invert byte-exactly fails closed. It decodes CBOR, not DER, so it is an explicit-call surface and is not auto-routed by pki.schema.parse.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Certification-path validation now rejects a trust anchor whose per-purpose distrust-after date is an invalid Date at input (path/bad-input). Previously a malformed date value passed the Date type check but compared as not-a-number, silently disabling the distrust-after restriction it was meant to enforce.
|
|
18
|
+
|
|
19
|
+
## v0.2.29 — 2026-07-16
|
|
20
|
+
|
|
21
|
+
Certificate Transparency log-list signature verification arrives as pki.ct.verifyLogListSignature: verify the detached signature published alongside the CT log list against a caller-pinned signer key, completing the offline log-list trust chain.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- pki.ct.verifyLogListSignature(json, signature, publicKey) verifies the detached signature over the Certificate Transparency log list (log_list.sig over log_list.json). The message is the raw json bytes, verified byte-for-byte with no canonicalization; publicKey is the caller-pinned signer SubjectPublicKeyInfo (there is no embedded key). The scheme is RSASSA-PKCS1-v1.5 with SHA-256 (an EC P-256 arm is accepted for future-proofing). It resolves true or false (a cryptographic verdict) and throws a typed CtError on a forgeable or unsupported key (an RSA exponent below 3, a sub-2048-bit RSA key, an unsupported key type or curve, a non-conformant ECDSA DER signature). Its verdict is cross-checked against OpenSSL's dgst -verify. Paired with pki.ct.parseLogList, this completes the offline CT log-list trust chain.
|
|
26
|
+
|
|
7
27
|
## v0.2.28 — 2026-07-16
|
|
8
28
|
|
|
9
29
|
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.
|
package/README.md
CHANGED
|
@@ -204,6 +204,7 @@ is callable today; nothing below is a stub.
|
|
|
204
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
|
+
| `pki.schema.c509` | Parse C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed) |
|
|
207
208
|
| `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` |
|
|
208
209
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
209
210
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
|
@@ -225,7 +226,7 @@ is callable today; nothing below is a stub.
|
|
|
225
226
|
| `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
227
|
| `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
228
|
| `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). 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
|
+
| `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`. `verifyLogListSignature(json, signature, publicKey)` verifies the detached `log_list.sig` over the raw log-list bytes against a caller-pinned signer key (RSASSA-PKCS1-v1.5/SHA-256, EC P-256 arm; forgeable-key defenses fail closed) — cross-checked against `openssl dgst`, completing the offline log-list trust chain. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct`, `parseLogList`, `verifySctWithLogList`, `verifyLogListSignature` |
|
|
229
230
|
| `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
231
|
| `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
232
|
| `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
|
@@ -1111,6 +1111,9 @@ function validateRenewalInfo(obj) {
|
|
|
1111
1111
|
_validate("renewalInfo", obj);
|
|
1112
1112
|
var w = obj.suggestedWindow;
|
|
1113
1113
|
if (!_isObject(w) || !_isRfc3339(w.start) || !_isRfc3339(w.end)) throw E("acme/bad-renewal-window", "a renewalInfo suggestedWindow must carry RFC 3339 start and end (RFC 9773 sec. 4.2)");
|
|
1114
|
+
// The line above already rejected any w.start/w.end that is not a grammar+calendar-valid
|
|
1115
|
+
// RFC 3339 string, so neither Date.parse can be NaN here -- the comparison is source-validated.
|
|
1116
|
+
// allow:nan-date-comparison-unguarded -- source-validated by the rfc3339.isValid check above.
|
|
1114
1117
|
if (Date.parse(w.end) <= Date.parse(w.start)) throw E("acme/bad-renewal-window", "the renewal window end must be strictly after start (RFC 9773 sec. 4.2)");
|
|
1115
1118
|
return obj;
|
|
1116
1119
|
}
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -240,7 +240,7 @@ function _assertIterations(n) {
|
|
|
240
240
|
// Bound the PBKDF2 salt to the same cap the decryptor enforces, so a message we emit is always one we
|
|
241
241
|
// can read back.
|
|
242
242
|
function _assertSalt(salt) {
|
|
243
|
-
|
|
243
|
+
guard.limits.byteCap(salt, C.LIMITS.PBKDF2_MAX_SALT, _err, "cms/bad-input", "salt");
|
|
244
244
|
return salt;
|
|
245
245
|
}
|
|
246
246
|
|
package/lib/cms-sign.js
CHANGED
|
@@ -21,6 +21,7 @@ var pkix = require("./schema-pkix");
|
|
|
21
21
|
var frameworkError = require("./framework-error");
|
|
22
22
|
|
|
23
23
|
var signScheme = require("./sign-scheme");
|
|
24
|
+
var guard = require("./guard-all");
|
|
24
25
|
var CmsError = frameworkError.CmsError;
|
|
25
26
|
var b = asn1.build;
|
|
26
27
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
@@ -156,9 +157,7 @@ function sign(content, signers, opts) {
|
|
|
156
157
|
}
|
|
157
158
|
// A supplied signing-time MUST be a valid Date (or false to omit the attribute) -- never a
|
|
158
159
|
// silently-ignored non-Date or an Invalid Date that would encode a garbage Time.
|
|
159
|
-
if (opts.signingTime != null && opts.signingTime !== false
|
|
160
|
-
throw _err("cms/bad-input", "signingTime must be a valid Date, or false to omit it");
|
|
161
|
-
}
|
|
160
|
+
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertValid(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
|
162
161
|
|
|
163
162
|
return Promise.all(list.map(function (s) { return _buildSignerInfo(s, contentBuf, eContentType, opts); })).then(function (built) {
|
|
164
163
|
// digestAlgorithms: the distinct SignerInfo digestAlgorithm AlgorithmIdentifiers, deduped.
|
package/lib/cms-verify.js
CHANGED
|
@@ -253,7 +253,9 @@ function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOi
|
|
|
253
253
|
if (scheme.kind === "ec") {
|
|
254
254
|
var ec = EC_CURVE[curveOid];
|
|
255
255
|
if (!ec) throw _err("cms/unsupported-algorithm", "the signer key is on an unsupported EC curve");
|
|
256
|
-
|
|
256
|
+
// The ORDER-AWARE gate: r,s in [1, n-1] per FIPS 186-5 sec. 6.4.2 (rejecting an out-of-range r/s >= the
|
|
257
|
+
// curve order n, not only the r=s=0 forgery) -- a verifier that knows the curve MUST use it.
|
|
258
|
+
var raw = validator.sig.ecdsaDerToP1363(sigBytes, ec.curve, CmsError, "cms/bad-signature");
|
|
257
259
|
return subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: ec.curve }, false, ["verify"])
|
|
258
260
|
.then(function (k) { return subtle.verify({ name: "ECDSA", hash: hashName }, k, raw, signedBytes); });
|
|
259
261
|
}
|
package/lib/ct.js
CHANGED
|
@@ -181,9 +181,7 @@ function _parseSct(r, sctLen) {
|
|
|
181
181
|
*/
|
|
182
182
|
function parseSctList(extValue) {
|
|
183
183
|
var blob = _peelInner(_toBuffer(extValue, "the SCT-list extension value"));
|
|
184
|
-
|
|
185
|
-
throw new CtError("ct/too-large", "SCT list " + blob.length + " bytes exceeds the cap " + C.LIMITS.SCT_MAX_BYTES);
|
|
186
|
-
}
|
|
184
|
+
guard.limits.byteCap(blob, C.LIMITS.SCT_MAX_BYTES, _ctErr, "ct/too-large", "SCT list");
|
|
187
185
|
var outer = new TlsReader(blob, 0, blob.length);
|
|
188
186
|
var listLen = outer.u16("ct/bad-list");
|
|
189
187
|
if (listLen + 2 !== blob.length) {
|
|
@@ -396,9 +394,10 @@ async function verifySct(entry, sct, logPublicKey) {
|
|
|
396
394
|
if (!ec) throw new CtError("ct/unsupported-algorithm", "unsupported SCT log EC curve (RFC 6962 sec. 2.1.4 mandates NIST P-256)");
|
|
397
395
|
imp = { name: "ECDSA", namedCurve: ec.curve };
|
|
398
396
|
ver = { name: "ECDSA", hash: hashName };
|
|
399
|
-
// The SCT signature is a DER ECDSA-Sig-Value; route it through the
|
|
400
|
-
//
|
|
401
|
-
|
|
397
|
+
// The SCT signature is a DER ECDSA-Sig-Value; route it through the ORDER-AWARE conformance gate
|
|
398
|
+
// (primitive, minimal, and r,s in [1, n-1] per FIPS 186-5 sec. 6.4.2 -- rejecting an out-of-range
|
|
399
|
+
// r/s >= the curve order n, not only the r=s=0 shape) before converting to raw r||s.
|
|
400
|
+
sig = validator.sig.ecdsaDerToP1363(sig, ec.curve, CtError, "ct/bad-signature");
|
|
402
401
|
} else if (sigInfo.signatureName === "rsa") {
|
|
403
402
|
if (alg.algOid !== oid.byName("rsaEncryption")) throw new CtError("ct/bad-input", "the SCT declares an RSA signature but the log key is not an RSA key");
|
|
404
403
|
if (!(alg.rsaBits >= 2048)) throw new CtError("ct/unsupported-algorithm", "the SCT log RSA key is below the RFC 6962 sec. 2.1.4 minimum of 2048 bits");
|
|
@@ -622,6 +621,7 @@ function _parseTemporalInterval(ti) {
|
|
|
622
621
|
if (typeof ti !== "object") throw _ctErr("ct/bad-log-list", "a CT log temporal_interval must be an object");
|
|
623
622
|
var start = rfc3339.parse(ti.start_inclusive, _ctErr, "ct/bad-date", "temporal_interval.start_inclusive");
|
|
624
623
|
var end = rfc3339.parse(ti.end_exclusive, _ctErr, "ct/bad-date", "temporal_interval.end_exclusive");
|
|
624
|
+
// allow:nan-date-comparison-unguarded -- start/end are rfc3339.parse results, guaranteed non-NaN (rfc3339.isValid rejects a NaN date).
|
|
625
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
626
|
return { startInclusive: start, endExclusive: end };
|
|
627
627
|
}
|
|
@@ -775,18 +775,74 @@ async function verifySctWithLogList(entry, sct, logList, opts) {
|
|
|
775
775
|
// Temporal gate (fail-closed): the covered cert's notAfter must be in [start_inclusive, end_exclusive).
|
|
776
776
|
if (log.temporalInterval) {
|
|
777
777
|
var notAfter = _resolveNotAfter(entry, opts);
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (t < log.temporalInterval.startInclusive.getTime() || t >= log.temporalInterval.endExclusive.getTime()) {
|
|
778
|
+
// within() validates notAfter (and the interval bounds) fail-closed before comparing: an Invalid
|
|
779
|
+
// Date is instanceof Date yet NaN, and NaN < x / NaN >= x are both false, so an unvalidated notAfter
|
|
780
|
+
// would silently BYPASS the [startInclusive, endExclusive) containment. A malformed notAfter throws
|
|
781
|
+
// inside within (pass a valid opts.certNotAfter); an out-of-window notAfter returns false here.
|
|
782
|
+
if (!guard.time.within(notAfter, log.temporalInterval.startInclusive, log.temporalInterval.endExclusive, _ctErr, "ct/temporal-interval", "the covered certificate notAfter (pass a valid opts.certNotAfter)")) {
|
|
784
783
|
throw _ctErr("ct/temporal-interval", "the covered certificate's notAfter is outside the CT log's temporal_interval");
|
|
785
784
|
}
|
|
786
785
|
}
|
|
787
786
|
return verifySct(entry, sct, log.key); // the shipped crypto verdict (re-checks the logId binding)
|
|
788
787
|
}
|
|
789
788
|
|
|
789
|
+
/**
|
|
790
|
+
* @primitive pki.ct.verifyLogListSignature
|
|
791
|
+
* @signature pki.ct.verifyLogListSignature(json, signature, publicKey) -> Promise<boolean>
|
|
792
|
+
* @since 0.2.29
|
|
793
|
+
* @status experimental
|
|
794
|
+
* @spec RFC 6962, RFC 8017
|
|
795
|
+
* @related pki.ct.parseLogList, pki.ct.verifySct
|
|
796
|
+
*
|
|
797
|
+
* Verify the detached signature published alongside the Certificate Transparency log list (the
|
|
798
|
+
* `log_list.sig` over `log_list.json`). `json` is the RAW log-list bytes (a Buffer, or the fetched text
|
|
799
|
+
* as a string -- verified byte-for-byte, never re-serialized), `signature` is the detached signature, and
|
|
800
|
+
* `publicKey` is the caller-PINNED signer SubjectPublicKeyInfo (DER; there is no baked-in key). The scheme
|
|
801
|
+
* is RSASSA-PKCS1-v1.5 with SHA-256 over an RSA key (the deployed scheme; an EC P-256 / ECDSA-SHA-256 arm
|
|
802
|
+
* is accepted for future-proofing). Resolves `true` for a valid signature, `false` on a cryptographic
|
|
803
|
+
* mismatch (a verdict). Fail-closed forgery defenses throw before any verify: an RSA public exponent below
|
|
804
|
+
* 3 or even (`ct/bad-input`), a sub-2048-bit RSA key or an unsupported key type / curve
|
|
805
|
+
* (`ct/unsupported-algorithm`), a non-conformant ECDSA DER Sig-Value (`ct/bad-signature`); a structural
|
|
806
|
+
* evaluation failure is `ct/verify-error`. Offline: the caller fetches and pins; the toolkit only verifies.
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* var ok = await pki.ct.verifyLogListSignature(logListJsonBytes, logListSig, googleSignerSpki);
|
|
810
|
+
*/
|
|
811
|
+
async function verifyLogListSignature(json, signature, publicKey) {
|
|
812
|
+
var message = typeof json === "string" ? Buffer.from(json) : _toBuffer(json, "the CT log list JSON");
|
|
813
|
+
// Bound the signed message before the digest/verify (the same cap parseLogList enforces) so a hostile
|
|
814
|
+
// caller cannot force unbounded hashing work on an oversized input (CWE-400).
|
|
815
|
+
guard.limits.byteCap(message, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/too-large", "the CT log list");
|
|
816
|
+
var sig = _toBuffer(signature, "the CT log list signature");
|
|
817
|
+
var spki = _toBuffer(publicKey, "the CT log list signer public key (SPKI)");
|
|
818
|
+
var alg = _spkiAlg(spki); // fail-closed: a non-SPKI key or a forgeable RSA e < 3 throws ct/bad-input
|
|
819
|
+
var imp, ver;
|
|
820
|
+
if (alg.algOid === oid.byName("rsaEncryption")) {
|
|
821
|
+
if (!(alg.rsaBits >= 2048)) throw new CtError("ct/unsupported-algorithm", "the CT log-list signer RSA key is below the 2048-bit minimum");
|
|
822
|
+
imp = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
823
|
+
ver = { name: "RSASSA-PKCS1-v1_5" };
|
|
824
|
+
} else if (alg.algOid === oid.byName("ecPublicKey")) {
|
|
825
|
+
var ec = CT_EC_CURVE[alg.curveOid];
|
|
826
|
+
if (!ec) throw new CtError("ct/unsupported-algorithm", "unsupported CT log-list signer EC curve (only NIST P-256)");
|
|
827
|
+
imp = { name: "ECDSA", namedCurve: ec.curve };
|
|
828
|
+
ver = { name: "ECDSA", hash: "SHA-256" };
|
|
829
|
+
// A DER ECDSA-Sig-Value: route through the ORDER-AWARE conformance gate (primitive, minimal, and
|
|
830
|
+
// r,s in [1, n-1] per FIPS 186-5 sec. 6.4.2 -- defeating the CVE-2022-21449 r=s=0 forgery AND an
|
|
831
|
+
// out-of-range r/s >= the curve order n) before the raw r||s conversion.
|
|
832
|
+
sig = validator.sig.ecdsaDerToP1363(sig, ec.curve, CtError, "ct/bad-signature");
|
|
833
|
+
} else {
|
|
834
|
+
throw new CtError("ct/unsupported-algorithm", "unsupported CT log-list signer key algorithm (only rsaEncryption / ecPublicKey P-256)");
|
|
835
|
+
}
|
|
836
|
+
// A wrong signature resolves false from subtle.verify (a verdict); a structural failure -- an
|
|
837
|
+
// unimportable key -- is re-thrown fail-closed.
|
|
838
|
+
try {
|
|
839
|
+
var key = await subtle.importKey("spki", spki, imp, false, ["verify"]);
|
|
840
|
+
return await subtle.verify(ver, key, sig, message);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
throw new CtError("ct/verify-error", "the CT log-list signature could not be evaluated", e);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
790
846
|
module.exports = {
|
|
791
847
|
parseSctList: parseSctList,
|
|
792
848
|
reconstructSignedData: reconstructSignedData,
|
|
@@ -795,6 +851,7 @@ module.exports = {
|
|
|
795
851
|
signSct: signSct,
|
|
796
852
|
parseLogList: parseLogList,
|
|
797
853
|
verifySctWithLogList: verifySctWithLogList,
|
|
854
|
+
verifyLogListSignature: verifyLogListSignature,
|
|
798
855
|
HASH_ALGORITHMS: HASH_ALGORITHMS,
|
|
799
856
|
SIGNATURE_ALGORITHMS: SIGNATURE_ALGORITHMS,
|
|
800
857
|
};
|
package/lib/framework-error.js
CHANGED
|
@@ -231,6 +231,14 @@ var SmimeError = defineClass("SmimeError", { withCause: true });
|
|
|
231
231
|
// inner `asn1/*` decode error) as `.cause`.
|
|
232
232
|
var CtError = defineClass("CtError", { withCause: true });
|
|
233
233
|
|
|
234
|
+
// C509Error -- a CBOR byte sequence that is not a well-formed C509 certificate
|
|
235
|
+
// (draft-ietf-cose-cbor-encoded-cert): a root that is not the 11-element array, a
|
|
236
|
+
// c509CertificateType outside {2,3}, a field encoded against its ~biguint/~time/~oid
|
|
237
|
+
// contract, an algorithm/attribute/extension int with no registry row, or a type-3
|
|
238
|
+
// cert that does not invert byte-for-byte to its original DER. Carries the underlying
|
|
239
|
+
// leaf fault (the inner `cbor/*` or `asn1/*` decode error) as `.cause`.
|
|
240
|
+
var C509Error = defineClass("C509Error", { withCause: true });
|
|
241
|
+
|
|
234
242
|
// MerkleError -- a malformed input to the RFC 6962 / RFC 9162 Merkle-tree
|
|
235
243
|
// proof-verification core: a tree coordinate that is not a non-negative integer
|
|
236
244
|
// (or a Number above 2^53 where an exact BigInt is required), a leafIndex
|
|
@@ -333,6 +341,7 @@ module.exports = {
|
|
|
333
341
|
CmpError: CmpError,
|
|
334
342
|
PathError: PathError,
|
|
335
343
|
CtError: CtError,
|
|
344
|
+
C509Error: C509Error,
|
|
336
345
|
ShbsError: ShbsError,
|
|
337
346
|
HpkeError: HpkeError,
|
|
338
347
|
SigstoreError: SigstoreError,
|
package/lib/guard-all.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// guard.range.int / .uint31 / .positiveInt31
|
|
22
22
|
// -- bound a decoded integer before narrowing
|
|
23
23
|
// to Number (silent-narrowing defence)
|
|
24
|
+
// guard.time.assertValid / .within -- validate a Date-instant before a temporal
|
|
25
|
+
// comparison (NaN-Date fail-open defence)
|
|
24
26
|
// guard.name.dnEqual / .rdnEqual / .assertNoControlBytes / .assertPrintableIa5
|
|
25
27
|
// -- canonical DN identity + name-string integrity
|
|
26
28
|
// guard.name.escapeControlBytes / .escapeDnValue
|
|
@@ -44,6 +46,7 @@ var text = require("./guard-text");
|
|
|
44
46
|
var limits = require("./guard-limits");
|
|
45
47
|
var crypto = require("./guard-crypto");
|
|
46
48
|
var range = require("./guard-range");
|
|
49
|
+
var time = require("./guard-time");
|
|
47
50
|
var name = require("./guard-name");
|
|
48
51
|
var encoding = require("./guard-encoding");
|
|
49
52
|
var json = require("./guard-json");
|
|
@@ -55,6 +58,7 @@ module.exports = {
|
|
|
55
58
|
limits: limits,
|
|
56
59
|
crypto: crypto,
|
|
57
60
|
range: range,
|
|
61
|
+
time: time,
|
|
58
62
|
name: name,
|
|
59
63
|
encoding: encoding,
|
|
60
64
|
json: json,
|
package/lib/guard-limits.js
CHANGED
|
@@ -103,4 +103,29 @@ function counter(max, E, code, label) {
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
// byteCap(buf, max, E, code, label) -> the same buffer, once proven within `max`
|
|
107
|
+
// bytes. Bounds a caller-supplied buffer BEFORE an expensive hash / verify / parse
|
|
108
|
+
// materializes over it (CWE-400 uncontrolled resource consumption / CWE-770
|
|
109
|
+
// allocation without limits -- the class guard.text.decode closes for a decoded
|
|
110
|
+
// string and counter closes for element fanout, here for a raw byte input). Tier-2:
|
|
111
|
+
// an over-size input is hostile / malformed content, so it throws the caller's typed
|
|
112
|
+
// PkiError via the E(code, message) factory. `max` is the caller's ceiling (a
|
|
113
|
+
// C.LIMITS.* constant, or an operator max already run through cap()).
|
|
114
|
+
// @enforced-by behavioral -- a `buf.length > cap` size gate has no rename-proof code
|
|
115
|
+
// shape distinct from an arity / field-width check (children.length > 2,
|
|
116
|
+
// ext.length > 0xffff), so a codebase-patterns detector would false-fire; the
|
|
117
|
+
// over-size RED vectors driving the composing verifiers are the guard.
|
|
118
|
+
function byteCap(buf, max, E, code, label) {
|
|
119
|
+
// The ceiling is an authoring input: an undefined / NaN / fractional / negative
|
|
120
|
+
// max makes `length > max` never fire -- a silently dead size defence. Reject at
|
|
121
|
+
// config time (TypeError), regardless of the value currency E selects.
|
|
122
|
+
if (!Number.isInteger(max) || max < 0) {
|
|
123
|
+
throw new TypeError("guard.limits.byteCap: max must be a non-negative integer");
|
|
124
|
+
}
|
|
125
|
+
if (buf.length > max) {
|
|
126
|
+
throw E(code, (label || "input") + " is " + buf.length + " bytes, over the " + max + "-byte cap");
|
|
127
|
+
}
|
|
128
|
+
return buf;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { cap: cap, depthCap: depthCap, counter: counter, byteCap: byteCap };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- no operator-facing namespace. The documented surface is the
|
|
6
|
+
// verifiers whose validity / currency / temporal gates compose this guard
|
|
7
|
+
// (pki.path.validate, pki.ocsp.verify, pki.tsp.*, pki.ct.*, pki.cms.sign).
|
|
8
|
+
//
|
|
9
|
+
// guard-time -- fail-closed Date-instant validation before a temporal comparison.
|
|
10
|
+
//
|
|
11
|
+
// Defends the NaN-Date fail-open class (CWE-20 improper input validation feeding
|
|
12
|
+
// a silent security-gate bypass). An Invalid Date is still `instanceof Date`, and
|
|
13
|
+
// EVERY relational comparison against its NaN getTime() (NaN < x, NaN >= x) is
|
|
14
|
+
// false -- so a validity / not-before / not-after / currency window built on an
|
|
15
|
+
// unvalidated Date silently ACCEPTS when it should reject. The class recurred at
|
|
16
|
+
// three independent boundaries (TSP genTime, OCSP producedAt/thisUpdate, CT
|
|
17
|
+
// temporal-interval) before it was centralized here; the sibling
|
|
18
|
+
// nan-date-comparison-unguarded codebase-patterns detector flags any lib function
|
|
19
|
+
// that re-inlines a `.getTime()` comparison WITHOUT first rejecting a NaN, so a
|
|
20
|
+
// new boundary is routed to this guard rather than re-growing the bug.
|
|
21
|
+
//
|
|
22
|
+
// Tier split: `value instanceof Date` with a NaN time is malformed CALLER input
|
|
23
|
+
// (a config-time / entry-point Date -- an operator-supplied distrustAfter, a
|
|
24
|
+
// caller `opts.time`), so assertValid throws the caller's typed error rather than
|
|
25
|
+
// returning a silent default. within() is the fail-closed window primitive: it
|
|
26
|
+
// THROWS on a malformed operand but RETURNS a boolean for in/out-of-window, so the
|
|
27
|
+
// OCSP / CRL currency callers that treat out-of-window as a `continue` skip keep
|
|
28
|
+
// their control flow while a NaN operand can never slip through as a false.
|
|
29
|
+
|
|
30
|
+
// assertValid(value, E, code, label) -> the same Date, once proven valid.
|
|
31
|
+
// value : a Date (or an alleged one) from a caller boundary.
|
|
32
|
+
// E : the (code, message[, cause]) typed-error FACTORY in scope at the call
|
|
33
|
+
// site (ns.E / the module-local _err) -- NEVER a defineClass class, which
|
|
34
|
+
// would crash `class cannot be invoked without new` on the error path.
|
|
35
|
+
// code : the frozen domain/reason code this boundary rejects malformed time under.
|
|
36
|
+
// label : field phrase for the message.
|
|
37
|
+
// @enforced-by nan-date-comparison-unguarded
|
|
38
|
+
function assertValid(value, E, code, label) {
|
|
39
|
+
if (!(value instanceof Date) || isNaN(value.getTime())) {
|
|
40
|
+
throw E(code, (label || "value") + " must be a valid Date");
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// within(instant, lower, upper, E, code, label, opts) -> boolean (instant in window).
|
|
46
|
+
// Rejects a malformed instant / lower / upper by THROWING (assertValid), then
|
|
47
|
+
// answers containment as a boolean. Half-open [lower, upper) by default (a CT
|
|
48
|
+
// temporal-interval / an OCSP-CRL currency window); pass opts.upperInclusive for
|
|
49
|
+
// the closed [notBefore, notAfter] certificate-validity shape.
|
|
50
|
+
// @enforced-by nan-date-comparison-unguarded
|
|
51
|
+
function within(instant, lower, upper, E, code, label, opts) {
|
|
52
|
+
// assertValid rejects a non-Date / Invalid Date operand up front, so t/lo/hi below are
|
|
53
|
+
// guaranteed non-NaN -- the containment comparisons can never be a silent NaN-false. The
|
|
54
|
+
// getTime() results are bound to locals (not compared inline), so this home carries no
|
|
55
|
+
// unguarded `.getTime()`-in-a-comparison shape for the nan-date-comparison-unguarded detector.
|
|
56
|
+
assertValid(instant, E, code, label);
|
|
57
|
+
assertValid(lower, E, code, (label || "window") + " lower bound");
|
|
58
|
+
assertValid(upper, E, code, (label || "window") + " upper bound");
|
|
59
|
+
var t = instant.getTime();
|
|
60
|
+
var lo = lower.getTime();
|
|
61
|
+
var hi = upper.getTime();
|
|
62
|
+
return t >= lo && (opts && opts.upperInclusive ? t <= hi : t < hi);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
assertValid: assertValid,
|
|
67
|
+
within: within,
|
|
68
|
+
};
|
package/lib/lint.js
CHANGED
|
@@ -156,6 +156,7 @@ function _makeCtx(cert, profile) {
|
|
|
156
156
|
function _effective(rule, cert) {
|
|
157
157
|
if (!rule.effectiveDate) return true;
|
|
158
158
|
var nb = cert.validity && cert.validity.notBefore;
|
|
159
|
+
// allow:nan-date-comparison-unguarded -- nb is a codec-parsed cert notBefore (asn1 readTime rejects a NaN instant); effectiveDate is a Date literal.
|
|
159
160
|
return (nb instanceof Date) && nb.getTime() >= rule.effectiveDate.getTime();
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -296,6 +297,7 @@ var RFC5280_RULES = [
|
|
|
296
297
|
message: "the certificate notBefore must not be later than notAfter",
|
|
297
298
|
check: function (cert) {
|
|
298
299
|
var v = cert.validity;
|
|
300
|
+
// allow:nan-date-comparison-unguarded -- notBefore/notAfter are codec-parsed cert dates (asn1 readTime rejects a NaN instant).
|
|
299
301
|
return (v.notBefore instanceof Date && v.notAfter instanceof Date && v.notBefore.getTime() > v.notAfter.getTime()) ? true : null;
|
|
300
302
|
},
|
|
301
303
|
},
|
|
@@ -375,6 +377,7 @@ var VALIDITY_SCHEDULE = [
|
|
|
375
377
|
var VALIDITY_SCHEDULE_START = VALIDITY_SCHEDULE[VALIDITY_SCHEDULE.length - 1].from;
|
|
376
378
|
function _validityCeilingDays(notBefore) {
|
|
377
379
|
for (var i = 0; i < VALIDITY_SCHEDULE.length; i++) {
|
|
380
|
+
// allow:nan-date-comparison-unguarded -- notBefore is a codec-parsed cert date (NaN-rejected); the schedule bounds are Date literals.
|
|
378
381
|
if (notBefore.getTime() >= VALIDITY_SCHEDULE[i].from.getTime()) return VALIDITY_SCHEDULE[i].maxDays;
|
|
379
382
|
}
|
|
380
383
|
// Unreachable: the rule's effectiveDate (VALIDITY_SCHEDULE_START) reports NE for any cert
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -206,6 +206,7 @@ function makeOcspVerify(deps) {
|
|
|
206
206
|
if (st.type === "revoked") {
|
|
207
207
|
// Present-time validation revokes regardless of a future revocationTime (skew/post-dating);
|
|
208
208
|
// only explicit historical validation defers a strictly-future revocation (reports good).
|
|
209
|
+
// allow:nan-date-comparison-unguarded -- revocationTime is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (sawGood stays unset -> not treated good), and `time` is validated at the pki.ocsp.verify / path.verifyOcspResponse entry points.
|
|
209
210
|
if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { out.sawGood = true; }
|
|
210
211
|
else if (!out.revoked) { out.revoked = { revocationReason: st.revocationReason || null, reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : "") }; }
|
|
211
212
|
} else if (st.type === "good") { out.sawGood = true; }
|
package/lib/path-validate.js
CHANGED
|
@@ -1133,9 +1133,7 @@ async function validate(path, opts) {
|
|
|
1133
1133
|
if (!opts.trustAnchor) throw E("path/bad-input", "validate: a trustAnchor is required");
|
|
1134
1134
|
// The validity-window check is always on (6.1.3(a)(2)); a missing/invalid
|
|
1135
1135
|
// check date must fail closed, never silently disable it.
|
|
1136
|
-
|
|
1137
|
-
throw E("path/bad-input", "validate: opts.time must be a Date (the always-on validity-window check date)");
|
|
1138
|
-
}
|
|
1136
|
+
guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
|
|
1139
1137
|
// Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
|
|
1140
1138
|
// throws here rather than silently disabling the behavior it configures.
|
|
1141
1139
|
// Validate-if-present (the default is applied where the policy state is built);
|
|
@@ -1355,13 +1353,20 @@ async function validate(path, opts) {
|
|
|
1355
1353
|
// per-purpose distrust-after date and delegator purposes apply to the
|
|
1356
1354
|
// end-entity leaf it ultimately certifies.
|
|
1357
1355
|
var ta = opts.trustAnchor;
|
|
1358
|
-
|
|
1359
|
-
|
|
1356
|
+
// A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a
|
|
1357
|
+
// NaN time) would make `notBefore > it` NaN-false and SILENTLY drop the distrust
|
|
1358
|
+
// restriction -- the NaN-Date fail-open. Validate a present date fail-closed
|
|
1359
|
+
// before the comparison; an absent (undefined/null) date is no restriction.
|
|
1360
|
+
var distrustDate = (checkPurpose && ta.distrustAfter) ? ta.distrustAfter[checkPurpose] : null;
|
|
1361
|
+
if (distrustDate != null) {
|
|
1362
|
+
distrustDate = guard.time.assertValid(distrustDate, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
|
|
1360
1363
|
// STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
|
|
1361
1364
|
// (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
|
|
1362
1365
|
// <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
|
|
1363
1366
|
// convention keeps the whole boundary day trusted).
|
|
1364
|
-
|
|
1367
|
+
if (cert.validity.notBefore > distrustDate) {
|
|
1368
|
+
checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
|
|
1369
|
+
}
|
|
1365
1370
|
}
|
|
1366
1371
|
if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
|
|
1367
1372
|
checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
|
|
@@ -1766,6 +1771,7 @@ function crlChecker(crls) {
|
|
|
1766
1771
|
// validation (opts.historicalMode) -- validating as of a past instant,
|
|
1767
1772
|
// e.g. a timestamped signature -- does an entry dated AFTER the
|
|
1768
1773
|
// validation time not yet apply.
|
|
1774
|
+
// allow:nan-date-comparison-unguarded -- revocationDate is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (the skip is not taken -> the entry is treated as revoked), and `time` is validated at the path.validate / crlChecker entry points.
|
|
1769
1775
|
if (historical && entry.revocationDate instanceof Date && entry.revocationDate.getTime() > time.getTime()) continue;
|
|
1770
1776
|
// Record the revocation but keep scanning: a delta removeFromCRL for the
|
|
1771
1777
|
// same serial (in another CRL) overrides it (base/delta not merged).
|
package/lib/schema-all.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
var engine = require("./schema-engine");
|
|
34
34
|
var pkix = require("./schema-pkix");
|
|
35
35
|
var x509 = require("./schema-x509");
|
|
36
|
+
var c509 = require("./schema-c509");
|
|
36
37
|
var crl = require("./schema-crl");
|
|
37
38
|
var csr = require("./schema-csr");
|
|
38
39
|
var pkcs8 = require("./schema-pkcs8");
|
|
@@ -295,6 +296,9 @@ function parse(input) {
|
|
|
295
296
|
module.exports = {
|
|
296
297
|
engine: engine,
|
|
297
298
|
x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
|
|
299
|
+
// C509 is CBOR, not DER: an explicit-call surface only, NOT added to FORMATS / the detect-and-route
|
|
300
|
+
// parse() below (which detect DER shapes). No pemDecode/pemEncode -- C509 is binary CBOR.
|
|
301
|
+
c509: { parse: c509.parse },
|
|
298
302
|
crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
|
|
299
303
|
csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
|
|
300
304
|
pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.c509
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title C509
|
|
8
|
+
* @intro C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert). A compact CBOR
|
|
9
|
+
* re-encoding of an X.509 v3 certificate: a deterministic-CBOR array of exactly 11 elements
|
|
10
|
+
* (10 TBS fields + the issuer signature). Two modes -- c509CertificateType 2 = natively-signed
|
|
11
|
+
* C509, 3 = a CBOR re-encoding of a DER X.509 certificate that inverts byte-for-byte to the
|
|
12
|
+
* original DER (so the original signature still verifies). It decodes CBOR, not DER, so it is
|
|
13
|
+
* reached by an explicit pki.schema.c509.parse call and is NOT auto-routed by pki.schema.parse.
|
|
14
|
+
* @card Composes the shipped pki.cbor codec (core-deterministic, fail-closed) + the X.509 model.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var cbor = require("./cbor-det");
|
|
18
|
+
var asn1 = require("./asn1-der");
|
|
19
|
+
var oid = require("./oid");
|
|
20
|
+
var constants = require("./constants");
|
|
21
|
+
var frameworkError = require("./framework-error");
|
|
22
|
+
var validator = require("./validator-all");
|
|
23
|
+
var webcrypto = require("./webcrypto");
|
|
24
|
+
|
|
25
|
+
var b = asn1.build;
|
|
26
|
+
|
|
27
|
+
var C = constants;
|
|
28
|
+
var C509Error = frameworkError.C509Error;
|
|
29
|
+
function _err(code, message, cause) { return new C509Error(code, message, cause); }
|
|
30
|
+
// The ECMAScript Date window is +/- 8.64e15 ms = +/- 8.64e12 seconds; a C509 ~time is an unsigned
|
|
31
|
+
// epoch-seconds value, so only the upper bound can be exceeded (mirrors cbor-det read.time).
|
|
32
|
+
var MAX_EPOCH_SECONDS = 8640000000000n;
|
|
33
|
+
|
|
34
|
+
// The C509 integer registries (draft-20 sec. 8.6/sec. 8.8/sec. 8.14/sec. 8.15): a C509 int is a compact ALIAS of an
|
|
35
|
+
// OID, resolved to the SAME name oid.byName returns for the DER form. Declared as int -> registered
|
|
36
|
+
// OID NAME (never a dotted-decimal literal -- the oid-dotted-decimal-literal gate); oid.byName then
|
|
37
|
+
// yields the dotted string. A row whose target name is not registered fails closed at module load.
|
|
38
|
+
function _name(n) { var d = oid.byName(n); if (!d) { throw new Error("schema-c509: unregistered OID name " + JSON.stringify(n)); } return n; }
|
|
39
|
+
|
|
40
|
+
// sec. 8.14 issuerSignatureAlgorithm / signature (the subset v1 covers; negative = legacy SHA-1 values).
|
|
41
|
+
var SIG_ALG_BY_INT = {
|
|
42
|
+
0: _name("ecdsaWithSHA256"),
|
|
43
|
+
1: _name("ecdsaWithSHA384"),
|
|
44
|
+
2: _name("ecdsaWithSHA512"),
|
|
45
|
+
};
|
|
46
|
+
// sec. 8.15 subjectPublicKeyAlgorithm (int -> {alg, curve?} so the reconstruction can rebuild the SPKI).
|
|
47
|
+
var PK_ALG_BY_INT = {
|
|
48
|
+
0: { alg: _name("rsaEncryption") },
|
|
49
|
+
1: { alg: _name("ecPublicKey"), curve: _name("prime256v1") },
|
|
50
|
+
2: { alg: _name("ecPublicKey"), curve: _name("secp384r1") },
|
|
51
|
+
3: { alg: _name("ecPublicKey"), curve: _name("secp521r1") },
|
|
52
|
+
};
|
|
53
|
+
// EC curve -> field size in bytes (the SEC1 coordinate width), for point-length validation.
|
|
54
|
+
var EC_FIELD_BYTES = { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 };
|
|
55
|
+
|
|
56
|
+
// sec. 8.6 attribute types (abs(int) -> name; the sign selects the X.509 string type).
|
|
57
|
+
var ATTR_BY_INT = {
|
|
58
|
+
1: _name("commonName"),
|
|
59
|
+
2: _name("surname"),
|
|
60
|
+
3: _name("serialNumber"),
|
|
61
|
+
4: _name("countryName"),
|
|
62
|
+
6: _name("localityName"),
|
|
63
|
+
7: _name("stateOrProvinceName"),
|
|
64
|
+
8: _name("organizationName"),
|
|
65
|
+
9: _name("organizationalUnitName"),
|
|
66
|
+
10: _name("title"),
|
|
67
|
+
};
|
|
68
|
+
// sec. 8.8 extension types (abs(int) -> name; the sign selects criticality).
|
|
69
|
+
var EXT_BY_INT = {
|
|
70
|
+
1: _name("subjectKeyIdentifier"),
|
|
71
|
+
2: _name("keyUsage"),
|
|
72
|
+
3: _name("subjectAltName"),
|
|
73
|
+
4: _name("basicConstraints"),
|
|
74
|
+
7: _name("keyUsage"),
|
|
75
|
+
10: _name("authorityKeyIdentifier"),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ---- field readers (the unwrapped ~biguint / ~time / ~oid contracts; draft-20 sec. 3.1) ----
|
|
79
|
+
|
|
80
|
+
// ~biguint (sec. 3.1.2): a BARE byte string (major type 2), big-endian magnitude, the non-negative
|
|
81
|
+
// leading 0x00 OMITTED. NOT the shipped read.biguint (which requires the tag-2 wrapper and rejects
|
|
82
|
+
// <= 8-byte content). A leading 0x00 is non-minimal; content over the cap is rejected.
|
|
83
|
+
function _biguint(node, code, label) {
|
|
84
|
+
if (!node || node.majorType !== 2) throw _err(code, label + " must be an unwrapped CBOR byte string (~biguint)");
|
|
85
|
+
var b = node.content;
|
|
86
|
+
if (b.length > C.LIMITS.CBOR_MAX_BIGUINT_BYTES) throw _err(code, label + " exceeds the ~biguint byte cap");
|
|
87
|
+
if (b.length > 1 && b[0] === 0x00) throw _err("c509/non-minimal-serial", label + " has a redundant leading 0x00 (~biguint omits the sign octet)");
|
|
88
|
+
return b.length ? BigInt("0x" + b.toString("hex")) : 0n;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ~time (sec. 3.1.5): a BARE unsigned integer (major type 0), epoch seconds. A major-type-1 / tag / float
|
|
92
|
+
// MUST reject. Bound to the Date window; the CBOR simple null (permitted only for notAfter) -> null.
|
|
93
|
+
function _time(node, allowNull, label) {
|
|
94
|
+
if (allowNull && node.majorType === 7 && node.ai === 22) return null; // CBOR simple null (0xF6)
|
|
95
|
+
if (!node || node.majorType !== 0) throw _err("c509/bad-validity", label + " must be an unwrapped CBOR epoch integer (~time)");
|
|
96
|
+
var secs = node.argument;
|
|
97
|
+
if (secs > MAX_EPOCH_SECONDS) throw _err("c509/bad-validity", label + " is outside the representable Date range");
|
|
98
|
+
return new Date(C.TIME.seconds(Number(secs)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ~oid (sec. 3.1.3 etc.): a BARE byte string carrying the BER OID content octets (RFC 9090), no tag-111
|
|
102
|
+
// head. Compose asn1.decodeOidContent -> dotted -> oid.name (the same the tag reader does internally).
|
|
103
|
+
function _oidName(node, code, label) {
|
|
104
|
+
if (!node || node.majorType !== 2) throw _err(code, label + " must be an unwrapped CBOR byte string (~oid)");
|
|
105
|
+
var dotted;
|
|
106
|
+
try { dotted = asn1.decodeOidContent(node.content); }
|
|
107
|
+
catch (e) { throw _err(code, label + " is not a valid BER OID content encoding", e); }
|
|
108
|
+
return { oid: dotted, name: oid.name(dotted) || dotted };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// AlgorithmIdentifier (sec. 3.1.3/sec. 3.1.7): int (registry) | ~oid (bare bytes) | [ ~oid, params ].
|
|
112
|
+
function _algorithm(node, byInt, code, label) {
|
|
113
|
+
if (node.majorType === 0 || node.majorType === 1) {
|
|
114
|
+
var i = Number(cbor.read.int(node));
|
|
115
|
+
var mapped = byInt[i]; // registry keyed by the signed int (negatives are legacy SHA-1 rows)
|
|
116
|
+
if (mapped === undefined) throw _err("c509/unknown-algorithm", label + " integer " + i + " has no C509 registry row");
|
|
117
|
+
if (typeof mapped === "string") return { name: mapped, oid: oid.byName(mapped) };
|
|
118
|
+
return { name: mapped.alg, oid: oid.byName(mapped.alg), curve: mapped.curve || null };
|
|
119
|
+
}
|
|
120
|
+
if (node.majorType === 2) { var r = _oidName(node, code, label); return { name: r.name, oid: r.oid }; }
|
|
121
|
+
if (node.majorType === 4 && node.children && node.children.length === 2) {
|
|
122
|
+
var a = _oidName(node.children[0], code, label);
|
|
123
|
+
// The [~oid, params] form carries the DER parameters as a CBOR byte string; a non-byte-string here
|
|
124
|
+
// is malformed and cannot be reconstructed (b.raw would append garbage) -- fail closed.
|
|
125
|
+
if (node.children[1].majorType !== 2) throw _err(code, label + " algorithm parameters must be a CBOR byte string");
|
|
126
|
+
return { name: a.name, oid: a.oid, parameters: node.children[1].content };
|
|
127
|
+
}
|
|
128
|
+
throw _err(code, label + " is not a C509 AlgorithmIdentifier (int / ~oid / [~oid, params])");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// SpecialText attribute value (sec. 3.1.4/sec. 3.1.6): text | bytes (even-length-hex optimization) | tag-48
|
|
132
|
+
// (a MAC address, RFC 9542). v1 surfaces the value; the DN string uses the text form.
|
|
133
|
+
function _specialText(node) {
|
|
134
|
+
if (node.majorType === 3) return { text: cbor.read.textString(node) };
|
|
135
|
+
if (node.majorType === 2) return { hex: node.content.toString("hex") };
|
|
136
|
+
if (node.majorType === 6 && Number(node.argument) === 48) {
|
|
137
|
+
// A tag-48 MAC address (RFC 9542) MUST wrap a CBOR byte string of 6 (EUI-48/MAC-48) or 8 (EUI-64)
|
|
138
|
+
// bytes; anything else is malformed and cannot reconstruct a well-formed EUI-64 commonName.
|
|
139
|
+
if (!node.children || !node.children[0] || node.children[0].majorType !== 2) {
|
|
140
|
+
throw _err("c509/bad-name", "a tag-48 MAC-address value must wrap a CBOR byte string");
|
|
141
|
+
}
|
|
142
|
+
var euiBytes = node.children[0].content;
|
|
143
|
+
if (euiBytes.length !== 6 && euiBytes.length !== 8) throw _err("c509/bad-name", "a tag-48 MAC address must be 6 (EUI-48) or 8 (EUI-64) bytes");
|
|
144
|
+
return { eui64: euiBytes };
|
|
145
|
+
}
|
|
146
|
+
throw _err("c509/bad-name", "an attribute value is not a C509 SpecialText (text / bytes / tag-48)");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A single Name (sec. 3.1.4/sec. 3.1.6): the CBOR simple null (issuer only) | a bare SpecialText single
|
|
150
|
+
// commonName | an array of RDNAttributes. Surfaces { dn, rdns, eui64? } shape-compatible with x509.
|
|
151
|
+
function _name509(node, isSubject) {
|
|
152
|
+
if (!isSubject && node.majorType === 7 && node.ai === 22) return null; // issuer == subject (self-signed)
|
|
153
|
+
// A bare SpecialText (not an array) is a single commonName attribute (attributeType == +1).
|
|
154
|
+
if (node.majorType === 3 || node.majorType === 2 || node.majorType === 6) {
|
|
155
|
+
var sv = _specialText(node);
|
|
156
|
+
if (sv.eui64) return { rdns: [{ type: "commonName", eui64: sv.eui64 }], eui64: sv.eui64, dn: "CN=" + _macToEui64String(sv.eui64) };
|
|
157
|
+
var val = sv.text !== undefined ? sv.text : sv.hex;
|
|
158
|
+
return { rdns: [{ type: "commonName", value: val }], dn: "CN=" + val };
|
|
159
|
+
}
|
|
160
|
+
if (node.majorType !== 4) throw _err("c509/bad-name", "a C509 Name must be null, a SpecialText, or an array of RDN attributes");
|
|
161
|
+
var rdns = [];
|
|
162
|
+
var parts = [];
|
|
163
|
+
var kids = node.children || [];
|
|
164
|
+
// Each RDN attribute is an (attributeType, attributeValue) pair; an odd-length array is a dangling
|
|
165
|
+
// attribute type with no value -- reject rather than silently drop the trailing element.
|
|
166
|
+
if (kids.length % 2 !== 0) throw _err("c509/bad-name", "a C509 Name array must be attribute-type/value pairs (dangling attribute type)");
|
|
167
|
+
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
168
|
+
var ti = Number(cbor.read.int(kids[i]));
|
|
169
|
+
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
170
|
+
if (tname === undefined) throw _err("c509/bad-name", "attribute type integer " + ti + " has no C509 registry row");
|
|
171
|
+
var v = _specialText(kids[i + 1]);
|
|
172
|
+
var vv = v.text !== undefined ? v.text : (v.hex !== undefined ? v.hex : _macToEui64String(v.eui64));
|
|
173
|
+
rdns.push({ type: tname, value: vv, printable: ti < 0 });
|
|
174
|
+
parts.push(_shortName(tname) + "=" + vv);
|
|
175
|
+
}
|
|
176
|
+
return { rdns: rdns, dn: parts.join(",") };
|
|
177
|
+
}
|
|
178
|
+
function _shortName(n) { return n === "commonName" ? "CN" : n === "countryName" ? "C" : n === "organizationName" ? "O" : n === "organizationalUnitName" ? "OU" : n === "localityName" ? "L" : n === "stateOrProvinceName" ? "ST" : n; }
|
|
179
|
+
|
|
180
|
+
// extensions (sec. 3.1.10/sec. 3.3/sec. 8.8): [ * Extension ] | a single keyUsage int-shortcut.
|
|
181
|
+
function _extensions(node) {
|
|
182
|
+
// The keyUsage int-shortcut (sec. 3.1.10): a bare int -> one keyUsage extension, criticality from the
|
|
183
|
+
// sign, value = abs(int) (Appendix A.1.1: the single int 1 -> non-critical keyUsage digitalSignature).
|
|
184
|
+
if (node.majorType === 0 || node.majorType === 1) {
|
|
185
|
+
var iv = Number(cbor.read.int(node));
|
|
186
|
+
return [{ name: "keyUsage", oid: oid.byName("keyUsage"), critical: iv < 0, keyUsageBits: Math.abs(iv) }];
|
|
187
|
+
}
|
|
188
|
+
if (node.majorType !== 4) throw _err("c509/bad-extensions", "C509 extensions must be an array or a keyUsage int shortcut");
|
|
189
|
+
var out = [];
|
|
190
|
+
var kids = node.children || [];
|
|
191
|
+
// Each extension is an (extensionID, extensionValue) pair; an odd-length array is a dangling
|
|
192
|
+
// extension identifier with no value -- reject rather than silently drop the trailing element.
|
|
193
|
+
if (kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a C509 extensions array must be id/value pairs (dangling extension identifier)");
|
|
194
|
+
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
195
|
+
var idNode = kids[i], valNode = kids[i + 1];
|
|
196
|
+
var name, extOid, critical, valContent;
|
|
197
|
+
if (idNode.majorType === 0 || idNode.majorType === 1) {
|
|
198
|
+
var ei = Number(cbor.read.int(idNode));
|
|
199
|
+
name = EXT_BY_INT[Math.abs(ei)];
|
|
200
|
+
if (name === undefined) throw _err("c509/bad-extensions", "extension type integer " + ei + " has no C509 registry row");
|
|
201
|
+
extOid = oid.byName(name); critical = ei < 0;
|
|
202
|
+
// An int extension value is a Defined CBOR item; v1 reconstructs only a byte-string value (a
|
|
203
|
+
// non-byte-string value surfaces as null and fails closed at the type-3 reconstruction).
|
|
204
|
+
valContent = valNode.content;
|
|
205
|
+
} else {
|
|
206
|
+
var r = _oidName(idNode, "c509/bad-extensions", "an extension id");
|
|
207
|
+
name = r.name; extOid = r.oid;
|
|
208
|
+
// ~oid extension (sec. 3.1.10): the extnValue is a byte string -- BARE (non-critical) or wrapped in a
|
|
209
|
+
// single-element array (critical). Validate the shape so a malformed value fails closed at decode.
|
|
210
|
+
critical = valNode.majorType === 4;
|
|
211
|
+
if (critical) {
|
|
212
|
+
if (!valNode.children || valNode.children.length !== 1 || valNode.children[0].majorType !== 2) {
|
|
213
|
+
throw _err("c509/bad-extensions", "a critical ~oid extension value must wrap a single byte string");
|
|
214
|
+
}
|
|
215
|
+
valContent = valNode.children[0].content;
|
|
216
|
+
} else {
|
|
217
|
+
if (valNode.majorType !== 2) throw _err("c509/bad-extensions", "a non-critical ~oid extension value must be a byte string");
|
|
218
|
+
valContent = valNode.content;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
out.push({ name: name, oid: extOid, critical: critical, value: valContent || null });
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- type-3 DER reconstruction (byte-exact inversion; draft-20 sec. 3 / sec. 5) ----
|
|
227
|
+
// Type 3 is an INVERTIBLE transform: the reconstruction reproduces the ORIGINAL DER Certificate
|
|
228
|
+
// byte-for-byte (so the original signature verifies). A field outside the covered set fails closed
|
|
229
|
+
// (c509/non-invertible) -- never a partial or best-effort DER.
|
|
230
|
+
|
|
231
|
+
// A C509 tag-48 MAC (RFC 9542) reconstructs to the commonName EUI-64 string: a 6-byte value is a 48-bit
|
|
232
|
+
// MAC expanded to the EUI-64 HH-HH-HH-FF-FE-HH-HH-HH by inserting FF-FE; an 8-byte value is the EUI-64.
|
|
233
|
+
function _macToEui64String(buf) {
|
|
234
|
+
var bytes = buf.length === 6 ? Buffer.concat([buf.subarray(0, 3), Buffer.from([0xff, 0xfe]), buf.subarray(3)]) : buf;
|
|
235
|
+
var s = [];
|
|
236
|
+
for (var i = 0; i < bytes.length; i++) { var h = bytes[i].toString(16).toUpperCase(); if (h.length < 2) h = "0" + h; s.push(h); }
|
|
237
|
+
return s.join("-");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// One RDN attribute value -> its DER string. The C509 sign convention: a positive attribute int ->
|
|
241
|
+
// utf8String, a negative -> printableString; countryName / serialNumber are PrintableString-restricted.
|
|
242
|
+
function _reconAttrValue(rdn) {
|
|
243
|
+
if (rdn.eui64) return b.utf8(_macToEui64String(rdn.eui64));
|
|
244
|
+
if (rdn.type === "countryName" || rdn.type === "serialNumber") return b.printable(String(rdn.value));
|
|
245
|
+
return rdn.printable ? b.printable(String(rdn.value)) : b.utf8(String(rdn.value));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A Name -> the DER RDNSequence (SEQUENCE OF SET OF SEQUENCE{ type, value }); one attribute per RDN.
|
|
249
|
+
function _reconName(name) {
|
|
250
|
+
return b.sequence(name.rdns.map(function (rdn) {
|
|
251
|
+
return b.set([b.sequence([b.oid(oid.byName(rdn.type)), _reconAttrValue(rdn)])]);
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// A validity instant -> UTCTime (RFC 5280 sec. 4.1.2.5: year < 2050) or GeneralizedTime. A null notAfter
|
|
256
|
+
// -> the no-well-defined-expiry sentinel 99991231235959Z.
|
|
257
|
+
function _reconTime(date) {
|
|
258
|
+
if (date === null) return b.generalizedTime(new Date(Date.UTC(9999, 11, 31, 23, 59, 59)));
|
|
259
|
+
return date.getUTCFullYear() < 2050 ? b.utcTime(date) : b.generalizedTime(date);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// subjectPublicKeyInfo -> DER. EC: rebuild AlgorithmIdentifier{ ecPublicKey, namedCurve } + the BIT
|
|
263
|
+
// STRING point, de-compressing a C509 0xFE/0xFD marker back to the original uncompressed 0x04||X||Y.
|
|
264
|
+
function _reconSpki(spkAlg, keyBytes, rsaKey) {
|
|
265
|
+
if (spkAlg.name === "ecPublicKey") {
|
|
266
|
+
var fieldSize = EC_FIELD_BYTES[spkAlg.curve];
|
|
267
|
+
if (!fieldSize) throw _err("c509/non-invertible", "unsupported EC curve " + spkAlg.curve);
|
|
268
|
+
if (!keyBytes || keyBytes.length === 0) throw _err("c509/non-invertible", "the EC subjectPublicKey byte string is empty");
|
|
269
|
+
var head = keyBytes[0], point;
|
|
270
|
+
// The point length must match the curve field size for its encoding -- an uncompressed 0x04 point is
|
|
271
|
+
// 1 + 2*fieldSize, a compressed 0x02/0x03/0xFE/0xFD point is 1 + fieldSize -- so a truncated / padded
|
|
272
|
+
// point cannot be re-emitted as a valid (or byte-exact) SubjectPublicKeyInfo.
|
|
273
|
+
if (head === 0x04) {
|
|
274
|
+
if (keyBytes.length !== 1 + 2 * fieldSize) throw _err("c509/non-invertible", "uncompressed EC point length " + keyBytes.length + " does not match " + spkAlg.curve);
|
|
275
|
+
point = keyBytes;
|
|
276
|
+
} else if (head === 0x02 || head === 0x03) {
|
|
277
|
+
if (keyBytes.length !== 1 + fieldSize) throw _err("c509/non-invertible", "compressed EC point length " + keyBytes.length + " does not match " + spkAlg.curve);
|
|
278
|
+
point = keyBytes;
|
|
279
|
+
} else if (head === 0xfe || head === 0xfd) { // C509 marker -> de-compress
|
|
280
|
+
if (keyBytes.length !== 1 + fieldSize) throw _err("c509/non-invertible", "C509-marked EC point length " + keyBytes.length + " does not match " + spkAlg.curve);
|
|
281
|
+
var sec1 = Buffer.concat([Buffer.from([head === 0xfe ? 0x02 : 0x03]), keyBytes.subarray(1)]);
|
|
282
|
+
point = webcrypto.decompressEcPoint(sec1, spkAlg.curve, _err, "c509/non-invertible");
|
|
283
|
+
} else throw _err("c509/non-invertible", "unrecognized EC point encoding 0x" + head.toString(16));
|
|
284
|
+
return b.sequence([b.sequence([b.oid(oid.byName("ecPublicKey")), b.oid(oid.byName(spkAlg.curve))]), b.bitString(point, 0)]);
|
|
285
|
+
}
|
|
286
|
+
if (spkAlg.name === "rsaEncryption") {
|
|
287
|
+
// draft-20 sec. 3.2.1: the RSA key is [modulus, exponent] ~biguints, OR just the modulus ~biguint
|
|
288
|
+
// when the exponent is 65537 (parse has already resolved rsaKey to { modulus, exponent }). Reconstruct
|
|
289
|
+
// AlgorithmIdentifier{ rsaEncryption, NULL } + the BIT STRING wrapping RSAPublicKey ::= SEQUENCE {
|
|
290
|
+
// modulus INTEGER, publicExponent INTEGER }.
|
|
291
|
+
var rsaPk = b.sequence([b.integer(rsaKey.modulus), b.integer(rsaKey.exponent)]);
|
|
292
|
+
return b.sequence([b.sequence([b.oid(oid.byName("rsaEncryption")), b.nullValue()]), b.bitString(rsaPk, 0)]);
|
|
293
|
+
}
|
|
294
|
+
throw _err("c509/non-invertible", "subjectPublicKey algorithm " + spkAlg.name + " is not in the type-3 reconstruction covered set");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The keyUsage int value -> the DER KeyUsage BIT STRING (RFC 5280 sec. 4.2.1.3): bit i of the value ->
|
|
298
|
+
// BIT STRING named bit i (bit 0 = digitalSignature = the MSB of the first content octet).
|
|
299
|
+
function _reconKeyUsageBits(value) {
|
|
300
|
+
// The keyUsage value indexes the 9 named bits of RFC 5280 sec. 4.2.1.3 (digitalSignature..decipherOnly);
|
|
301
|
+
// a non-positive, non-integer, or > 0x1FF value is not a valid KeyUsage and would also corrupt the
|
|
302
|
+
// 32-bit bitwise re-encoding below (a value past 2^31 wraps). Fail closed before the bit walk.
|
|
303
|
+
if (!Number.isInteger(value) || value <= 0 || value > 0x1ff) throw _err("c509/non-invertible", "a keyUsage value must be a positive integer within the 9 defined bits");
|
|
304
|
+
var hi = 0; for (var t = value; t; t >>= 1) hi++; // number of significant bits
|
|
305
|
+
hi -= 1; // highest set bit index
|
|
306
|
+
var nbytes = (hi >> 3) + 1;
|
|
307
|
+
var buf = Buffer.alloc(nbytes);
|
|
308
|
+
for (var bit = 0; bit <= hi; bit++) { if (value & (1 << bit)) buf[bit >> 3] |= 0x80 >> (bit & 7); }
|
|
309
|
+
return b.bitString(buf, 7 - (hi & 7));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// extensions -> the [3] EXPLICIT SEQUENCE OF Extension DER.
|
|
313
|
+
function _reconExtensions(exts) {
|
|
314
|
+
var items = exts.map(function (ext) {
|
|
315
|
+
var extnValue;
|
|
316
|
+
if (ext.name === "keyUsage" && typeof ext.keyUsageBits === "number") extnValue = _reconKeyUsageBits(ext.keyUsageBits);
|
|
317
|
+
else if (Buffer.isBuffer(ext.value)) extnValue = ext.value; // raw DER extnValue bytes
|
|
318
|
+
else throw _err("c509/non-invertible", "extension " + ext.name + " has no reconstructable value in the covered set");
|
|
319
|
+
var fields = [b.oid(ext.oid || oid.byName(ext.name))];
|
|
320
|
+
if (ext.critical) fields.push(b.boolean(true));
|
|
321
|
+
fields.push(b.octetString(extnValue));
|
|
322
|
+
return b.sequence(fields);
|
|
323
|
+
});
|
|
324
|
+
return b.explicit(3, b.sequence(items));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// An AlgorithmIdentifier -> DER SEQUENCE { algorithm OID, parameters? }. A C509 [~oid, params]
|
|
328
|
+
// algorithm carries its DER parameters bytes, which MUST be reproduced so the reconstruction inverts
|
|
329
|
+
// byte-exact (silently dropping them would change the signed bytes); the int / ~oid forms carry no
|
|
330
|
+
// parameters (ecdsaWith* and the like omit them).
|
|
331
|
+
function _reconAlgId(alg) {
|
|
332
|
+
var fields = [b.oid(alg.oid)];
|
|
333
|
+
if (alg.parameters && alg.parameters.length) {
|
|
334
|
+
// AlgorithmIdentifier.parameters is ANY -- exactly one well-formed DER element. Validate the supplied
|
|
335
|
+
// bytes decode as a single element (the strict decoder rejects trailing bytes / malformed encodings)
|
|
336
|
+
// before re-emitting them, so a malformed or multi-element parameter blob fails closed rather than
|
|
337
|
+
// producing an invalid reconstructed AlgorithmIdentifier.
|
|
338
|
+
try { asn1.decode(alg.parameters); }
|
|
339
|
+
catch (e) { throw _err("c509/non-invertible", "algorithm parameters are not a single well-formed DER element", e); }
|
|
340
|
+
fields.push(b.raw(alg.parameters));
|
|
341
|
+
}
|
|
342
|
+
return b.sequence(fields);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The full type-3 -> DER Certificate reconstruction, byte-for-byte.
|
|
346
|
+
function _reconstructDer(r, sigNode) {
|
|
347
|
+
var sigAlgSeq = _reconAlgId(r.signatureAlgorithm);
|
|
348
|
+
var tbsFields = [
|
|
349
|
+
b.explicit(0, b.integer(2n)), // version v3 (type-3 is X.509 v3)
|
|
350
|
+
b.integer(r.serialNumber),
|
|
351
|
+
sigAlgSeq,
|
|
352
|
+
_reconName(r.issuer && r.issuer.rdns ? r.issuer : r.subject), // null issuer -> issuer == subject
|
|
353
|
+
b.sequence([_reconTime(r.validity.notBefore), _reconTime(r.validity.notAfter)]),
|
|
354
|
+
_reconName(r.subject),
|
|
355
|
+
_reconSpki(r.subjectPublicKeyAlgorithm, r.subjectPublicKey, r.rsaPublicKey),
|
|
356
|
+
];
|
|
357
|
+
// RFC 5280 sec. 4.1: the [3] extensions field is OPTIONAL and, when present, SHALL contain at least one
|
|
358
|
+
// extension -- an empty C509 extensions array reconstructs to an OMITTED field, not an empty SEQUENCE.
|
|
359
|
+
if (r.extensions.length) tbsFields.push(_reconExtensions(r.extensions));
|
|
360
|
+
var tbs = b.sequence(tbsFields);
|
|
361
|
+
// The signature is re-wrapped as a DER ECDSA-Sig-Value from the fixed-width r||s, so only an ECDSA
|
|
362
|
+
// signature algorithm is in the type-3 reconstruction covered set (an RSA/EdDSA signature is raw bytes,
|
|
363
|
+
// not r||s -- rejected rather than mis-wrapped). A wrong-length r||s surfaces the caller's typed code.
|
|
364
|
+
if (!/^ecdsa/i.test(r.signatureAlgorithm.name || "")) {
|
|
365
|
+
throw _err("c509/non-invertible", "type-3 signature reconstruction covers only ECDSA; got " + r.signatureAlgorithm.name);
|
|
366
|
+
}
|
|
367
|
+
// The fixed-width r||s must split at a real curve field width -- P-256/384/521 = 64/96/132 bytes
|
|
368
|
+
// (RFC 9053 sec. 2.1). A width that is not 2x a supported field size is not a valid ECDSA signature and
|
|
369
|
+
// cannot be re-wrapped byte-exact; surface the caller's typed code rather than split at a bogus offset.
|
|
370
|
+
var coordLen = r.signatureValue.length / 2;
|
|
371
|
+
if (coordLen !== 32 && coordLen !== 48 && coordLen !== 66) {
|
|
372
|
+
throw _err("c509/bad-signature", "the type-3 ECDSA signature width " + r.signatureValue.length + " is not a valid fixed-width r||s (expected 64/96/132 for P-256/384/521)");
|
|
373
|
+
}
|
|
374
|
+
var sigValue = validator.sig.rawToEcdsaDer(r.signatureValue, coordLen);
|
|
375
|
+
return b.sequence([tbs, sigAlgSeq, b.bitString(sigValue, 0)]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---- the parse ----
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @primitive pki.schema.c509.parse
|
|
382
|
+
* @signature pki.schema.c509.parse(bytes) -> { certificateType, serialNumber, serialNumberHex, ... }
|
|
383
|
+
* @since 0.2.30
|
|
384
|
+
* @status experimental
|
|
385
|
+
* @spec draft-ietf-cose-cbor-encoded-cert, RFC 8949, RFC 9090, RFC 5280
|
|
386
|
+
*
|
|
387
|
+
* Decode a C509 certificate (draft-ietf-cose-cbor-encoded-cert) from its deterministic-CBOR bytes.
|
|
388
|
+
* Returns the decoded fields (c509CertificateType 2 native or 3 re-encoded); a malformed shape throws a
|
|
389
|
+
* typed C509Error carrying the inner cbor/asn1 fault as .cause. It decodes CBOR, not DER, so it is
|
|
390
|
+
* reached by an explicit call and is not auto-routed by pki.schema.parse. The type-2 signedData and the
|
|
391
|
+
* raw signature are surfaced RAW (a native verifier hashes them without re-serialization).
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* // the RFC 7925 profiled certificate from draft-ietf-cose-cbor-encoded-cert Appendix A.1 (type 3)
|
|
395
|
+
* var bytes = Buffer.from(
|
|
396
|
+
* "8b03" + "4301f50d" + "00" + "6b52464320746573742043" + "41" + "1a63b0cd00" + "1a6955b900" +
|
|
397
|
+
* "d830460123456789ab" + "01" + "5821feb1216ab96e5b3b3340f5bdf02e693f16213a04525ed44450b1019c2dfd3838ab" +
|
|
398
|
+
* "01" + "5840d4320b1d6849e309219d30037e138166f2508247dddae76ccceea55053c108e90d551f6d60106f1abb484cfbe6256c178e4ac3314ea19191e8b607da5ae3bda16",
|
|
399
|
+
* "hex");
|
|
400
|
+
* var c = pki.schema.c509.parse(bytes);
|
|
401
|
+
* c.certificateType; // 3
|
|
402
|
+
*/
|
|
403
|
+
function parse(input) {
|
|
404
|
+
var root;
|
|
405
|
+
try { root = cbor.decode(input); }
|
|
406
|
+
catch (e) { throw _err("c509/not-a-certificate", "the input is not well-formed deterministic CBOR", e); }
|
|
407
|
+
if (root.majorType !== 4 || !root.children) throw _err("c509/not-a-certificate", "a C509 certificate must be a CBOR array");
|
|
408
|
+
var f = root.children;
|
|
409
|
+
if (f.length !== 11) throw _err("c509/bad-tbs", "a C509 certificate must be an array of exactly 11 elements, got " + f.length);
|
|
410
|
+
|
|
411
|
+
var type = Number(cbor.read.int(f[0]));
|
|
412
|
+
if (type !== 2 && type !== 3) throw _err("c509/bad-certificate-type", "c509CertificateType must be 2 (native) or 3 (re-encoded), got " + type);
|
|
413
|
+
|
|
414
|
+
var serialBytes = f[1];
|
|
415
|
+
var serial = _biguint(serialBytes, "c509/bad-serial", "certificateSerialNumber");
|
|
416
|
+
var sHex = serialBytes.content.toString("hex");
|
|
417
|
+
|
|
418
|
+
var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm");
|
|
419
|
+
var issuer = _name509(f[3], false);
|
|
420
|
+
var notBefore = _time(f[4], false, "validityNotBefore");
|
|
421
|
+
var notAfter = _time(f[5], true, "validityNotAfter");
|
|
422
|
+
var subject = _name509(f[6], true);
|
|
423
|
+
var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm");
|
|
424
|
+
var subjectPublicKey = null, rsaKey = null;
|
|
425
|
+
if (spkAlg.name === "rsaEncryption") {
|
|
426
|
+
// draft-20 sec. 3.2.1: [modulus, exponent] ~biguints, OR a bare modulus ~biguint (exponent = 65537).
|
|
427
|
+
if (f[8].majorType === 2) rsaKey = { modulus: _biguint(f[8], "c509/bad-spki", "RSA modulus"), exponent: 65537n };
|
|
428
|
+
else if (f[8].majorType === 4 && f[8].children && f[8].children.length === 2) {
|
|
429
|
+
rsaKey = { modulus: _biguint(f[8].children[0], "c509/bad-spki", "RSA modulus"), exponent: _biguint(f[8].children[1], "c509/bad-spki", "RSA exponent") };
|
|
430
|
+
} else throw _err("c509/bad-spki", "an RSA subjectPublicKey must be a ~biguint modulus or [modulus, exponent]");
|
|
431
|
+
if (rsaKey.modulus < 1n || rsaKey.exponent < 1n) throw _err("c509/bad-spki", "an RSA modulus and public exponent must be positive");
|
|
432
|
+
} else {
|
|
433
|
+
if (f[8].majorType !== 2) throw _err("c509/bad-spki", "subjectPublicKey must be a CBOR byte string");
|
|
434
|
+
subjectPublicKey = f[8].content;
|
|
435
|
+
}
|
|
436
|
+
var extensions = _extensions(f[9]);
|
|
437
|
+
if (f[10].majorType !== 2) throw _err("c509/bad-signature", "issuerSignatureValue must be a CBOR byte string");
|
|
438
|
+
var signatureValue = f[10].content;
|
|
439
|
+
|
|
440
|
+
var result = {
|
|
441
|
+
certificateType: type,
|
|
442
|
+
serialNumber: serial,
|
|
443
|
+
serialNumberHex: sHex,
|
|
444
|
+
signatureAlgorithm: sigAlg,
|
|
445
|
+
issuer: issuer,
|
|
446
|
+
validity: { notBefore: notBefore, notAfter: notAfter },
|
|
447
|
+
subject: subject,
|
|
448
|
+
subjectPublicKeyAlgorithm: spkAlg,
|
|
449
|
+
subjectPublicKey: subjectPublicKey,
|
|
450
|
+
rsaPublicKey: rsaKey,
|
|
451
|
+
extensions: extensions,
|
|
452
|
+
signatureValue: signatureValue,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Native (type-2) signed region (sec. 3.1.12): the RAW bytes of the CBOR-sequence elements 0..9 (NOT
|
|
456
|
+
// the outer array head, NOT the signature) -- a zero-copy subarray a native verifier hashes.
|
|
457
|
+
if (type === 2) {
|
|
458
|
+
var b0 = f[0].bytes, b9 = f[9].bytes;
|
|
459
|
+
var start = b0.byteOffset - input.byteOffset;
|
|
460
|
+
var end = b9.byteOffset - input.byteOffset + b9.length;
|
|
461
|
+
result.signedData = input.subarray(start, end);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Type-3 is an invertible re-encoding of a DER X.509 certificate: reconstruct the original DER
|
|
465
|
+
// byte-for-byte so the original signature verifies and x509.parse recovers the certificate.
|
|
466
|
+
if (type === 3) result.reconstructedDer = _reconstructDer(result, f[10]);
|
|
467
|
+
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// matches(node) -- a STRUCTURAL probe over a DECODED CBOR node: an array of 11 whose first element is
|
|
472
|
+
// a major-type-0/1 int equal to 2 or 3. Not wired into the DER orchestrator (C509 is CBOR, not DER).
|
|
473
|
+
function matches(node) {
|
|
474
|
+
return !!node && node.majorType === 4 && !!node.children && node.children.length === 11 &&
|
|
475
|
+
(node.children[0].majorType === 0 || node.children[0].majorType === 1) &&
|
|
476
|
+
(Number(node.children[0].argument) === 2 || Number(node.children[0].argument) === 3);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = { parse: parse, matches: matches };
|
package/lib/smime.js
CHANGED
|
@@ -168,7 +168,7 @@ async function sign(content, signers, opts) {
|
|
|
168
168
|
// Coverage residual: the throw only fires on a >16 MiB assembled message -- a real cap, exercised by no
|
|
169
169
|
// unit vector (driving it would sign 16 MiB).
|
|
170
170
|
function _capped(msg) {
|
|
171
|
-
|
|
171
|
+
guard.limits.byteCap(msg, C.LIMITS.MIME_MAX_BYTES, _err, "smime/too-large", "the assembled S/MIME message");
|
|
172
172
|
return msg;
|
|
173
173
|
}
|
|
174
174
|
|
package/lib/tsp-sign.js
CHANGED
|
@@ -119,9 +119,7 @@ function sign(messageImprint, tsa, opts) {
|
|
|
119
119
|
var imprint = b.sequence([_hashAlgId(mi.hashAlgorithm), b.octetString(Buffer.from(mi.hashedMessage))]);
|
|
120
120
|
// genTime defaults to now; a supplied value MUST be a valid Date (never a silently-ignored
|
|
121
121
|
// non-Date or an Invalid Date that would encode a garbage GeneralizedTime).
|
|
122
|
-
if (opts.genTime != null
|
|
123
|
-
throw _err("tsp/bad-input", "genTime must be a valid Date");
|
|
124
|
-
}
|
|
122
|
+
if (opts.genTime != null) guard.time.assertValid(opts.genTime, _err, "tsp/bad-input", "genTime");
|
|
125
123
|
var genTime = opts.genTime instanceof Date ? opts.genTime : new Date();
|
|
126
124
|
var fields = [b.integer(1n), _policy(opts.policy), imprint, b.integer(BigInt(opts.serialNumber)), b.generalizedTime(genTime)];
|
|
127
125
|
if (opts.accuracy) fields.push(_accuracy(opts.accuracy));
|
package/lib/validator-all.js
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
// -- the COMPLETE WebAuthn credential COSE_Key rule
|
|
16
16
|
// set (RFC 9052/9053 + WebAuthn sec. 6.5.1 +
|
|
17
17
|
// CTAP2 canonical + on-curve), one home
|
|
18
|
-
// validator.sig.
|
|
19
|
-
// (RFC 3279 + X.690 strict-DER
|
|
18
|
+
// validator.sig.ecdsaDerToP1363 -- the COMPLETE order-aware DER ECDSA-Sig-Value
|
|
19
|
+
// conformance (RFC 3279 + X.690 strict-DER +
|
|
20
|
+
// FIPS 186-5 [1,n-1], CVE-2022-21449) + raw r||s
|
|
20
21
|
// validator.attcert.packedCert / .aikCert / .aaguidExt / .requireV3 / .assertNotCa
|
|
21
22
|
// -- the WebAuthn attestation-certificate profile
|
|
22
23
|
// (sec. 8.2.1 packed, sec. 8.3.1 TPM AIK), one home
|
package/lib/validator-sig.js
CHANGED
|
@@ -25,42 +25,13 @@
|
|
|
25
25
|
|
|
26
26
|
var asn1 = require("./asn1-der");
|
|
27
27
|
|
|
28
|
-
// ecdsaSigToRaw(der, coordLen, E, code) -> the validated signature as raw r||s, each
|
|
29
|
-
// coordinate left-padded to coordLen bytes, or throws new E(code, ...). The complete DER
|
|
30
|
-
// ECDSA-Sig-Value conformance gate; a verifier MUST route an ECDSA signature through here,
|
|
31
|
-
// never hand-decode the SEQUENCE and read r/s content raw (which skips minimality).
|
|
32
|
-
// @enforced-by behavioral -- a DER ECDSA-Sig-Value decode has no rename-proof code shape
|
|
33
|
-
// distinct from generic 2-child-SEQUENCE content access; the RED conformance vectors
|
|
34
|
-
// (non-minimal / negative / zero / over-size r or s rejected) and the webauthn ECDSA KATs
|
|
35
|
-
// are the guard.
|
|
36
|
-
function ecdsaSigToRaw(der, coordLen, E, code) {
|
|
37
|
-
var node;
|
|
38
|
-
try { node = asn1.decode(der); } catch (e) { throw new E(code, "ECDSA signature is not a DER SEQUENCE", e); }
|
|
39
|
-
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2) {
|
|
40
|
-
throw new E(code, "ECDSA signature must be a DER SEQUENCE { r, s }");
|
|
41
|
-
}
|
|
42
|
-
function coord(c, label) {
|
|
43
|
-
// The strict DER integer reader enforces PRIMITIVE + MINIMAL encoding (a constructed
|
|
44
|
-
// child, an empty INTEGER, or a redundant 0x00/0xFF sign octet all throw). The value
|
|
45
|
-
// is then range-checked: r and s MUST be positive (>= 1) and fit the curve field size.
|
|
46
|
-
var v;
|
|
47
|
-
try { v = asn1.read.integer(c); } catch (e) { throw new E(code, "ECDSA signature " + label + " is not a minimally-encoded DER INTEGER", e); }
|
|
48
|
-
if (v <= 0n) throw new E(code, "ECDSA signature " + label + " must be a positive integer");
|
|
49
|
-
var hex = v.toString(16); if (hex.length % 2) hex = "0" + hex;
|
|
50
|
-
var b = Buffer.from(hex, "hex");
|
|
51
|
-
if (b.length > coordLen) throw new E(code, "ECDSA signature " + label + " exceeds the curve field size");
|
|
52
|
-
var out = Buffer.alloc(coordLen); b.copy(out, coordLen - b.length); return out;
|
|
53
|
-
}
|
|
54
|
-
return Buffer.concat([coord(node.children[0], "r"), coord(node.children[1], "s")]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
28
|
// rawToEcdsaDer(raw, coordLen) -> the canonical DER ECDSA-Sig-Value SEQUENCE { r, s } for a raw
|
|
58
|
-
// r||s signature (IEEE P1363, the WebCrypto sign() output). The inverse of
|
|
29
|
+
// r||s signature (IEEE P1363, the WebCrypto sign() output). The inverse of ecdsaDerToP1363: a
|
|
59
30
|
// signer composes this so the emitted ECDSA signature is canonical DER (minimally-encoded
|
|
60
31
|
// INTEGERs via the build layer), never a hand-built SEQUENCE that could re-introduce a
|
|
61
32
|
// non-minimal encoding. A config-time TypeError guards a mis-sized raw signature at entry.
|
|
62
33
|
// @enforced-by behavioral -- a DER ECDSA-Sig-Value BUILD has no rename-proof code shape distinct
|
|
63
|
-
// from generic sequence([integer,integer]); the round-trip vectors (build ->
|
|
34
|
+
// from generic sequence([integer,integer]); the round-trip vectors (build -> ecdsaDerToP1363
|
|
64
35
|
// identity) and the cms.sign ECDSA KATs are the guard.
|
|
65
36
|
function rawToEcdsaDer(raw, coordLen) {
|
|
66
37
|
if (!Buffer.isBuffer(raw) || typeof coordLen !== "number" || coordLen <= 0 || raw.length !== coordLen * 2) {
|
|
@@ -82,12 +53,15 @@ var CURVE_ORDER = {
|
|
|
82
53
|
// ecdsaDerToP1363(der, curve, E, code) -> the DER ECDSA-Sig-Value converted to raw r||s (P1363),
|
|
83
54
|
// each coordinate left-padded to the curve field width, rejecting r or s outside [1, n-1] against
|
|
84
55
|
// the curve ORDER (CVE-2022-21449 "Psychic Signatures" -- the r/s = 0 case AND the >= n upper
|
|
85
|
-
// bound). `curve` is a WebCrypto namedCurve (P-256/384/521). This is the
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
56
|
+
// bound). `curve` is a WebCrypto namedCurve (P-256/384/521). This is the SINGLE ECDSA-Sig-Value
|
|
57
|
+
// conformance gate every curve-aware verifier routes through: it enforces the complete strict-DER
|
|
58
|
+
// rule set (2-INTEGER SEQUENCE, minimal encoding) AND the order bound, so a verifier never
|
|
59
|
+
// hand-decodes the SEQUENCE (skipping minimality) nor bounds r/s only by the field size (missing
|
|
60
|
+
// the order). A non-minimal, negative, zero, over-size, or >= n coordinate fails closed.
|
|
61
|
+
// @enforced-by behavioral -- a DER ECDSA-Sig-Value decode has no rename-proof code shape distinct
|
|
62
|
+
// from generic 2-child-SEQUENCE content access; the RED conformance vectors (r/s = 0, r/s >= n,
|
|
63
|
+
// non-minimal, negative, over-size) and the cms / ct / composite / path / webauthn ECDSA KATs are
|
|
64
|
+
// the guard.
|
|
91
65
|
function ecdsaDerToP1363(der, curve, E, code) {
|
|
92
66
|
var width = CURVE_FIELD_BYTES[curve];
|
|
93
67
|
var order = CURVE_ORDER[curve];
|
|
@@ -98,8 +72,12 @@ function ecdsaDerToP1363(der, curve, E, code) {
|
|
|
98
72
|
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
|
|
99
73
|
throw new E(code, "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
|
|
100
74
|
}
|
|
101
|
-
|
|
102
|
-
|
|
75
|
+
// Wrap the strict DER integer reads (they enforce PRIMITIVE + MINIMAL encoding) so a non-minimal /
|
|
76
|
+
// constructed / empty INTEGER surfaces the CALLER's typed code, not a raw asn1/* -- the complete DER
|
|
77
|
+
// conformance rule set, gated together with the order bound below.
|
|
78
|
+
var r, s;
|
|
79
|
+
try { r = asn1.read.integer(n.children[0]); } catch (e) { throw new E(code, "ECDSA signature r is not a minimally-encoded DER INTEGER", e); }
|
|
80
|
+
try { s = asn1.read.integer(n.children[1]); } catch (e) { throw new E(code, "ECDSA signature s is not a minimally-encoded DER INTEGER", e); }
|
|
103
81
|
if (r < 1n || s < 1n || r >= order || s >= order) {
|
|
104
82
|
throw new E(code, "ECDSA signature component out of range [1, n-1] (CVE-2022-21449)");
|
|
105
83
|
}
|
|
@@ -118,4 +96,4 @@ function ecdsaDerToP1363(der, curve, E, code) {
|
|
|
118
96
|
return Buffer.concat([pad(r), pad(s)]);
|
|
119
97
|
}
|
|
120
98
|
|
|
121
|
-
module.exports = {
|
|
99
|
+
module.exports = { rawToEcdsaDer: rawToEcdsaDer, ecdsaDerToP1363: ecdsaDerToP1363 };
|
package/lib/webauthn.js
CHANGED
|
@@ -169,9 +169,9 @@ var COSE_ALG = {
|
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
// DER ECDSA-Sig-Value { r, s } -> raw r||s (the ieee-p1363 form WebCrypto verify expects).
|
|
172
|
-
// validator-sig owns the complete strict-DER conformance
|
|
173
|
-
//
|
|
174
|
-
function _derEcdsaToRaw(der,
|
|
172
|
+
// validator-sig owns the complete strict-DER conformance PLUS the FIPS 186-5 sec. 6.4.2 order bound
|
|
173
|
+
// (r,s in [1, n-1] -- the order-aware gate a curve-aware verifier MUST use), composed here.
|
|
174
|
+
function _derEcdsaToRaw(der, curve) { return validator.sig.ecdsaDerToP1363(der, curve, WebauthnError, "webauthn/bad-signature"); }
|
|
175
175
|
|
|
176
176
|
// Verify `sig` over `message` with the SPKI public key `spkiBytes` under COSE `alg`.
|
|
177
177
|
// A wrong signature resolves `false` from subtle.verify without throwing (a false
|
|
@@ -189,7 +189,7 @@ function _verifySig(alg, sig, spkiBytes, message, E) {
|
|
|
189
189
|
// point before verify. This covers EVERY key that signs a WebAuthn statement: the x5c
|
|
190
190
|
// attestation-certificate key (packed/tpm/apple) AND the self-attestation credential key.
|
|
191
191
|
if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name, E);
|
|
192
|
-
var s = d.ecdsa ? _derEcdsaToRaw(sig, d.
|
|
192
|
+
var s = d.ecdsa ? _derEcdsaToRaw(sig, d.imp.namedCurve) : sig;
|
|
193
193
|
return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
|
|
194
194
|
.then(function (key) { return subtle.verify(ver, key, s, message); })
|
|
195
195
|
.catch(function (e) { throw _err("webauthn/verify-error", "the attestation signature could not be evaluated", e); });
|
package/lib/webcrypto.js
CHANGED
|
@@ -1000,10 +1000,27 @@ Crypto.prototype.getRandomValues = function getRandomValues(typedArray) {
|
|
|
1000
1000
|
|
|
1001
1001
|
Crypto.prototype.randomUUID = function randomUUID() { return nodeCrypto.randomUUID(); };
|
|
1002
1002
|
|
|
1003
|
+
// decompressEcPoint(sec1Compressed, nodeCurve) -> the uncompressed SEC1 point 0x04||X||Y for a
|
|
1004
|
+
// compressed 0x02/0x03||X input, computing Y on-curve via node's ECDH.convertKey. The crypto-engine
|
|
1005
|
+
// home for EC point de-compression (Hard rule #8: EC key material is webcrypto's domain). Fail-closed:
|
|
1006
|
+
// convertKey throws on an off-curve X or an unsupported curve, surfaced as the caller's typed error.
|
|
1007
|
+
// `nodeCurve` is the OpenSSL curve name (prime256v1 / secp384r1 / secp521r1). The C509-specific
|
|
1008
|
+
// 0xFE/0xFD marker -> 0x02/0x03 parity translation stays in the C509 layer (this takes a real SEC1 point).
|
|
1009
|
+
function decompressEcPoint(sec1Compressed, nodeCurve, E, code) {
|
|
1010
|
+
var head = sec1Compressed[0];
|
|
1011
|
+
if (head !== 0x02 && head !== 0x03) throw E(code, "EC point de-compression expects a compressed SEC1 point (0x02/0x03)");
|
|
1012
|
+
try {
|
|
1013
|
+
return nodeCrypto.ECDH.convertKey(sec1Compressed, nodeCurve, undefined, undefined, "uncompressed");
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
throw E(code, "EC point is not on curve " + nodeCurve + " (de-compression failed)", e);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1003
1019
|
module.exports = {
|
|
1004
1020
|
webcrypto: new Crypto(),
|
|
1005
1021
|
Crypto: Crypto,
|
|
1006
1022
|
SubtleCrypto: SubtleCrypto,
|
|
1007
1023
|
CryptoKey: CryptoKey,
|
|
1008
1024
|
WebCryptoError: WebCryptoError,
|
|
1025
|
+
decompressEcPoint: decompressEcPoint,
|
|
1009
1026
|
};
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:fcac8430-97c8-4f4c-a497-dc2f02fb3c6f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T18:34:40.509Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.30",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.30",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.30",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.30",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|