@blamejs/pki 0.2.29 → 0.2.31
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 +25 -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/ct.js +7 -10
- 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/inspect.js +18 -0
- package/lib/lint.js +11 -1
- package/lib/oid.js +14 -1
- package/lib/path-validate.js +17 -6
- package/lib/schema-all.js +4 -0
- package/lib/schema-c509.js +479 -0
- package/lib/schema-pkix.js +117 -1
- package/lib/sigstore.js +12 -1
- 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 +14 -40
- package/lib/webcrypto.js +17 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.2.31 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
X.509 certificates now decode the RFC 3739 / ETSI EN 319 412-5 qualified-certificate qcStatements extension, and the sigstore verifier gains a low-order EdDSA public-key gate.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The toolkit decodes the RFC 3739 sec. 3.2.6 / ETSI EN 319 412-5 qualified-certificate qcStatements extension (id-pe-qcStatements). The decoded statements -- QcCompliance (EU-qualified), QcLimitValue (reliance limit), QcSSCD (key in a QSCD), QcType (certificate purpose: esign / eseal / web), QcRetentionPeriod, QcPDS (disclosure-statement URLs), QcCClegislation (country of qualification), QcIdentMethod, QcQSCDlegislation, and the PKIX SemanticsInformation -- are decoded and validated by the toolkit's certificate-extension decoders and rendered by pki.inspect. An unknown statementId is preserved opaque (its raw bytes, semantics not executed); a malformed shape fails closed with a typed error.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- The sigstore keyless-bundle verifier now routes an Ed25519/Ed448 Fulcio leaf or Rekor log public key through the shared full-order Edwards-point gate before verification. A low-order OKP key (which node imports without complaint and which can verify a forged EdDSA signature) is rejected at key-parse (sigstore/bad-key) instead of being handed to verify -- the same defense the webauthn and certification-path EdDSA verifiers already apply.
|
|
18
|
+
- Certification-path validation rejects a CRITICAL qcStatements extension (RFC 5280 sec. 6.1.4). A critical qualified-certificate statement asserts semantics a relying party must enforce -- a QcLimitValue reliance limit, a QcType certificate purpose -- and the validator does not enforce them, so treating the extension as processed would let a caller rely on a certificate outside its asserted qualified-certificate constraints; it is left unprocessed and fails as an unrecognized critical extension. A non-critical qcStatements is informational and does not affect the verdict; the extension is still decoded for pki.inspect and lint, and the linter flags a critical qcStatements as an unrecognized critical extension for the same reason -- structural decodability is not validation processing.
|
|
19
|
+
|
|
20
|
+
## v0.2.30 — 2026-07-16
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- 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.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- 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.
|
|
31
|
+
|
|
7
32
|
## v0.2.29 — 2026-07-16
|
|
8
33
|
|
|
9
34
|
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.
|
package/README.md
CHANGED
|
@@ -203,7 +203,8 @@ is callable today; nothing below is a stub.
|
|
|
203
203
|
| `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
|
|
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
|
-
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
|
|
206
|
+
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque), fail-closed — `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` |
|
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/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) {
|
|
@@ -777,12 +775,11 @@ async function verifySctWithLogList(entry, sct, logList, opts) {
|
|
|
777
775
|
// Temporal gate (fail-closed): the covered cert's notAfter must be in [start_inclusive, end_exclusive).
|
|
778
776
|
if (log.temporalInterval) {
|
|
779
777
|
var notAfter = _resolveNotAfter(entry, opts);
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
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)")) {
|
|
786
783
|
throw _ctErr("ct/temporal-interval", "the covered certificate's notAfter is outside the CT log's temporal_interval");
|
|
787
784
|
}
|
|
788
785
|
}
|
|
@@ -815,7 +812,7 @@ async function verifyLogListSignature(json, signature, publicKey) {
|
|
|
815
812
|
var message = typeof json === "string" ? Buffer.from(json) : _toBuffer(json, "the CT log list JSON");
|
|
816
813
|
// Bound the signed message before the digest/verify (the same cap parseLogList enforces) so a hostile
|
|
817
814
|
// caller cannot force unbounded hashing work on an oversized input (CWE-400).
|
|
818
|
-
|
|
815
|
+
guard.limits.byteCap(message, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/too-large", "the CT log list");
|
|
819
816
|
var sig = _toBuffer(signature, "the CT log list signature");
|
|
820
817
|
var spki = _toBuffer(publicKey, "the CT log list signer public key (SPKI)");
|
|
821
818
|
var alg = _spkiAlg(spki); // fail-closed: a non-SPKI key or a forgeable RSA e < 3 throws ct/bad-input
|
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/inspect.js
CHANGED
|
@@ -317,6 +317,24 @@ var EXT_RENDERERS = {
|
|
|
317
317
|
if (decoded.pathLenConstraint != null) s += ", pathlen:" + decoded.pathLenConstraint;
|
|
318
318
|
return inner + s;
|
|
319
319
|
},
|
|
320
|
+
qcStatements: function (decoded, inner) {
|
|
321
|
+
return decoded.map(function (s) {
|
|
322
|
+
var label = inner + (s.name || s.statementId), info = s.info;
|
|
323
|
+
if (!info) return label; // presence-only (QcCompliance / QcSSCD)
|
|
324
|
+
if (info.opaque) return label + " (opaque)";
|
|
325
|
+
if (typeof info.amount !== "undefined") return label + ": " + info.amount + " " + info.currency + (info.exponent ? " x10^" + info.exponent : ""); // QcLimitValue
|
|
326
|
+
if (typeof info.years !== "undefined") return label + ": " + info.years + " years"; // QcRetentionPeriod
|
|
327
|
+
if (info.typeNames) return label + ": " + info.typeNames.map(function (n, i) { return n || info.types[i]; }).join(", "); // QcType
|
|
328
|
+
if (info.methodNames) return label + ": " + info.methodNames.map(function (n, i) { return n || info.methods[i]; }).join(", "); // QcIdentMethod
|
|
329
|
+
if (info.locations) return label + ": " + info.locations.map(function (l) { return l.url + " (" + l.language + ")"; }).join(", "); // QcPDS
|
|
330
|
+
if (info.countries) return label + ": " + info.countries.join(", "); // QcCClegislation / QcQSCDlegislation
|
|
331
|
+
if (typeof info.semanticsIdentifier !== "undefined") { // SemanticsInformation (id-qcs)
|
|
332
|
+
var nra = info.nameRegistrationAuthorities && info.nameRegistrationAuthorities.length;
|
|
333
|
+
return label + (info.semanticsIdentifier ? ": " + info.semanticsIdentifier : "") + (nra ? " (" + nra + " NRA)" : "");
|
|
334
|
+
}
|
|
335
|
+
return label;
|
|
336
|
+
}).join("\n");
|
|
337
|
+
},
|
|
320
338
|
subjectAltName: _renderAltName,
|
|
321
339
|
issuerAltName: _renderAltName,
|
|
322
340
|
certificatePolicies: function (decoded, inner) {
|
package/lib/lint.js
CHANGED
|
@@ -244,7 +244,17 @@ function _subjectCNs(cert) {
|
|
|
244
244
|
// policyMappings, which the table decodes) and correctly scoped to EXTENSION OIDs: an
|
|
245
245
|
// algorithm / EKU-purpose OID the name registry happens to resolve is NOT in the decoder
|
|
246
246
|
// table, so a critical extension carrying such an OID is still flagged.
|
|
247
|
-
|
|
247
|
+
//
|
|
248
|
+
// The one exception: an extension the table DECODES for display but whose CRITICAL semantics
|
|
249
|
+
// the toolkit does not enforce. qcStatements decodes (for pki.inspect / this linter) yet a
|
|
250
|
+
// critical instance asserts qualified-certificate constraints (a reliance limit, a certificate
|
|
251
|
+
// purpose) no consumer here processes -- path-validate rejects a critical qcStatements as
|
|
252
|
+
// unrecognized-critical for exactly that reason. Structural decodability is not validation
|
|
253
|
+
// processing, so a critical qcStatements is still flagged here, keeping lint consistent with
|
|
254
|
+
// certification-path validation.
|
|
255
|
+
var _DECODE_ONLY_UNPROCESSED_CRITICAL = {};
|
|
256
|
+
_DECODE_ONLY_UNPROCESSED_CRITICAL[oid.byName("qcStatements")] = true;
|
|
257
|
+
function _isUnknownExtension(extOid) { return !EXT_DECODERS[extOid] || _DECODE_ONLY_UNPROCESSED_CRITICAL[extOid] === true; }
|
|
248
258
|
|
|
249
259
|
// RSA-modulus / EC-curve helpers for the weak-key lint. Each returns null when the key
|
|
250
260
|
// material cannot be read as expected (a malformed RSA modulus, or EC parameters that are
|
package/lib/oid.js
CHANGED
|
@@ -80,7 +80,20 @@ var FAMILIES = {
|
|
|
80
80
|
// aaControls sec. 7.4, ac-proxying sec. 7.2).
|
|
81
81
|
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
|
|
82
82
|
authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10,
|
|
83
|
-
acmeIdentifier: 31
|
|
83
|
+
acmeIdentifier: 31,
|
|
84
|
+
// id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
|
|
85
|
+
qcStatements: 3 } },
|
|
86
|
+
|
|
87
|
+
// RFC 3739 sec. 3.2.6.1 id-qcs -- the PKIX QCStatement statementIds carrying SemanticsInformation.
|
|
88
|
+
pkixQcSyntax: { base: [1, 3, 6, 1, 5, 5, 7, 11], of: { qcsPkixQCSyntaxV1: 1, qcsPkixQCSyntaxV2: 2 } },
|
|
89
|
+
|
|
90
|
+
// ETSI EN 319 412-5 qualified-certificate statements (esi4-qcStatement-N) on the id-etsi-qcs arc.
|
|
91
|
+
etsiQcs: { base: [0, 4, 0, 1862, 1], of: {
|
|
92
|
+
qcCompliance: 1, qcLimitValue: 2, qcRetentionPeriod: 3, qcSSCD: 4, qcPDS: 5,
|
|
93
|
+
qcType: 6, qcCClegislation: 7, qcIdentMethod: 8, qcQSCDlegislation: 9 } },
|
|
94
|
+
|
|
95
|
+
// ETSI EN 319 412-5 QcType member OIDs (the certificate-purpose types) on the id-etsi-qct arc.
|
|
96
|
+
etsiQcType: { base: [0, 4, 0, 1862, 1, 6], of: { qctEsign: 1, qctEseal: 2, qctWeb: 3 } },
|
|
84
97
|
|
|
85
98
|
// id-aca -- RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
|
|
86
99
|
// authenticationInfo sec. 4.4.1 .. group sec. 4.4.4, plus encAttrs sec. 7.1 (the encrypted-
|
package/lib/path-validate.js
CHANGED
|
@@ -91,6 +91,12 @@ var PROCESSED_EXTENSIONS = {};
|
|
|
91
91
|
// validateCriticalExtensionStructure via the registered decoder. freshestCRL
|
|
92
92
|
// stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
|
|
93
93
|
// consult it (no delta merge), so a critical instance fails unrecognized.
|
|
94
|
+
// qcStatements is deliberately NOT processed: a critical QC statement (QcLimitValue reliance limit,
|
|
95
|
+
// QcType certificate purpose) asserts qualified-certificate semantics a relying party MUST enforce, and
|
|
96
|
+
// this validator does not enforce them (nor expose a handler to). Marking it processed would let a caller
|
|
97
|
+
// treat a certificate as valid outside its asserted critical QC constraints, so a critical qcStatements
|
|
98
|
+
// fails as an unrecognized-critical extension (RFC 5280 sec. 6.1.4); a non-critical instance is
|
|
99
|
+
// informational and does not affect the verdict. The extension is still decoded for pki.inspect / lint.
|
|
94
100
|
[OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
|
|
95
101
|
OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
|
|
96
102
|
OID.extKeyUsage, OID.cRLDistributionPoints].
|
|
@@ -1133,9 +1139,7 @@ async function validate(path, opts) {
|
|
|
1133
1139
|
if (!opts.trustAnchor) throw E("path/bad-input", "validate: a trustAnchor is required");
|
|
1134
1140
|
// The validity-window check is always on (6.1.3(a)(2)); a missing/invalid
|
|
1135
1141
|
// 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
|
-
}
|
|
1142
|
+
guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
|
|
1139
1143
|
// Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
|
|
1140
1144
|
// throws here rather than silently disabling the behavior it configures.
|
|
1141
1145
|
// Validate-if-present (the default is applied where the policy state is built);
|
|
@@ -1355,13 +1359,20 @@ async function validate(path, opts) {
|
|
|
1355
1359
|
// per-purpose distrust-after date and delegator purposes apply to the
|
|
1356
1360
|
// end-entity leaf it ultimately certifies.
|
|
1357
1361
|
var ta = opts.trustAnchor;
|
|
1358
|
-
|
|
1359
|
-
|
|
1362
|
+
// A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a
|
|
1363
|
+
// NaN time) would make `notBefore > it` NaN-false and SILENTLY drop the distrust
|
|
1364
|
+
// restriction -- the NaN-Date fail-open. Validate a present date fail-closed
|
|
1365
|
+
// before the comparison; an absent (undefined/null) date is no restriction.
|
|
1366
|
+
var distrustDate = (checkPurpose && ta.distrustAfter) ? ta.distrustAfter[checkPurpose] : null;
|
|
1367
|
+
if (distrustDate != null) {
|
|
1368
|
+
distrustDate = guard.time.assertValid(distrustDate, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
|
|
1360
1369
|
// STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
|
|
1361
1370
|
// (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
|
|
1362
1371
|
// <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
|
|
1363
1372
|
// convention keeps the whole boundary day trusted).
|
|
1364
|
-
|
|
1373
|
+
if (cert.validity.notBefore > distrustDate) {
|
|
1374
|
+
checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
|
|
1375
|
+
}
|
|
1365
1376
|
}
|
|
1366
1377
|
if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
|
|
1367
1378
|
checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
|
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 },
|