@blamejs/pki 0.1.30 → 0.1.32
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 +17 -0
- package/README.md +1 -1
- package/lib/guard-all.js +7 -0
- package/lib/guard-bytes.js +3 -0
- package/lib/guard-crypto.js +2 -0
- package/lib/guard-limits.js +7 -0
- package/lib/guard-name.js +111 -0
- package/lib/guard-range.js +69 -0
- package/lib/guard-text.js +3 -0
- package/lib/jose.js +7 -7
- package/lib/path-validate.js +293 -50
- package/lib/schema-all.js +2 -2
- package/lib/schema-attrcert.js +3 -3
- package/lib/schema-cmp.js +5 -6
- package/lib/schema-crmf.js +3 -3
- package/lib/schema-csrattrs.js +1 -1
- package/lib/schema-pkcs12.js +11 -17
- package/lib/schema-pkix.js +15 -20
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.1.32 — 2026-07-11
|
|
8
|
+
|
|
9
|
+
OCSP-backed revocation checking joins certification-path validation.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.path.ocspChecker(responses) -> RevocationChecker builds an OCSP-backed revocation checker for pki.path.validate's revocationChecker option from a set of pre-fetched OCSP responses (DER Buffer, PEM string, or already-parsed). It matches the full CertID triple against the certificate under whichever hash algorithm the CertID declares, authorizes the responder (issuing CA or a CA-issued id-kp-OCSPSigning delegate), verifies the response signature over tbsResponseData, and enforces thisUpdate/nextUpdate currency, reporting good, revoked (with revocationReason), or unknown. A wrong-issuer CertID, an unauthorized responder, a stale, not-yet-valid, or nextUpdate-less response, a non-successful responseStatus, or any verification failure yields unknown, which the validator fails closed unless softFail is set. RFC 6960.
|
|
14
|
+
|
|
15
|
+
## v0.1.31 — 2026-07-11
|
|
16
|
+
|
|
17
|
+
The DER format cohort and the JOSE surface graduate to stable.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- pki.schema.pkcs12 / attrcert / crmf / cmp / csrattrs (parse, pemDecode, pemEncode), pki.schema.all and pki.schema.parse, and the pki.jose signing / verification / thumbprint / base64url / JSON surface graduate from experimental to stable.
|
|
22
|
+
- The LTS-CALENDAR graduation criterion now states that a settled, well-tested format no mainstream tool implements graduates on the toolkit's own conformance-vector round-trip plus coverage-guided fuzzing, rather than on a harness oracle that does not exist.
|
|
23
|
+
|
|
7
24
|
## v0.1.30 — 2026-07-11
|
|
8
25
|
|
|
9
26
|
Fail-closed hardening of the byte-input and text-decode boundaries.
|
package/README.md
CHANGED
|
@@ -220,7 +220,7 @@ is callable today; nothing below is a stub.
|
|
|
220
220
|
| `pki.acme` | RFC 8555 / 8737 / 8738 / 9773 ACME message layer over `pki.jose` — resource-object validators (closed status enums, conditional-required fields, unknown fields ignored), the three §7.1.6 state machines, request builders (newAccount + EAB, newOrder + `replaces`, finalize with CSR identifier-set match and account-key-reuse rejection, challenge responses, deactivation, revokeCert in both key modes, the keyChange nested JWS, POST-as-GET), the http-01 / dns-01 / tls-alpn-01 challenge computations, the dns/ip identifier validators, and the ARI certID (serial sign-padding preserved). A message layer, not an HTTP client — transport-injectable, fail-closed — `validate`, `identify`, `assertTransition`, the builders, `keyAuthorization`, `http01`, `dns01`, `tlsAlpn01Extension`, `verifyTlsAlpn01`, `ariCertId` |
|
|
221
221
|
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
222
222
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
223
|
-
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
|
|
223
|
+
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
224
224
|
| `pki.ct` | Parse RFC 6962 Certificate Transparency SCT lists — `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 for external verification. Structure decoded, crypto surfaced raw, bounded decode, fail-closed — `parseSctList`, `reconstructSignedData` |
|
|
225
225
|
| `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` |
|
|
226
226
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
package/lib/guard-all.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// (recursion / allocation DoS defence)
|
|
19
19
|
// guard.crypto.constantTimeEqual -- length-checked constant-time compare
|
|
20
20
|
// (timing side-channel defence)
|
|
21
|
+
// guard.range.int / .uint31 / .positiveInt31
|
|
22
|
+
// -- bound a decoded integer before narrowing
|
|
23
|
+
// to Number (silent-narrowing defence)
|
|
21
24
|
//
|
|
22
25
|
// Each shape is enforced by a codebase-patterns detector: the characteristic
|
|
23
26
|
// token of a guard (the Buffer.from(x.buffer, byteOffset) re-view, the
|
|
@@ -29,10 +32,14 @@ var bytes = require("./guard-bytes");
|
|
|
29
32
|
var text = require("./guard-text");
|
|
30
33
|
var limits = require("./guard-limits");
|
|
31
34
|
var crypto = require("./guard-crypto");
|
|
35
|
+
var range = require("./guard-range");
|
|
36
|
+
var name = require("./guard-name");
|
|
32
37
|
|
|
33
38
|
module.exports = {
|
|
34
39
|
bytes: bytes,
|
|
35
40
|
text: text,
|
|
36
41
|
limits: limits,
|
|
37
42
|
crypto: crypto,
|
|
43
|
+
range: range,
|
|
44
|
+
name: name,
|
|
38
45
|
};
|
package/lib/guard-bytes.js
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
// Accepts a Buffer / Uint8Array -- the DER / CBOR / CT / Merkle input contract.
|
|
28
28
|
// ErrorClass MUST be a withCause PkiError subclass (the raw detach failure is
|
|
29
29
|
// threaded as the cause).
|
|
30
|
+
// @enforced-by guard-shape-reinlined
|
|
31
|
+
// @guard-shape Buffer\.from\(\s*([A-Za-z_$][\w$]*)\.buffer\s*,\s*\1\.byteOffset
|
|
30
32
|
function view(input, ErrorClass, code, label) {
|
|
31
33
|
if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
|
|
32
34
|
try {
|
|
@@ -42,6 +44,7 @@ function view(input, ErrorClass, code, label) {
|
|
|
42
44
|
// Accepts the full W3C BufferSource (Buffer / TypedArray view / raw ArrayBuffer)
|
|
43
45
|
// -- the WebCrypto input contract. A raw ArrayBuffer is copied via Buffer.from,
|
|
44
46
|
// which also throws on a detached backing store.
|
|
47
|
+
// @enforced-by guard-shape-reinlined (the re-view shape is declared on view above)
|
|
45
48
|
function source(input, ErrorClass, code, label) {
|
|
46
49
|
var isAb = input instanceof ArrayBuffer;
|
|
47
50
|
if (isAb || ArrayBuffer.isView(input)) {
|
package/lib/guard-crypto.js
CHANGED
|
@@ -25,6 +25,8 @@ var nodeCrypto = require("node:crypto");
|
|
|
25
25
|
|
|
26
26
|
// constantTimeEqual(a, b) -> boolean. Length-checked, then constant-time over
|
|
27
27
|
// equal lengths. a and b are Buffers.
|
|
28
|
+
// @enforced-by guard-shape-reinlined
|
|
29
|
+
// @guard-shape \.timingSafeEqual\s*\(
|
|
28
30
|
function constantTimeEqual(a, b) {
|
|
29
31
|
return a.length === b.length && nodeCrypto.timingSafeEqual(a, b);
|
|
30
32
|
}
|
package/lib/guard-limits.js
CHANGED
|
@@ -27,6 +27,8 @@ var constants = require("./constants");
|
|
|
27
27
|
// cap(value, key, dflt) -> a validated non-negative integer, or dflt when value
|
|
28
28
|
// is undefined. A non-integer / non-finite / negative value is a config-time
|
|
29
29
|
// TypeError (Number.isInteger already rejects non-numbers, NaN, and Infinity).
|
|
30
|
+
// @enforced-by behavioral -- a config-time cap validator has no rename-proof code
|
|
31
|
+
// shape; the decode-rejects-a-bad-cap RED vectors (asn1/cbor) are the guard.
|
|
30
32
|
function cap(value, key, dflt) {
|
|
31
33
|
if (value === undefined) return dflt;
|
|
32
34
|
if (!Number.isInteger(value) || value < 0) {
|
|
@@ -38,6 +40,11 @@ function cap(value, key, dflt) {
|
|
|
38
40
|
// depthCap(value, key, dflt) -> a validated recursion-depth cap: cap() plus the
|
|
39
41
|
// stack-safe ceiling, so a raised maxDepth cannot drive recursive descent into a
|
|
40
42
|
// raw RangeError past the native frame limit.
|
|
43
|
+
// @enforced-by guard-shape-reinlined
|
|
44
|
+
// @guard-scope file
|
|
45
|
+
// @guard-shape \bopts\.maxDepth\b
|
|
46
|
+
// @guard-shape \bdepth\s*\+\s*1\b
|
|
47
|
+
// @guard-via \.depthCap\s*\(
|
|
41
48
|
function depthCap(value, key, dflt) {
|
|
42
49
|
var n = cap(value, key, dflt);
|
|
43
50
|
if (n > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
// consumers whose name-string integrity composes this guard (pki.path.validate
|
|
7
|
+
// DN chaining, pki.schema.x509 GeneralName / SAN decode).
|
|
8
|
+
//
|
|
9
|
+
// guard-name -- fail-closed name integrity: reject an embedded control byte in a
|
|
10
|
+
// distinguished-name / SAN string, and compare two distinguished names by their
|
|
11
|
+
// RFC 5280 sec. 7.1 canonical identity rather than their raw bytes.
|
|
12
|
+
//
|
|
13
|
+
// Defends the name-truncation / display-confusion class (CVE-2009-2408): a NUL
|
|
14
|
+
// or control byte embedded in a decoded name lets an attacker make two different
|
|
15
|
+
// names compare equal (or a UI truncate at the NUL), so a cert issued for
|
|
16
|
+
// "good.example.com\0.evil.com" is treated as "good.example.com". CWE-158
|
|
17
|
+
// (improper neutralization of null byte) / CWE-20. The reject is at decode, so a
|
|
18
|
+
// truncation name never reaches a comparison or a display.
|
|
19
|
+
//
|
|
20
|
+
// Defends the DN identity-vs-bytes class (CWE-706): a distinguished name has many
|
|
21
|
+
// RFC 5280 sec. 7.1-equal DER encodings (case, whitespace, PrintableString vs
|
|
22
|
+
// UTF8String). Binding identity to raw bytes -- a byte compare, or hashing
|
|
23
|
+
// name.bytes as a lookup key -- silently treats two equal names as different, so
|
|
24
|
+
// certificate chaining breaks, a revocation issuer / OCSP responder fails to
|
|
25
|
+
// match, or (the mirror risk) a name constraint is escaped. Every DN identity
|
|
26
|
+
// decision routes through the one canonical comparison here.
|
|
27
|
+
|
|
28
|
+
// assertNoControlBytes(str, E, code, label) -> str | throws E(code, ...)
|
|
29
|
+
// DirectoryString policy (a DN attribute value): reject NUL and C0 control bytes
|
|
30
|
+
// in a DECODED name string. TAB (0x09) is exempt; printable non-ASCII (a
|
|
31
|
+
// UTF8String CN carries accented / CJK characters) is allowed. `str` is assumed a
|
|
32
|
+
// string (the caller guards typeof). E is the (code, message) typed-error factory.
|
|
33
|
+
// @enforced-by behavioral -- the control-byte reject has no rename-proof code
|
|
34
|
+
// shape distinct from the ASN.1 charset readers; the CVE-2009-2408 RED vectors
|
|
35
|
+
// (a DN string with an embedded NUL / control byte rejects) are the guard.
|
|
36
|
+
function assertNoControlBytes(str, E, code, label) {
|
|
37
|
+
for (var i = 0; i < str.length; i++) {
|
|
38
|
+
var c = str.charCodeAt(i);
|
|
39
|
+
if (c === 0 || (c < 0x20 && c !== 0x09)) {
|
|
40
|
+
throw E(code, label + " contains an embedded control byte (CVE-2009-2408)");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return str;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// assertPrintableIa5(buf, E, code, label) -> buf | throws E(code, ...)
|
|
47
|
+
// IA5String policy (a dNSName / rfc822Name / URI GeneralName): every byte must be
|
|
48
|
+
// printable 7-bit ASCII [0x20, 0x7e] -- an embedded NUL / control byte enables the
|
|
49
|
+
// same name-truncation bypass downstream. `buf` is the raw GeneralName content.
|
|
50
|
+
// @enforced-by behavioral -- the printable-IA5 byte-range reject has no rename-proof
|
|
51
|
+
// code shape distinct from the ASN.1 IA5 reader; the CVE-2009-2408 RED vectors
|
|
52
|
+
// (a SAN with a control byte rejects) are the guard.
|
|
53
|
+
function assertPrintableIa5(buf, E, code, label) {
|
|
54
|
+
for (var i = 0; i < buf.length; i++) {
|
|
55
|
+
if (buf[i] < 0x20 || buf[i] > 0x7e) {
|
|
56
|
+
throw E(code, label + " must be a printable IA5String (no control bytes)");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return buf;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Canonical form of a single DN attribute value (RFC 5280 sec. 7.1): reject an
|
|
63
|
+
// embedded control byte (CVE-2009-2408) then case-fold and collapse internal
|
|
64
|
+
// whitespace. Every standard X.520 attribute uses caseIgnoreMatch, and this form
|
|
65
|
+
// matches OpenSSL's X509_NAME_cmp, so a chain OpenSSL accepts is not rejected. This
|
|
66
|
+
// canonicalization is the shape the guard-shape-reinlined detector keys on
|
|
67
|
+
// (declared on dnEqual): a boundary hand-rolling it is re-implementing DN identity.
|
|
68
|
+
function _canonAttrValue(v, E, code, label) {
|
|
69
|
+
if (typeof v !== "string") return v;
|
|
70
|
+
assertNoControlBytes(v, E, code, label);
|
|
71
|
+
return v.trim().replace(/\s+/g, " ").toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
// rdnEqual(a, b, E, code, label) -> boolean. Canonical comparison of a single
|
|
74
|
+
// RelativeDistinguishedName (an unordered SET of type/value pairs, compared as a
|
|
75
|
+
// multiset). The RDN-level primitive a name-constraint directoryName prefix match
|
|
76
|
+
// composes; dnEqual composes it over the RDN sequence. Comparing an RDN by raw DER
|
|
77
|
+
// would treat two RFC 5280-equal names as different (or let a truncation name
|
|
78
|
+
// compare equal).
|
|
79
|
+
// @enforced-by guard-shape-reinlined (shares the canonicalization shape declared on dnEqual)
|
|
80
|
+
function rdnEqual(a, b, E, code, label) {
|
|
81
|
+
if (a.length !== b.length) return false;
|
|
82
|
+
var used = [];
|
|
83
|
+
for (var i = 0; i < a.length; i++) {
|
|
84
|
+
var found = false;
|
|
85
|
+
for (var j = 0; j < b.length; j++) {
|
|
86
|
+
if (used[j]) continue;
|
|
87
|
+
if (a[i].type === b[j].type && _canonAttrValue(a[i].value, E, code, label) === _canonAttrValue(b[j].value, E, code, label)) {
|
|
88
|
+
used[j] = true; found = true; break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!found) return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// dnEqual(rdnsA, rdnsB, E, code, label) -> boolean. RFC 5280 sec. 7.1 canonical
|
|
96
|
+
// distinguished-name comparison over the RDN sequence -- the ONE place a DN identity
|
|
97
|
+
// is decided, so no caller binds identity to raw DER (a byte compare, or hashing
|
|
98
|
+
// name.bytes, would treat two RFC 5280-equal names as different, breaking a chain /
|
|
99
|
+
// a revocation match, or -- the mirror risk -- escaping a name constraint). The
|
|
100
|
+
// per-RDN canonicalization + control-byte reject is in rdnEqual / _canonAttrValue.
|
|
101
|
+
// @enforced-by guard-shape-reinlined
|
|
102
|
+
// @guard-shape replace\(/\\s\+/g,
|
|
103
|
+
function dnEqual(rdnsA, rdnsB, E, code, label) {
|
|
104
|
+
if (rdnsA.length !== rdnsB.length) return false;
|
|
105
|
+
for (var i = 0; i < rdnsA.length; i++) {
|
|
106
|
+
if (!rdnEqual(rdnsA[i], rdnsB[i], E, code, label)) return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5, dnEqual: dnEqual, rdnEqual: rdnEqual };
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
// parsers whose decoded-integer bounds compose this guard (pki.schema.x509 /
|
|
7
|
+
// crl / pkcs12 / cmp, pki.path.validate).
|
|
8
|
+
//
|
|
9
|
+
// guard-range -- fail-closed bound of an untrusted DECODED integer before it is
|
|
10
|
+
// narrowed to a JS Number. Parse-time (Tier-2): the value is malformed content,
|
|
11
|
+
// so it throws the caller's typed PkiError -- distinct from guard-limits, which
|
|
12
|
+
// validates an operator-supplied option at config time (Tier-1, TypeError).
|
|
13
|
+
//
|
|
14
|
+
// Defends the silent-narrowing class (a decoded ASN.1 INTEGER past 2^53 rounds
|
|
15
|
+
// when coerced to a Number, so a caller acting on the result -- a pathLen /
|
|
16
|
+
// skipCerts / saltLength / iteration counter -- acts on the WRONG number;
|
|
17
|
+
// CWE-190 integer overflow / CWE-681 incorrect conversion). The bound and the
|
|
18
|
+
// narrow are ATOMIC here: a caller cannot obtain the Number without the range
|
|
19
|
+
// having been enforced, so the value can never round silently and be acted on.
|
|
20
|
+
|
|
21
|
+
// 2^31 - 1 -- the shared skip-cert / path-length / salt / iteration ceiling.
|
|
22
|
+
// This literal lives ONLY here (a re-inline anywhere else is flagged: the
|
|
23
|
+
// guard-shape walk anchors on the literal). 2^53 - 1 is the safe-narrow ceiling.
|
|
24
|
+
var UINT31_MAX = 2147483647n;
|
|
25
|
+
var SAFE_MAX = 9007199254740991n;
|
|
26
|
+
var SAFE_MIN = -9007199254740991n;
|
|
27
|
+
|
|
28
|
+
// int(value, min, max, E, code, label) -> a Number in [min, max].
|
|
29
|
+
// value : the BigInt result of an ASN.1 INTEGER / ENUMERATED read.
|
|
30
|
+
// min, max : inclusive BigInt bounds.
|
|
31
|
+
// E : the (code, message, cause) typed-error factory in scope at the call
|
|
32
|
+
// site (ns.E / ctx.E / the module-local E) -- guard-range injects the
|
|
33
|
+
// caller's typed error through it, the tier-appropriate currency (a
|
|
34
|
+
// parse boundary carries a factory, not a bare ErrorClass).
|
|
35
|
+
// code : the frozen domain/reason code this field rejects under.
|
|
36
|
+
// label : field phrase (+ optional RFC cite) for the message.
|
|
37
|
+
// @enforced-by guard-shape-reinlined
|
|
38
|
+
// @guard-shape 2147483647n
|
|
39
|
+
function int(value, min, max, E, code, label) {
|
|
40
|
+
// Config-time authoring guard (Tier-1): narrowing to Number is lossless only
|
|
41
|
+
// when the WHOLE range sits inside the safe-integer band [-(2^53-1), 2^53-1].
|
|
42
|
+
// Both ends bind -- a min below the floor rounds a decoded value near it just
|
|
43
|
+
// as a max above the ceiling does. A wider range needs a BigInt-preserving
|
|
44
|
+
// guard, not this one (this is why the uint64 Merkle-coordinate domain must
|
|
45
|
+
// NOT route here).
|
|
46
|
+
if (max > SAFE_MAX) {
|
|
47
|
+
throw new TypeError("guard.range.int: max " + max + " exceeds the safe-integer ceiling; use a BigInt-preserving guard");
|
|
48
|
+
}
|
|
49
|
+
if (min < SAFE_MIN) {
|
|
50
|
+
throw new TypeError("guard.range.int: min " + min + " is below the safe-integer floor; use a BigInt-preserving guard");
|
|
51
|
+
}
|
|
52
|
+
// Parse-time reject (Tier-2): malformed / out-of-range decoded content.
|
|
53
|
+
if (typeof value !== "bigint" || value < min || value > max) {
|
|
54
|
+
throw E(code, label + " must be an integer within " + min + ".." + max);
|
|
55
|
+
}
|
|
56
|
+
return Number(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// uint31(value, E, code, label) -> Number in [0, 2^31-1]. The DER-INTEGER
|
|
60
|
+
// non-negative skip/path/salt shape (RFC 5280 / 4211 / 9810).
|
|
61
|
+
// @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
|
|
62
|
+
function uint31(value, E, code, label) { return int(value, 0n, UINT31_MAX, E, code, label); }
|
|
63
|
+
|
|
64
|
+
// positiveInt31(value, E, code, label) -> Number in [1, 2^31-1]. The PKCS#12
|
|
65
|
+
// PBKDF2 / MAC iteration-count shape (a count of at least one).
|
|
66
|
+
// @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
|
|
67
|
+
function positiveInt31(value, E, code, label) { return int(value, 1n, UINT31_MAX, E, code, label); }
|
|
68
|
+
|
|
69
|
+
module.exports = { int: int, uint31: uint31, positiveInt31: positiveInt31 };
|
package/lib/guard-text.js
CHANGED
|
@@ -35,6 +35,9 @@ var LATIN1 = "latin1";
|
|
|
35
35
|
// (required only when fatal is set),
|
|
36
36
|
// badInput: code for a non-Buffer/non-string reject,
|
|
37
37
|
// label: human phrase for the message }
|
|
38
|
+
// @enforced-by behavioral -- cap-before-copy has no rename-proof code shape; the
|
|
39
|
+
// over-cap RED vectors + the detached re-view through guard.bytes.view (itself
|
|
40
|
+
// enforced) are the guard.
|
|
38
41
|
function decode(input, maxBytes, ErrorClass, spec) {
|
|
39
42
|
var charset = spec.charset || LATIN1;
|
|
40
43
|
if (Buffer.isBuffer(input)) {
|
package/lib/jose.js
CHANGED
|
@@ -53,7 +53,7 @@ var B64U_ALPHABET = /^[A-Za-z0-9_-]*$/;
|
|
|
53
53
|
* @primitive pki.jose.base64url.encode
|
|
54
54
|
* @signature pki.jose.base64url.encode(bytes) -> string
|
|
55
55
|
* @since 0.1.25
|
|
56
|
-
* @status
|
|
56
|
+
* @status stable
|
|
57
57
|
* @spec RFC 7515, RFC 4648
|
|
58
58
|
* @related pki.jose.base64url.decode
|
|
59
59
|
*
|
|
@@ -72,7 +72,7 @@ function b64uEncode(bytes) {
|
|
|
72
72
|
* @primitive pki.jose.base64url.decode
|
|
73
73
|
* @signature pki.jose.base64url.decode(text) -> Buffer
|
|
74
74
|
* @since 0.1.25
|
|
75
|
-
* @status
|
|
75
|
+
* @status stable
|
|
76
76
|
* @spec RFC 7515, RFC 4648, RFC 8555
|
|
77
77
|
* @related pki.jose.base64url.encode
|
|
78
78
|
*
|
|
@@ -214,7 +214,7 @@ function _jsonReader(str) {
|
|
|
214
214
|
* @primitive pki.jose.parseJson
|
|
215
215
|
* @signature pki.jose.parseJson(input) -> value
|
|
216
216
|
* @since 0.1.25
|
|
217
|
-
* @status
|
|
217
|
+
* @status stable
|
|
218
218
|
* @spec RFC 7515, RFC 8259
|
|
219
219
|
* @related pki.jose.base64url.decode
|
|
220
220
|
*
|
|
@@ -408,7 +408,7 @@ var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
|
|
|
408
408
|
* @primitive pki.jose.assertPublicJwk
|
|
409
409
|
* @signature pki.jose.assertPublicJwk(jwk) -> jwk
|
|
410
410
|
* @since 0.1.25
|
|
411
|
-
* @status
|
|
411
|
+
* @status stable
|
|
412
412
|
* @spec RFC 7517, RFC 7518
|
|
413
413
|
* @related pki.jose.sign, pki.jose.thumbprint
|
|
414
414
|
*
|
|
@@ -435,7 +435,7 @@ function assertPublicJwk(jwk) {
|
|
|
435
435
|
* @primitive pki.jose.verify
|
|
436
436
|
* @signature pki.jose.verify(jws, opts) -> Promise<{ header, payload }>
|
|
437
437
|
* @since 0.1.25
|
|
438
|
-
* @status
|
|
438
|
+
* @status stable
|
|
439
439
|
* @spec RFC 7515, RFC 7518, RFC 8555
|
|
440
440
|
* @related pki.jose.sign, pki.jose.parseJson
|
|
441
441
|
*
|
|
@@ -488,7 +488,7 @@ async function verify(jws, opts) {
|
|
|
488
488
|
* @primitive pki.jose.sign
|
|
489
489
|
* @signature pki.jose.sign(opts) -> Promise<{ protected, payload, signature }>
|
|
490
490
|
* @since 0.1.25
|
|
491
|
-
* @status
|
|
491
|
+
* @status stable
|
|
492
492
|
* @spec RFC 7515, RFC 7518, RFC 8555
|
|
493
493
|
* @related pki.jose.verify
|
|
494
494
|
*
|
|
@@ -574,7 +574,7 @@ var THUMBPRINT_MEMBERS = {
|
|
|
574
574
|
* @primitive pki.jose.thumbprint
|
|
575
575
|
* @signature pki.jose.thumbprint(jwk) -> Promise<string>
|
|
576
576
|
* @since 0.1.25
|
|
577
|
-
* @status
|
|
577
|
+
* @status stable
|
|
578
578
|
* @spec RFC 7638, RFC 8037, RFC 9964
|
|
579
579
|
* @related pki.jose.verify
|
|
580
580
|
*
|
package/lib/path-validate.js
CHANGED
|
@@ -37,6 +37,8 @@ var asn1 = require("./asn1-der");
|
|
|
37
37
|
var schema = require("./schema-engine");
|
|
38
38
|
var x509 = require("./schema-x509");
|
|
39
39
|
var crl = require("./schema-crl");
|
|
40
|
+
var ocsp = require("./schema-ocsp");
|
|
41
|
+
var guard = require("./guard-all");
|
|
40
42
|
var constants = require("./constants");
|
|
41
43
|
|
|
42
44
|
var PathError = errors.PathError;
|
|
@@ -226,14 +228,11 @@ function resolveRsaPss(paramsBytes) {
|
|
|
226
228
|
// verifier binds to the salt length the certificate states, so a value
|
|
227
229
|
// that would round is not verifiable material (no real salt exceeds the
|
|
228
230
|
// modulus size, let alone this).
|
|
229
|
-
|
|
230
|
-
saltLength = Number(sl);
|
|
231
|
+
saltLength = guard.range.uint31(sl, E, "path/unsupported-algorithm", "RSASSA-PSS saltLength");
|
|
231
232
|
} else if (f.tagNumber === 3) {
|
|
232
233
|
// Compared for equality with 1 below -- bound before conversion so an
|
|
233
234
|
// oversized value cannot round on its way to the comparison.
|
|
234
|
-
|
|
235
|
-
if (tf < 0n || tf > 2147483647n) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS trailerField " + tf.toString());
|
|
236
|
-
trailer = Number(tf);
|
|
235
|
+
trailer = guard.range.uint31(asn1.read.integer(f.children[0]), E, "path/unsupported-algorithm", "RSASSA-PSS trailerField");
|
|
237
236
|
}
|
|
238
237
|
});
|
|
239
238
|
if (hash === null) throw E("path/unsupported-algorithm", "RSASSA-PSS hashAlgorithm must be stated explicitly (the SHA-1 default is rejected)");
|
|
@@ -338,45 +337,17 @@ function builtinVerify(state, cert) {
|
|
|
338
337
|
|
|
339
338
|
// ---- 7.1 name comparison ---------------------------------------------------
|
|
340
339
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// cannot make two different names compare equal.
|
|
349
|
-
if (typeof v !== "string") return v;
|
|
350
|
-
for (var i = 0; i < v.length; i++) {
|
|
351
|
-
var c = v.charCodeAt(i);
|
|
352
|
-
if (c === 0 || (c < 0x20 && c !== 0x09)) throw E("path/name-chaining", "distinguished name contains an embedded control byte (CVE-2009-2408)");
|
|
353
|
-
}
|
|
354
|
-
return v.trim().replace(/\s+/g, " ").toLowerCase();
|
|
340
|
+
// RFC 5280 sec. 7.1 canonical DN / RDN comparison, via the shared name guard: the
|
|
341
|
+
// canonical form (case-fold + internal-whitespace collapse) and the embedded
|
|
342
|
+
// control-byte reject (CVE-2009-2408 -> path/name-chaining) live once in
|
|
343
|
+
// guard-name, so no path-validation caller can reintroduce a raw-byte DN
|
|
344
|
+
// comparison that treats two RFC 5280-equal names as different.
|
|
345
|
+
function dnEqual(rdnsA, rdnsB) {
|
|
346
|
+
return guard.name.dnEqual(rdnsA, rdnsB, E, "path/name-chaining", "distinguished name");
|
|
355
347
|
}
|
|
356
348
|
|
|
357
349
|
function rdnEqual(a, b) {
|
|
358
|
-
|
|
359
|
-
// An RDN is an unordered SET of type/value pairs; compare as multisets.
|
|
360
|
-
var used = [];
|
|
361
|
-
for (var i = 0; i < a.length; i++) {
|
|
362
|
-
var found = false;
|
|
363
|
-
for (var j = 0; j < b.length; j++) {
|
|
364
|
-
if (used[j]) continue;
|
|
365
|
-
if (a[i].type === b[j].type && normalizeAttrValue(a[i].value) === normalizeAttrValue(b[j].value)) {
|
|
366
|
-
used[j] = true; found = true; break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (!found) return false;
|
|
370
|
-
}
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function dnEqual(rdnsA, rdnsB) {
|
|
375
|
-
if (rdnsA.length !== rdnsB.length) return false;
|
|
376
|
-
for (var i = 0; i < rdnsA.length; i++) {
|
|
377
|
-
if (!rdnEqual(rdnsA[i], rdnsB[i])) return false;
|
|
378
|
-
}
|
|
379
|
-
return true;
|
|
350
|
+
return guard.name.rdnEqual(a, b, E, "path/name-chaining", "distinguished name");
|
|
380
351
|
}
|
|
381
352
|
|
|
382
353
|
// ---- extension access ------------------------------------------------------
|
|
@@ -1588,23 +1559,295 @@ function crlEntryReason(entry) {
|
|
|
1588
1559
|
return null;
|
|
1589
1560
|
}
|
|
1590
1561
|
|
|
1591
|
-
|
|
1562
|
+
// Verify a raw signature over tbsBytes with an SPKI public key -- the shared core
|
|
1563
|
+
// of every certificate / CRL / OCSP signature check. Resolve the algorithm
|
|
1564
|
+
// descriptor, enforce the key-OID <-> sig-OID binding (the algorithm-confusion
|
|
1565
|
+
// guard, RFC 9814 sec. 4), import the SPKI, bridge an ECDSA DER signature to
|
|
1566
|
+
// P1363, and verify. Any fault -- an unresolvable/forbidden algorithm, a
|
|
1567
|
+
// key/sig mismatch, an import or verify failure -- resolves false: a signature
|
|
1568
|
+
// check never throws out of this path, it fails closed. `rawSig` is the raw
|
|
1569
|
+
// signature octets (the caller has already unwrapped any BIT STRING and rejected
|
|
1570
|
+
// a non-octet-aligned one).
|
|
1571
|
+
function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
1592
1572
|
var d;
|
|
1593
1573
|
try {
|
|
1594
|
-
d = resolveDescriptor(
|
|
1595
|
-
assertKeyMatchesSigAlg(
|
|
1574
|
+
d = resolveDescriptor(sigAlg);
|
|
1575
|
+
assertKeyMatchesSigAlg(spkiBytes, sigAlg.oid, d);
|
|
1596
1576
|
} catch (_e) { return Promise.resolve(false); }
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
return subtle.importKey("spki", issuer.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
|
|
1600
|
-
key = k;
|
|
1601
|
-
var sig = theCrl.signatureValue.bytes;
|
|
1577
|
+
return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]).then(function (key) {
|
|
1578
|
+
var sig = rawSig;
|
|
1602
1579
|
if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
|
|
1603
|
-
return subtle.verify(d.verify, key, sig,
|
|
1580
|
+
return subtle.verify(d.verify, key, sig, tbsBytes);
|
|
1604
1581
|
}).then(function (ok) { return ok === true; }, function () { return false; });
|
|
1605
1582
|
}
|
|
1606
1583
|
|
|
1584
|
+
function verifyCrlSignature(theCrl, issuer) {
|
|
1585
|
+
if (theCrl.signatureValue.unusedBits !== 0) return Promise.resolve(false); // non-octet-aligned signature
|
|
1586
|
+
return _verifyWithSpki(theCrl.signatureAlgorithm, theCrl.signatureValue.bytes, issuer.workingPublicKey, theCrl.tbsBytes);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// ---- the OCSP revocation checker (RFC 6960) -------------------------------
|
|
1590
|
+
|
|
1591
|
+
// CertID identity-hash algorithms (RFC 6960 sec. 4.1.1). Kept SEPARATE from the
|
|
1592
|
+
// signature SIG_ALGS/HASH_BY_OID set (which omits SHA-1 -- a SHA-1 *signature* is
|
|
1593
|
+
// refused, SHAttered): a CertID hash is an identity binding of an already-known
|
|
1594
|
+
// issuer, not a signature, so collision resistance is irrelevant to the lookup
|
|
1595
|
+
// and RFC 6960's default SHA-1 CertID MUST interoperate. A hash OID outside this
|
|
1596
|
+
// set cannot be reproduced -> no CertID match (fail closed), never an assumption.
|
|
1597
|
+
var OCSP_CERTID_HASHES = {};
|
|
1598
|
+
OCSP_CERTID_HASHES[oid.byName("sha1")] = "SHA-1";
|
|
1599
|
+
OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
|
|
1600
|
+
OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
|
|
1601
|
+
OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
|
|
1602
|
+
var OID_OCSP_SIGNING = oid.byName("ocspSigning");
|
|
1603
|
+
var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
|
|
1604
|
+
|
|
1605
|
+
// The subjectPublicKey BIT STRING VALUE (excluding the unused-bits octet) of an
|
|
1606
|
+
// SPKI DER -- the exact bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash
|
|
1607
|
+
// over (RFC 6960 sec. 4.1.1). Throws on a malformed SPKI; the caller fails closed.
|
|
1608
|
+
function ocspKeyValue(spkiDer) {
|
|
1609
|
+
return asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// The delegate responder's importable SPKI. If its key omits algorithm parameters
|
|
1613
|
+
// and inherits them from the issuing CA (same key algorithm -- an EC public key
|
|
1614
|
+
// whose SPKI omits the namedCurve, RFC 5280 sec. 4.1.2.7), splice the issuer's
|
|
1615
|
+
// parameters in so importKey("spki", ...) has a complete key; otherwise a valid
|
|
1616
|
+
// response would verify as unknown. Mirrors updateWorkingKey's inheritance.
|
|
1617
|
+
function ocspResponderSpki(rc, issuer) {
|
|
1618
|
+
var keyAlg = rc.subjectPublicKeyInfo.algorithm;
|
|
1619
|
+
if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
|
|
1620
|
+
var issuerOid, issuerParams;
|
|
1621
|
+
try {
|
|
1622
|
+
var alg = asn1.decode(issuer.workingPublicKey).children[0];
|
|
1623
|
+
issuerOid = asn1.read.oid(alg.children[0]);
|
|
1624
|
+
issuerParams = alg.children[1] ? alg.children[1].bytes : null;
|
|
1625
|
+
} catch (_e) { return rc.subjectPublicKeyInfo.bytes; }
|
|
1626
|
+
if (issuerOid === keyAlg.oid && !isNullOrAbsentParams(issuerParams)) {
|
|
1627
|
+
return spliceSpkiParameters(rc.subjectPublicKeyInfo, keyAlg.oid, issuerParams);
|
|
1628
|
+
}
|
|
1629
|
+
return rc.subjectPublicKeyInfo.bytes;
|
|
1630
|
+
}
|
|
1631
|
+
function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
|
|
1632
|
+
|
|
1633
|
+
// The checker implements NO OCSP response-extension semantics (the nonce is the
|
|
1634
|
+
// live client's, RFC 6960 sec. 4.4.1; CRL References / Archive Cutoff / Service
|
|
1635
|
+
// Locator are not consulted), so a CRITICAL responseExtension / singleExtension
|
|
1636
|
+
// may change a response's meaning in a way this code cannot honor -- treat it as
|
|
1637
|
+
// unusable (RFC 6960 sec. 4.4 / the RFC 5280 critical-extension contract, the OCSP
|
|
1638
|
+
// analogue of the crlChecker unhandled-critical skip). Returns true if `extList`
|
|
1639
|
+
// carries any critical extension.
|
|
1640
|
+
function ocspHasCriticalExtension(extList) {
|
|
1641
|
+
if (!extList) return false;
|
|
1642
|
+
for (var i = 0; i < extList.length; i++) if (extList[i].critical) return true;
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// A SingleResponse's CertID names the target cert IFF its serial equals the
|
|
1647
|
+
// target's AND issuerNameHash + issuerKeyHash, recomputed under the CertID's OWN
|
|
1648
|
+
// hashAlgorithm, match the issuer (RFC 6960 sec. 4.1.1). A serial-only match would
|
|
1649
|
+
// let a "good" for issuer A serial N answer a query for issuer B serial N
|
|
1650
|
+
// (cross-CA substitution), so BOTH issuer hashes must match under the responder's
|
|
1651
|
+
// chosen algorithm -- never an assumed SHA-1.
|
|
1652
|
+
//
|
|
1653
|
+
// `issuerNameCandidates` is every byte encoding of the (name-chaining-validated)
|
|
1654
|
+
// issuer DN to try: the checked cert's issuer field (RFC 6960 sec. 4.1.1) plus,
|
|
1655
|
+
// when available, the issuer certificate's own subject encoding. These are RFC
|
|
1656
|
+
// 5280 sec. 7.1-equal (they chained) but MAY differ in DER -- case, whitespace, a
|
|
1657
|
+
// PrintableString-vs-UTF8String DirectoryString choice -- and a responder MAY have
|
|
1658
|
+
// hashed any of them; matching the CertID against any is still the SAME validated
|
|
1659
|
+
// issuer, because the issuerKeyHash + serial + authorized signature carry the
|
|
1660
|
+
// binding. The key (raw octets, no encoding variance) is matched once.
|
|
1661
|
+
async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
|
|
1662
|
+
if (certID.serialNumberHex !== cert.serialNumberHex) return false;
|
|
1663
|
+
var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
|
|
1664
|
+
if (!hashName) return false; // an unreproducible hash -> the match cannot be confirmed
|
|
1665
|
+
var keyHash = await ocspDigest(hashName, issuerKeyBits);
|
|
1666
|
+
if (!certID.issuerKeyHash.equals(keyHash)) return false;
|
|
1667
|
+
for (var i = 0; i < issuerNameCandidates.length; i++) {
|
|
1668
|
+
if (certID.issuerNameHash.equals(await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
|
|
1669
|
+
}
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Resolve the signer of a BasicOCSPResponse to an AUTHORIZED responder's SPKI DER,
|
|
1674
|
+
// or null when none is authorized (RFC 6960 sec. 4.2.2.2). Two models: the issuing
|
|
1675
|
+
// CA signing directly (responderID identifies the issuer), or a CA-delegated
|
|
1676
|
+
// responder -- a certificate in `certs` issued by the SAME CA, valid at `time`,
|
|
1677
|
+
// bearing id-kp-OCSPSigning in its extendedKeyUsage. anyExtendedKeyUsage and an
|
|
1678
|
+
// ABSENT EKU do NOT authorize an OCSP delegate (the opposite of the path-validation
|
|
1679
|
+
// EKU default), so an ordinary leaf the CA issued cannot forge revocation status.
|
|
1680
|
+
// Fails closed at every branch (a malformed delegate, a control-byte DN, an
|
|
1681
|
+
// unreadable EKU -> skip that candidate).
|
|
1682
|
+
async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
|
|
1683
|
+
var rid = basicResponse.responderID;
|
|
1684
|
+
var matchesIssuer = false;
|
|
1685
|
+
try {
|
|
1686
|
+
if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
|
|
1687
|
+
else if (rid.byKey) matchesIssuer = rid.byKey.equals(await ocspDigest("SHA-1", issuerKeyBits));
|
|
1688
|
+
} catch (_e) { matchesIssuer = false; }
|
|
1689
|
+
if (matchesIssuer) return issuer.workingPublicKey;
|
|
1690
|
+
|
|
1691
|
+
for (var i = 0; i < basicResponse.certs.length; i++) {
|
|
1692
|
+
var rc;
|
|
1693
|
+
try { rc = x509.parse(basicResponse.certs[i]); }
|
|
1694
|
+
catch (_e) { continue; }
|
|
1695
|
+
var identifies = false;
|
|
1696
|
+
try {
|
|
1697
|
+
if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
|
|
1698
|
+
else if (rid.byKey) identifies = rid.byKey.equals(await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
|
|
1699
|
+
} catch (_e) { identifies = false; }
|
|
1700
|
+
if (!identifies) continue;
|
|
1701
|
+
// The delegate MUST be issued directly by the CA that issued the target.
|
|
1702
|
+
var issuedByCa;
|
|
1703
|
+
try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
|
|
1704
|
+
catch (_e) { continue; }
|
|
1705
|
+
if (!issuedByCa) continue;
|
|
1706
|
+
if (rc.signatureValue.unusedBits !== 0) continue;
|
|
1707
|
+
if (!(await _verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
|
|
1708
|
+
// The delegate certificate MUST itself be valid at the validation instant.
|
|
1709
|
+
if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
|
|
1710
|
+
// The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
|
|
1711
|
+
var eku;
|
|
1712
|
+
try { eku = decodeExt(rc, OID.extKeyUsage); }
|
|
1713
|
+
catch (_e) { continue; }
|
|
1714
|
+
if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
|
|
1715
|
+
// A delegate asserting keyUsage MUST permit digitalSignature -- signing an OCSP
|
|
1716
|
+
// response is a digitalSignature operation (RFC 5280 sec. 4.2.1.3); an absent
|
|
1717
|
+
// keyUsage is unrestricted, an unreadable one is not authoritative.
|
|
1718
|
+
var ku;
|
|
1719
|
+
try { ku = decodeExt(rc, OID.keyUsage); }
|
|
1720
|
+
catch (_e) { continue; }
|
|
1721
|
+
if (ku && ku.value.digitalSignature !== true) continue;
|
|
1722
|
+
// A delegate carrying a critical extension this code does not process is
|
|
1723
|
+
// unusable, the same fail-closed rule the path validator applies to any cert
|
|
1724
|
+
// (RFC 5280 sec. 6.1.4(o)) -- an unknown critical constraint must not be ignored
|
|
1725
|
+
// while its key is trusted to authenticate revocation status. A RECOGNIZED
|
|
1726
|
+
// critical extension whose value is malformed is likewise rejected via the
|
|
1727
|
+
// shared structure validation, exactly as the path validator rejects it.
|
|
1728
|
+
if (unrecognizedCriticalExtension(rc, false)) continue;
|
|
1729
|
+
if (validateCriticalExtensionStructure(rc)) continue;
|
|
1730
|
+
// The delegate MUST carry id-pkix-ocsp-nocheck (RFC 6960 sec. 4.2.2.2.1): a
|
|
1731
|
+
// transport-free checker cannot otherwise confirm the responder certificate has
|
|
1732
|
+
// not itself been revoked, and a revoked OCSP-signing certificate would keep
|
|
1733
|
+
// signing "good" until its notAfter. A deployment that instead supplies the
|
|
1734
|
+
// responder's own status opts in through a future checker; absent nocheck, fail
|
|
1735
|
+
// closed (unknown) rather than trust an unvalidated responder.
|
|
1736
|
+
if (!findExt(rc, OID_OCSP_NOCHECK)) continue;
|
|
1737
|
+
return ocspResponderSpki(rc, issuer);
|
|
1738
|
+
}
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* @primitive pki.path.ocspChecker
|
|
1744
|
+
* @signature pki.path.ocspChecker(responses) -> RevocationChecker
|
|
1745
|
+
* @since 0.1.32
|
|
1746
|
+
* @status experimental
|
|
1747
|
+
* @spec RFC 6960
|
|
1748
|
+
* @related pki.path.validate, pki.schema.ocsp.parseResponse, pki.path.crlChecker
|
|
1749
|
+
*
|
|
1750
|
+
* Build an OCSP-backed `RevocationChecker` for `pki.path.validate`'s
|
|
1751
|
+
* `revocationChecker` option from a set of pre-fetched OCSP responses (DER/PEM
|
|
1752
|
+
* or already-parsed). For each certificate it locates a SingleResponse whose
|
|
1753
|
+
* CertID binds this cert's serial to its issuer -- recomputing `issuerNameHash`
|
|
1754
|
+
* and `issuerKeyHash` under the CertID's own hashAlgorithm (SHA-1 or SHA-2), so
|
|
1755
|
+
* a response using either matches -- confirms the responder is authorized (the
|
|
1756
|
+
* issuing CA directly, or a valid CA-issued delegate bearing both id-kp-OCSPSigning
|
|
1757
|
+
* and id-pkix-ocsp-nocheck), verifies the response signature over
|
|
1758
|
+
* `tbsResponseDataBytes`, checks currency
|
|
1759
|
+
* (`thisUpdate`/`nextUpdate`), and reports `{ status: "good"|"revoked"|
|
|
1760
|
+
* "unknown" }`. A wrong-issuer CertID, an unauthorized responder, a stale,
|
|
1761
|
+
* not-yet-valid, nextUpdate-less, non-successful, or unverifiable response
|
|
1762
|
+
* yields `unknown`, which the validator fails closed unless `softFail` is set;
|
|
1763
|
+
* a `revoked` status surfaces its `revocationReason`. It is transport-free: the
|
|
1764
|
+
* caller supplies bytes it collected (an OCSP fetch or a stapled response), so
|
|
1765
|
+
* nonce anti-replay is the live client's responsibility and the residual replay
|
|
1766
|
+
* defense is the `thisUpdate`/`nextUpdate` currency window.
|
|
1767
|
+
*
|
|
1768
|
+
* @example
|
|
1769
|
+
* var checker = pki.path.ocspChecker([]); // no responses -> every cert is "unknown"
|
|
1770
|
+
* typeof checker.check; // "function"
|
|
1771
|
+
*/
|
|
1772
|
+
function ocspChecker(responses) {
|
|
1773
|
+
var parsed = (responses || []).map(function (r) { return (r && r.responseStatus) ? r : ocsp.parseResponse(r); });
|
|
1774
|
+
return {
|
|
1775
|
+
check: async function (cert, issuer, ctx) {
|
|
1776
|
+
var time = ctx.time;
|
|
1777
|
+
var historical = ctx.historicalMode === true;
|
|
1778
|
+
// Issuer DN candidates to match the CertID against (RFC 6960 sec. 4.1.1 names
|
|
1779
|
+
// the checked cert's issuer field; a response MAY instead carry the issuer
|
|
1780
|
+
// certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
|
|
1781
|
+
var issuerNameCandidates = [cert.issuer.bytes];
|
|
1782
|
+
function addNameCandidate(nm) {
|
|
1783
|
+
if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
|
|
1784
|
+
}
|
|
1785
|
+
if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
|
|
1786
|
+
addNameCandidate(issuer.workingIssuerName);
|
|
1787
|
+
var issuerKeyBits;
|
|
1788
|
+
try { issuerKeyBits = ocspKeyValue(issuer.workingPublicKey); }
|
|
1789
|
+
catch (_e) { return { status: "unknown", reason: "the issuer public key could not be read to recompute the OCSP CertID" }; }
|
|
1790
|
+
|
|
1791
|
+
// A serial is revoked if ANY authoritative, verified, current response says
|
|
1792
|
+
// so -- a clean response must never shadow a revoking one (the crlChecker
|
|
1793
|
+
// fail-closed law). "good" needs at least one authoritative match; every
|
|
1794
|
+
// other outcome is undetermined.
|
|
1795
|
+
var revokedResult = null;
|
|
1796
|
+
var sawGood = false;
|
|
1797
|
+
var sawUnknownStatus = false;
|
|
1798
|
+
|
|
1799
|
+
for (var k = 0; k < parsed.length; k++) {
|
|
1800
|
+
var resp = parsed[k];
|
|
1801
|
+
if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
|
|
1802
|
+
var br = resp.basicResponse;
|
|
1803
|
+
if (!br) continue;
|
|
1804
|
+
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
|
|
1805
|
+
if (!signerSpki) continue; // unauthorized responder
|
|
1806
|
+
if (!(await _verifyWithSpki(br.signatureAlgorithm, br.signature, signerSpki, br.tbsResponseDataBytes))) continue;
|
|
1807
|
+
// A critical responseExtension changes the meaning of the WHOLE response
|
|
1808
|
+
// and this code processes none -> the response is unusable.
|
|
1809
|
+
if (ocspHasCriticalExtension(br.responseExtensions)) continue;
|
|
1810
|
+
for (var s = 0; s < br.responses.length; s++) {
|
|
1811
|
+
var sr = br.responses[s];
|
|
1812
|
+
if (!(await ocspCertIdMatches(sr.certID, cert, issuerNameCandidates, issuerKeyBits))) continue; // not about this cert
|
|
1813
|
+
if (ocspHasCriticalExtension(sr.singleExtensions)) continue; // a critical per-response extension this code cannot process -> skip
|
|
1814
|
+
if (sr.thisUpdate > time) continue; // not yet valid
|
|
1815
|
+
if (!sr.nextUpdate || sr.nextUpdate < time) continue; // no bounded validity / stale -> fail closed
|
|
1816
|
+
var st = sr.certStatus;
|
|
1817
|
+
if (st.type === "revoked") {
|
|
1818
|
+
// A revocation is effective as of its revocationTime. Present-time
|
|
1819
|
+
// validation revokes regardless (a future revocationTime is
|
|
1820
|
+
// post-dating/skew, never "good"); only an EXPLICIT historical
|
|
1821
|
+
// validation defers a strictly-future revocation.
|
|
1822
|
+
if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { sawGood = true; }
|
|
1823
|
+
else {
|
|
1824
|
+
revokedResult = {
|
|
1825
|
+
status: "revoked",
|
|
1826
|
+
revocationReason: st.revocationReason || null,
|
|
1827
|
+
reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : ""),
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
} else if (st.type === "good") {
|
|
1831
|
+
sawGood = true;
|
|
1832
|
+
} else {
|
|
1833
|
+
sawUnknownStatus = true; // an explicit responder "unknown"
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (revokedResult) return revokedResult;
|
|
1838
|
+
if (sawGood) return { status: "good" };
|
|
1839
|
+
return {
|
|
1840
|
+
status: "unknown",
|
|
1841
|
+
reason: sawUnknownStatus
|
|
1842
|
+
? "the OCSP responder reported certStatus unknown for this certificate"
|
|
1843
|
+
: "no authoritative, current, in-scope OCSP response covers this certificate",
|
|
1844
|
+
};
|
|
1845
|
+
},
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1607
1849
|
module.exports = {
|
|
1608
1850
|
validate: validate,
|
|
1609
1851
|
crlChecker: crlChecker,
|
|
1852
|
+
ocspChecker: ocspChecker,
|
|
1610
1853
|
};
|
package/lib/schema-all.js
CHANGED
|
@@ -249,7 +249,7 @@ var FORMATS = [
|
|
|
249
249
|
* @primitive pki.schema.all
|
|
250
250
|
* @signature pki.schema.all() -> string[]
|
|
251
251
|
* @since 0.1.7
|
|
252
|
-
* @status
|
|
252
|
+
* @status stable
|
|
253
253
|
* @spec RFC 5280
|
|
254
254
|
* @related pki.schema.parse
|
|
255
255
|
*
|
|
@@ -264,7 +264,7 @@ function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
|
264
264
|
* @primitive pki.schema.parse
|
|
265
265
|
* @signature pki.schema.parse(input) -> parsed
|
|
266
266
|
* @since 0.1.7
|
|
267
|
-
* @status
|
|
267
|
+
* @status stable
|
|
268
268
|
* @spec RFC 5280
|
|
269
269
|
* @related pki.schema.x509, pki.schema.all
|
|
270
270
|
*
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -299,7 +299,7 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
299
299
|
* @primitive pki.schema.attrcert.parse
|
|
300
300
|
* @signature pki.schema.attrcert.parse(input) -> attributeCertificate
|
|
301
301
|
* @since 0.1.14
|
|
302
|
-
* @status
|
|
302
|
+
* @status stable
|
|
303
303
|
* @spec RFC 5755, X.509
|
|
304
304
|
* @related pki.schema.parse, pki.schema.x509.parse
|
|
305
305
|
*
|
|
@@ -348,7 +348,7 @@ function parseV1(input) {
|
|
|
348
348
|
* @primitive pki.schema.attrcert.pemDecode
|
|
349
349
|
* @signature pki.schema.attrcert.pemDecode(text, label?) -> Buffer
|
|
350
350
|
* @since 0.1.14
|
|
351
|
-
* @status
|
|
351
|
+
* @status stable
|
|
352
352
|
* @spec RFC 7468, RFC 5755
|
|
353
353
|
* @related pki.schema.attrcert.parse
|
|
354
354
|
*
|
|
@@ -367,7 +367,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label === null ? n
|
|
|
367
367
|
* @primitive pki.schema.attrcert.pemEncode
|
|
368
368
|
* @signature pki.schema.attrcert.pemEncode(der, label?) -> string
|
|
369
369
|
* @since 0.1.23
|
|
370
|
-
* @status
|
|
370
|
+
* @status stable
|
|
371
371
|
* @spec RFC 7468, RFC 5755
|
|
372
372
|
* @related pki.schema.attrcert.pemDecode
|
|
373
373
|
*
|
package/lib/schema-cmp.js
CHANGED
|
@@ -42,6 +42,7 @@ var pkix = require("./schema-pkix.js");
|
|
|
42
42
|
var cms = require("./schema-cms.js");
|
|
43
43
|
var crmf = require("./schema-crmf.js");
|
|
44
44
|
var frameworkError = require("./framework-error.js");
|
|
45
|
+
var guard = require("./guard-all.js");
|
|
45
46
|
|
|
46
47
|
var CmpError = frameworkError.CmpError;
|
|
47
48
|
var PemError = frameworkError.PemError;
|
|
@@ -99,9 +100,7 @@ var PKI_FAILURE_INFO = schema.decode(function (n, ctx) {
|
|
|
99
100
|
// PollRep checkAfter is a delay in seconds -- negative poisons scheduling, and
|
|
100
101
|
// the value surfaces as an exact number or not at all.
|
|
101
102
|
var CHECK_AFTER = schema.decode(function (n, ctx) {
|
|
102
|
-
|
|
103
|
-
if (v < 0n || v > 2147483647n) throw ctx.E("cmp/bad-poll-rep", "checkAfter must be a non-negative delay in seconds (RFC 9810 sec. 5.3.22)");
|
|
104
|
-
return Number(v);
|
|
103
|
+
return guard.range.uint31(asn1.read.integer(n), ctx.E, "cmp/bad-poll-rep", "checkAfter delay in seconds (RFC 9810 sec. 5.3.22)");
|
|
105
104
|
});
|
|
106
105
|
|
|
107
106
|
// A raw certificate / CRL / publication-info element -- surfaced byte-exact for
|
|
@@ -841,7 +840,7 @@ var PKI_MESSAGE = schema.seq([
|
|
|
841
840
|
* @primitive pki.schema.cmp.parse
|
|
842
841
|
* @signature pki.schema.cmp.parse(input) -> pkiMessage
|
|
843
842
|
* @since 0.1.19
|
|
844
|
-
* @status
|
|
843
|
+
* @status stable
|
|
845
844
|
* @spec RFC 9810, RFC 9483, RFC 9481
|
|
846
845
|
* @related pki.schema.parse, pki.schema.crmf.parse, pki.schema.cms.parse
|
|
847
846
|
*
|
|
@@ -895,7 +894,7 @@ var parse = pkix.makeParser({
|
|
|
895
894
|
* @primitive pki.schema.cmp.pemDecode
|
|
896
895
|
* @signature pki.schema.cmp.pemDecode(text, label?) -> Buffer
|
|
897
896
|
* @since 0.1.19
|
|
898
|
-
* @status
|
|
897
|
+
* @status stable
|
|
899
898
|
* @spec RFC 7468, RFC 9810
|
|
900
899
|
* @related pki.schema.cmp.parse
|
|
901
900
|
*
|
|
@@ -912,7 +911,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMP", Pe
|
|
|
912
911
|
* @primitive pki.schema.cmp.pemEncode
|
|
913
912
|
* @signature pki.schema.cmp.pemEncode(der, label?) -> string
|
|
914
913
|
* @since 0.1.19
|
|
915
|
-
* @status
|
|
914
|
+
* @status stable
|
|
916
915
|
* @spec RFC 7468
|
|
917
916
|
* @related pki.schema.cmp.pemDecode
|
|
918
917
|
*
|
package/lib/schema-crmf.js
CHANGED
|
@@ -455,7 +455,7 @@ var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
|
|
|
455
455
|
* @primitive pki.schema.crmf.parse
|
|
456
456
|
* @signature pki.schema.crmf.parse(input) -> certReqMessages
|
|
457
457
|
* @since 0.1.17
|
|
458
|
-
* @status
|
|
458
|
+
* @status stable
|
|
459
459
|
* @spec RFC 4211
|
|
460
460
|
* @related pki.schema.parse, pki.schema.csr.parse
|
|
461
461
|
*
|
|
@@ -496,7 +496,7 @@ var parse = pkix.makeParser({
|
|
|
496
496
|
* @primitive pki.schema.crmf.pemDecode
|
|
497
497
|
* @signature pki.schema.crmf.pemDecode(text, label?) -> Buffer
|
|
498
498
|
* @since 0.1.17
|
|
499
|
-
* @status
|
|
499
|
+
* @status stable
|
|
500
500
|
* @spec RFC 7468, RFC 4211
|
|
501
501
|
* @related pki.schema.crmf.parse
|
|
502
502
|
*
|
|
@@ -513,7 +513,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, Pem
|
|
|
513
513
|
* @primitive pki.schema.crmf.pemEncode
|
|
514
514
|
* @signature pki.schema.crmf.pemEncode(der, label) -> string
|
|
515
515
|
* @since 0.1.23
|
|
516
|
-
* @status
|
|
516
|
+
* @status stable
|
|
517
517
|
* @spec RFC 7468, RFC 4211
|
|
518
518
|
* @related pki.schema.crmf.pemDecode
|
|
519
519
|
*
|
package/lib/schema-csrattrs.js
CHANGED
|
@@ -320,7 +320,7 @@ var CSR_ATTRS = schema.seqOf(ATTR_OR_OID, {
|
|
|
320
320
|
* @primitive pki.schema.csrattrs.parse
|
|
321
321
|
* @signature pki.schema.csrattrs.parse(der) -> { items }
|
|
322
322
|
* @since 0.1.24
|
|
323
|
-
* @status
|
|
323
|
+
* @status stable
|
|
324
324
|
* @spec RFC 8951, RFC 9908, RFC 7030
|
|
325
325
|
* @related pki.schema.parse, pki.est.buildEnrollAttributes
|
|
326
326
|
*
|
package/lib/schema-pkcs12.js
CHANGED
|
@@ -46,6 +46,7 @@ var pkix = require("./schema-pkix.js");
|
|
|
46
46
|
var cms = require("./schema-cms.js");
|
|
47
47
|
var pkcs8 = require("./schema-pkcs8.js");
|
|
48
48
|
var frameworkError = require("./framework-error.js");
|
|
49
|
+
var guard = require("./guard-all.js");
|
|
49
50
|
|
|
50
51
|
var Pkcs12Error = frameworkError.Pkcs12Error;
|
|
51
52
|
var PemError = frameworkError.PemError;
|
|
@@ -102,16 +103,10 @@ var PBKDF2_PARAMS = schema.seq([
|
|
|
102
103
|
if (!m.fields.keyLength.present) {
|
|
103
104
|
throw ctx.E("pkcs12/bad-mac-data", "PBMAC1 PBKDF2-params must carry keyLength (RFC 9579 sec. 5)");
|
|
104
105
|
}
|
|
105
|
-
//
|
|
106
|
-
// would round silently
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
throw ctx.E("pkcs12/bad-mac-data", "PBKDF2 iterationCount must be a positive integer within the iteration-count range");
|
|
110
|
-
}
|
|
111
|
-
var keyLength = m.fields.keyLength.value;
|
|
112
|
-
if (keyLength < 1n || keyLength > 2147483647n) {
|
|
113
|
-
throw ctx.E("pkcs12/bad-mac-data", "PBKDF2 keyLength must be a positive integer within the key-length range");
|
|
114
|
-
}
|
|
106
|
+
// guard.range.positiveInt31 bounds + narrows each counter atomically -- a
|
|
107
|
+
// value past the bound would round silently and hand a verifier wrong inputs.
|
|
108
|
+
var iterationCount = guard.range.positiveInt31(m.fields.iterationCount.value, ctx.E, "pkcs12/bad-mac-data", "PBKDF2 iterationCount");
|
|
109
|
+
var keyLength = guard.range.positiveInt31(m.fields.keyLength.value, ctx.E, "pkcs12/bad-mac-data", "PBKDF2 keyLength");
|
|
115
110
|
var prf = m.fields.prf.present ? m.fields.prf.value.result : null;
|
|
116
111
|
// X.690 sec. 11.5 -- a DEFAULT-valued component must be omitted from a DER
|
|
117
112
|
// encoding, and the prf DEFAULT is algid-hmacWithSHA1: hmacWithSHA1 with
|
|
@@ -124,8 +119,8 @@ var PBKDF2_PARAMS = schema.seq([
|
|
|
124
119
|
}
|
|
125
120
|
return {
|
|
126
121
|
salt: m.fields.salt.value,
|
|
127
|
-
iterationCount:
|
|
128
|
-
keyLength:
|
|
122
|
+
iterationCount: iterationCount,
|
|
123
|
+
keyLength: keyLength,
|
|
129
124
|
prfOid: prf ? prf.oid : OID_HMAC_SHA1,
|
|
130
125
|
prfName: prf ? prf.name : "hmacWithSHA1",
|
|
131
126
|
};
|
|
@@ -172,8 +167,7 @@ var MAC_DATA = schema.seq([
|
|
|
172
167
|
if (it.present) {
|
|
173
168
|
var v = it.value;
|
|
174
169
|
if (v === 1n) throw ctx.E("pkcs12/bad-mac-iterations", "iterations equal to its DEFAULT 1 must be omitted (X.690 sec. 11.5)");
|
|
175
|
-
|
|
176
|
-
iterations = Number(v);
|
|
170
|
+
iterations = guard.range.positiveInt31(v, ctx.E, "pkcs12/bad-mac-iterations", "MacData iterations");
|
|
177
171
|
}
|
|
178
172
|
var di = m.fields.mac.value.result;
|
|
179
173
|
var pbmac1 = null;
|
|
@@ -529,7 +523,7 @@ function _buildBag(rec, state, ctx) {
|
|
|
529
523
|
* @primitive pki.schema.pkcs12.parse
|
|
530
524
|
* @signature pki.schema.pkcs12.parse(input) -> pfx
|
|
531
525
|
* @since 0.1.18
|
|
532
|
-
* @status
|
|
526
|
+
* @status stable
|
|
533
527
|
* @spec RFC 7292, RFC 9579
|
|
534
528
|
* @defends ASN.1-parser-DoS (CWE-400)
|
|
535
529
|
* @related pki.schema.parse, pki.schema.pkcs8.parse, pki.schema.cms.parse
|
|
@@ -577,7 +571,7 @@ var parse = pkix.makeParser({
|
|
|
577
571
|
* @primitive pki.schema.pkcs12.pemDecode
|
|
578
572
|
* @signature pki.schema.pkcs12.pemDecode(text, label?) -> Buffer
|
|
579
573
|
* @since 0.1.18
|
|
580
|
-
* @status
|
|
574
|
+
* @status stable
|
|
581
575
|
* @spec RFC 7468, RFC 7292
|
|
582
576
|
* @related pki.schema.pkcs12.parse
|
|
583
577
|
*
|
|
@@ -594,7 +588,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PKCS12",
|
|
|
594
588
|
* @primitive pki.schema.pkcs12.pemEncode
|
|
595
589
|
* @signature pki.schema.pkcs12.pemEncode(der, label?) -> string
|
|
596
590
|
* @since 0.1.18
|
|
597
|
-
* @status
|
|
591
|
+
* @status stable
|
|
598
592
|
* @spec RFC 7468
|
|
599
593
|
* @related pki.schema.pkcs12.pemDecode
|
|
600
594
|
*
|
package/lib/schema-pkix.js
CHANGED
|
@@ -500,12 +500,9 @@ function generalName(ns, opts) {
|
|
|
500
500
|
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 sec. 10.2)");
|
|
501
501
|
if (GN_IA5[t]) {
|
|
502
502
|
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// (CVE-2009-2408 class) downstream, so reject it at decode.
|
|
507
|
-
if (n.content[i] < 0x20 || n.content[i] > 0x7e) throw ctx.E(code, "GeneralName [" + t + "] must be a printable IA5String (no control bytes)");
|
|
508
|
-
}
|
|
503
|
+
// 7-bit IA5, no C0/DEL control byte -- an embedded NUL/control in a
|
|
504
|
+
// dNSName/rfc822Name/URI enables a name-truncation bypass (CVE-2009-2408).
|
|
505
|
+
guard.name.assertPrintableIa5(n.content, ctx.E, code, "GeneralName [" + t + "]");
|
|
509
506
|
if (decodeValue) value = n.content.toString("latin1");
|
|
510
507
|
} else if (t === 7) {
|
|
511
508
|
if (subtreeBase) {
|
|
@@ -619,11 +616,10 @@ function certExtensionDecoders(ns) {
|
|
|
619
616
|
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
|
|
620
617
|
if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 sec. 4.2.1.9)");
|
|
621
618
|
var pl = readInt(kids[i], C, "pathLenConstraint");
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
pathLen = Number(pl); i++;
|
|
619
|
+
// guard.range.uint31 bounds the decoded BigInt and narrows atomically -- a
|
|
620
|
+
// value past the safe-integer range would otherwise round silently and be
|
|
621
|
+
// compared as a path-length ceiling.
|
|
622
|
+
pathLen = guard.range.uint31(pl, ns.E, C, "BasicConstraints pathLenConstraint (RFC 5280 sec. 4.2.1.9)"); i++;
|
|
627
623
|
}
|
|
628
624
|
if (i !== kids.length) throw ns.E(C, "BasicConstraints has unexpected trailing fields");
|
|
629
625
|
return { cA: cA, pathLenConstraint: pathLen };
|
|
@@ -746,11 +742,11 @@ function certExtensionDecoders(ns) {
|
|
|
746
742
|
pcLastTag = k.tagNumber;
|
|
747
743
|
var v;
|
|
748
744
|
try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
|
|
749
|
-
//
|
|
750
|
-
// range would round silently)
|
|
751
|
-
|
|
752
|
-
if (k.tagNumber === 0) rep =
|
|
753
|
-
else if (k.tagNumber === 1) ipm =
|
|
745
|
+
// guard.range.uint31 bounds + narrows atomically (a skip count past the
|
|
746
|
+
// safe-integer range would round silently).
|
|
747
|
+
var skip = guard.range.uint31(v, ns.E, C, "PolicyConstraints skip count (RFC 5280 sec. 4.2.1.11)");
|
|
748
|
+
if (k.tagNumber === 0) rep = skip;
|
|
749
|
+
else if (k.tagNumber === 1) ipm = skip;
|
|
754
750
|
else throw ns.E(C, "PolicyConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
755
751
|
});
|
|
756
752
|
return { requireExplicitPolicy: rep, inhibitPolicyMapping: ipm };
|
|
@@ -762,10 +758,9 @@ function certExtensionDecoders(ns) {
|
|
|
762
758
|
var n = decodeTop(buf, C, "InhibitAnyPolicy");
|
|
763
759
|
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 sec. 4.2.1.14)");
|
|
764
760
|
var v = readInt(n, C, "InhibitAnyPolicy");
|
|
765
|
-
//
|
|
766
|
-
// range would round silently)
|
|
767
|
-
|
|
768
|
-
return Number(v);
|
|
761
|
+
// guard.range.uint31 bounds + narrows atomically (a skip count past the
|
|
762
|
+
// safe-integer range would round silently).
|
|
763
|
+
return guard.range.uint31(v, ns.E, C, "InhibitAnyPolicy skip count (RFC 5280 sec. 4.2.1.14)");
|
|
769
764
|
}
|
|
770
765
|
|
|
771
766
|
// subjectAltName / issuerAltName ::= GeneralNames -- decoded values surfaced.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"c8": "11.0.0",
|
|
74
74
|
"esbuild": "0.28.1",
|
|
75
|
-
"eslint": "10.
|
|
75
|
+
"eslint": "10.7.0"
|
|
76
76
|
}
|
|
77
77
|
}
|
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:87ae1f84-9b42-4b45-820b-1323915f3fbe",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-11T18:49:24.325Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.32",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.32",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.32",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.32",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|