@blamejs/pki 0.8.30 → 0.8.48
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 +411 -0
- package/README.md +67 -16
- package/bin/pki.js +716 -30
- package/examples/issue-a-certificate.js +90 -0
- package/examples/post-quantum.js +66 -0
- package/examples/read-a-certificate.js +56 -0
- package/examples/revoke-and-check.js +86 -0
- package/examples/sign-and-verify-cms.js +63 -0
- package/index.js +18 -5
- package/lib/acme.js +223 -74
- package/lib/alt-sig.js +441 -0
- package/lib/asn1-der.js +48 -25
- package/lib/attrcert-sign.js +27 -19
- package/lib/cbor-det.js +24 -14
- package/lib/cmc-build.js +9 -2
- package/lib/cmc-verify.js +14 -3
- package/lib/cmp-build.js +17 -5
- package/lib/cmp-session.js +24 -11
- package/lib/cmp-verify.js +28 -19
- package/lib/cms-compress.js +3 -1
- package/lib/cms-decrypt.js +102 -7
- package/lib/cms-encrypt.js +59 -7
- package/lib/cms-sign.js +31 -9
- package/lib/cms-verify.js +22 -18
- package/lib/composite-kem.js +30 -18
- package/lib/composite-sig.js +29 -14
- package/lib/constants.js +28 -0
- package/lib/crl-sign.js +111 -19
- package/lib/crmf-sign.js +167 -15
- package/lib/csr-sign.js +117 -6
- package/lib/ct.js +939 -48
- package/lib/est.js +29 -16
- package/lib/framework-error.js +12 -0
- package/lib/guard-async.js +1 -1
- package/lib/guard-bytes.js +103 -20
- package/lib/guard-crypto.js +205 -3
- package/lib/guard-encoding.js +6 -1
- package/lib/guard-identifier.js +55 -8
- package/lib/guard-intrinsic.js +43 -0
- package/lib/guard-json.js +122 -9
- package/lib/guard-limits.js +12 -1
- package/lib/guard-list.js +63 -8
- package/lib/guard-name.js +167 -20
- package/lib/guard-range.js +43 -5
- package/lib/guard-text.js +41 -0
- package/lib/guard-verdict.js +39 -8
- package/lib/hpke.js +688 -42
- package/lib/http-transport.js +156 -4
- package/lib/identity-match.js +4 -4
- package/lib/inspect.js +448 -2
- package/lib/jose.js +93 -25
- package/lib/key.js +18 -13
- package/lib/lint.js +165 -1
- package/lib/merkle.js +233 -27
- package/lib/ocsp.js +27 -14
- package/lib/oid.js +14 -4
- package/lib/path-validate.js +143 -61
- package/lib/pkcs12-build.js +7 -2
- package/lib/pki-build.js +31 -7
- package/lib/possession.js +575 -0
- package/lib/related-cert.js +708 -0
- package/lib/revocation-fetch.js +6 -6
- package/lib/scep.js +15 -5
- package/lib/schema-c509.js +5 -2
- package/lib/schema-cmc.js +1 -1
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +2 -15
- package/lib/schema-csr.js +69 -0
- package/lib/schema-engine.js +10 -3
- package/lib/schema-pkix.js +267 -4
- package/lib/schema-tsp.js +7 -6
- package/lib/sign-scheme.js +22 -9
- package/lib/sigstore.js +684 -168
- package/lib/sleep.js +8 -1
- package/lib/smime.js +22 -1
- package/lib/tlog.js +1235 -0
- package/lib/tls-cert-compress.js +25 -8
- package/lib/tsp-sign.js +120 -35
- package/lib/tuf.js +1395 -0
- package/lib/validator-tpm.js +4 -1
- package/lib/verify-seam.js +7 -1
- package/lib/webauthn-mds.js +41 -4
- package/lib/webauthn.js +43 -25
- package/lib/webcrypto.js +48 -46
- package/lib/x509-sign.js +225 -19
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
package/lib/inspect.js
CHANGED
|
@@ -409,6 +409,29 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
|
|
|
409
409
|
ocspNoCheck: function (decoded, inner) {
|
|
410
410
|
return inner + "OCSP No Check (this is an OCSP responder certificate; do not check its revocation)";
|
|
411
411
|
},
|
|
412
|
+
/** @internal ITU-T X.509 (2019) clause 9.8. The alternative key and signature run to thousands of
|
|
413
|
+
* octets for a post-quantum algorithm, which is the case the extensions exist for, so each is
|
|
414
|
+
* reported by its algorithm and its length rather than dumped whole. The algorithm is what a reader
|
|
415
|
+
* needs: it says which set of algorithms this certificate is migrating to. */
|
|
416
|
+
subjectAltPublicKeyInfo: function (decoded, inner) {
|
|
417
|
+
return inner + "Algorithm: " + (decoded.algorithm.name || decoded.algorithm.oid) + "\n" +
|
|
418
|
+
inner + "Alternative Public Key: " + decoded.subjectAltPublicKey.length + " bytes";
|
|
419
|
+
},
|
|
420
|
+
altSignatureAlgorithm: function (decoded, inner) {
|
|
421
|
+
return inner + (decoded.name || decoded.oid);
|
|
422
|
+
},
|
|
423
|
+
altSignatureValue: function (decoded, inner) {
|
|
424
|
+
return inner + "Alternative Signature: " + decoded.bytes.length + " bytes";
|
|
425
|
+
},
|
|
426
|
+
/** @internal RFC 9763 sec. 4.1 names a second certificate of the same subject by the digest of the
|
|
427
|
+
* whole certificate, so both fields are shown: the digest is only meaningful beside the algorithm
|
|
428
|
+
* that produced it. */
|
|
429
|
+
relatedCertificate: function (decoded, inner) {
|
|
430
|
+
var alg = decoded.hashAlgorithm || decoded.hashAlgorithmOid || "(unnamed algorithm)";
|
|
431
|
+
var value = Buffer.isBuffer(decoded.hashValue) ? decoded.hashValue : Buffer.alloc(0);
|
|
432
|
+
return inner + "Hash Algorithm: " + alg + "\n" +
|
|
433
|
+
inner + "Related Certificate: " + _hexColon(value, { upper: true });
|
|
434
|
+
},
|
|
412
435
|
subjectKeyIdentifier: function (decoded, inner) {
|
|
413
436
|
return inner + _hexColon(Buffer.isBuffer(decoded) ? decoded : (decoded.bytes || Buffer.alloc(0)), { upper: true });
|
|
414
437
|
},
|
|
@@ -419,8 +442,8 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
|
|
|
419
442
|
decoded.authorityCertIssuer.names.forEach(function (g) { akiLines.push(inner + _gn(g)); });
|
|
420
443
|
}
|
|
421
444
|
if (decoded.authorityCertSerialNumber != null) {
|
|
422
|
-
var sn = (
|
|
423
|
-
|
|
445
|
+
var sn = guard.range.authoredInteger(decoded.authorityCertSerialNumber, _err,
|
|
446
|
+
"inspect/bad-input", "authorityCertSerialNumber").toString(16);
|
|
424
447
|
if (sn.length % 2) sn = "0" + sn;
|
|
425
448
|
akiLines.push(inner + "serial:0x" + sn.toUpperCase());
|
|
426
449
|
}
|
|
@@ -1983,6 +2006,428 @@ function attrCertReport(input) {
|
|
|
1983
2006
|
return L.join("\n") + "\n";
|
|
1984
2007
|
}
|
|
1985
2008
|
|
|
2009
|
+
/** @internal The tag names, inverted out of `asn1.TAGS` so the dump cannot name a universal type
|
|
2010
|
+
* the codec does not have. Tag 0 is the end-of-contents marker X.690 sec. 8.1.5 reserves; the codec
|
|
2011
|
+
* consumes it rather than naming it, and the header walk renders it as a node of its own. */
|
|
2012
|
+
var _DUMP_TAG_NAMES = (function () {
|
|
2013
|
+
var out = Object.create(null);
|
|
2014
|
+
out[0] = "EOC";
|
|
2015
|
+
Object.keys(asn1.TAGS).forEach(function (key) {
|
|
2016
|
+
var name = "";
|
|
2017
|
+
for (var i = 0; i < key.length; i++) name += key[i] === "_" ? " " : key[i];
|
|
2018
|
+
out[asn1.TAGS[key]] = name;
|
|
2019
|
+
});
|
|
2020
|
+
return out;
|
|
2021
|
+
})();
|
|
2022
|
+
var _DUMP_CLASS_LABEL = Object.assign(Object.create(null), {
|
|
2023
|
+
universal: "UNIVERSAL", application: "APPLICATION", private: "PRIVATE",
|
|
2024
|
+
});
|
|
2025
|
+
var _DUMP_CLASS_OF = Object.assign(Object.create(null), {
|
|
2026
|
+
0x00: "universal", 0x40: "application", 0x80: "context", 0xc0: "private",
|
|
2027
|
+
});
|
|
2028
|
+
var _DUMP_TEXT_TAGS = Object.assign(Object.create(null),
|
|
2029
|
+
{ 12: 1, 18: 1, 19: 1, 20: 1, 22: 1, 23: 1, 24: 1, 26: 1, 27: 1, 28: 1, 30: 1 });
|
|
2030
|
+
var _DUMP_MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2031
|
+
var _DUMP_NAME_COLUMN = 26;
|
|
2032
|
+
var _DUMP_OFFSET_COLUMN = 5;
|
|
2033
|
+
var _DUMP_LENGTH_COLUMN = 4;
|
|
2034
|
+
var _DUMP_MAX_TAG_OCTETS = 4;
|
|
2035
|
+
var _DUMP_MAX_LENGTH_OCTETS = 6;
|
|
2036
|
+
|
|
2037
|
+
function _padLeft(s, w) { return s.length >= w ? s : " ".repeat(w - s.length) + s; }
|
|
2038
|
+
|
|
2039
|
+
/** @internal `length` decides one name: X.690 sec. 8.1.5 gives the end-of-contents value a zero
|
|
2040
|
+
* length, so a tag-0 value carrying content is not one and is named for its class and number.
|
|
2041
|
+
* Pass -1 where the length is indefinite, which an end-of-contents value never is. */
|
|
2042
|
+
function _dumpTagName(tagClass, tagNumber, length) {
|
|
2043
|
+
if (tagClass === "context") return "[" + tagNumber + "]";
|
|
2044
|
+
if (tagClass === "universal" && tagNumber === 0 && length !== 0) return _DUMP_CLASS_LABEL.universal + " [0]";
|
|
2045
|
+
if (tagClass === "universal" && _DUMP_TAG_NAMES[tagNumber] !== undefined) return _DUMP_TAG_NAMES[tagNumber];
|
|
2046
|
+
return _DUMP_CLASS_LABEL[tagClass] + " [" + tagNumber + "]";
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function _dumpLine(offset, depth, hl, len, constructed, name, value) {
|
|
2050
|
+
var head = _padLeft(String(offset), _DUMP_OFFSET_COLUMN) + ":d=" + depth + " hl=" + hl +
|
|
2051
|
+
" l=" + _padLeft(String(len), _DUMP_LENGTH_COLUMN) + (constructed ? " cons:" : " prim:");
|
|
2052
|
+
var field = " ".repeat(depth) + name;
|
|
2053
|
+
if (value === null) return head + " " + field;
|
|
2054
|
+
/** @internal One space always separates the name from the value, so a field the column cannot
|
|
2055
|
+
* hold (a long type name at depth) gets that space rather than running into the colon. */
|
|
2056
|
+
var pad = _DUMP_NAME_COLUMN - field.length;
|
|
2057
|
+
return head + " " + field + (pad > 0 ? " ".repeat(pad) : " ") + ":" + value;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function _dumpPrefixed(prefix, omitted) {
|
|
2061
|
+
if (omitted === 0) return prefix;
|
|
2062
|
+
var note = "(" + omitted + " more byte" + (omitted === 1 ? "" : "s") + " not rendered)";
|
|
2063
|
+
return prefix.length === 0 ? note : prefix + " " + note;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function _dumpHex(buf, limit) {
|
|
2067
|
+
if (buf.length === 0) return "(empty)";
|
|
2068
|
+
var keep = buf.length <= limit ? buf.length : limit;
|
|
2069
|
+
return _dumpPrefixed(_hexColon(buf.subarray(0, keep), {}), buf.length - keep);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function _dumpText(buf, limit) {
|
|
2073
|
+
var keep = buf.length <= limit ? buf.length : limit;
|
|
2074
|
+
return _dumpPrefixed(buf.subarray(0, keep).toString("latin1"), buf.length - keep);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/** @internal The magnitude read from the content bytes as a two's-complement BigInt in one pass,
|
|
2078
|
+
* behind the same per-value byte cap `asn1.read.integer` applies, because a byte-at-a-time
|
|
2079
|
+
* accumulate over attacker-length content is quadratic. Past that cap the bytes are rendered and
|
|
2080
|
+
* no magnitude is computed. The hex beside a magnitude is the content as written, so a leading zero
|
|
2081
|
+
* octet survives; a magnitude rendered back to hex would drop it. */
|
|
2082
|
+
function _dumpInteger(content, limit) {
|
|
2083
|
+
if (content.length === 0) return "(empty)";
|
|
2084
|
+
if (content.length > constants.LIMITS.DER_MAX_INTEGER_BYTES + 1) return _dumpHex(content, limit);
|
|
2085
|
+
var mag = BigInt("0x" + content.toString("hex"));
|
|
2086
|
+
var v = (content[0] & 0x80) !== 0 ? mag - (1n << BigInt(content.length * 8)) : mag;
|
|
2087
|
+
var text = v.toString();
|
|
2088
|
+
if (v <= _DUMP_MAX_SAFE && v >= -_DUMP_MAX_SAFE) return text;
|
|
2089
|
+
return text + " (0x" + content.toString("hex") + ")";
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function _dumpBitString(content, limit) {
|
|
2093
|
+
if (content.length === 0) return "(empty)";
|
|
2094
|
+
return content[0] + " unused bits, " + _dumpHex(content.subarray(1), limit);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function _dumpOid(content, limit) {
|
|
2098
|
+
var dotted;
|
|
2099
|
+
try { dotted = asn1.decodeOidContent(content); }
|
|
2100
|
+
catch (_e) { return _dumpHex(content, limit); }
|
|
2101
|
+
var name = oid.name(dotted);
|
|
2102
|
+
return name === undefined ? dotted : dotted + " (" + name + ")";
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/** @internal `maxValueBytes` bounds every rendering whose size grows with the content length. A hex
|
|
2106
|
+
* dump and a text rendering are truncated and the omitted count stated, because a prefix of either
|
|
2107
|
+
* is still true of the value. A magnitude and a dotted OID are read from the whole content and
|
|
2108
|
+
* cannot be rendered from a prefix at all, so past the cap the bytes are what the report can
|
|
2109
|
+
* honestly show. A decoded form of fixed size renders whatever the cap is, since it cannot grow the
|
|
2110
|
+
* report: a BOOLEAN's verdict, and a BIT STRING's unused-bit count above its capped body. */
|
|
2111
|
+
function _dumpValue(tagClass, tagNumber, content, limit) {
|
|
2112
|
+
if (tagClass !== "universal") return _dumpHex(content, limit);
|
|
2113
|
+
if (tagNumber === asn1.TAGS.BOOLEAN) {
|
|
2114
|
+
if (content.length === 1 && content[0] === 0xff) return "TRUE";
|
|
2115
|
+
if (content.length === 1 && content[0] === 0x00) return "FALSE";
|
|
2116
|
+
return _dumpHex(content, limit);
|
|
2117
|
+
}
|
|
2118
|
+
if (tagNumber === asn1.TAGS.INTEGER || tagNumber === asn1.TAGS.ENUMERATED) {
|
|
2119
|
+
return content.length <= limit ? _dumpInteger(content, limit) : _dumpHex(content, limit);
|
|
2120
|
+
}
|
|
2121
|
+
if (tagNumber === asn1.TAGS.BIT_STRING) return _dumpBitString(content, limit);
|
|
2122
|
+
if (tagNumber === asn1.TAGS.NULL) return content.length === 0 ? null : _dumpHex(content, limit);
|
|
2123
|
+
if (tagNumber === asn1.TAGS.OBJECT_IDENTIFIER) {
|
|
2124
|
+
return content.length <= limit ? _dumpOid(content, limit) : _dumpHex(content, limit);
|
|
2125
|
+
}
|
|
2126
|
+
if (_DUMP_TEXT_TAGS[tagNumber] === 1 && _printable(content)) return _dumpText(content, limit);
|
|
2127
|
+
return _dumpHex(content, limit);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function _dumpBytes(n) { return n + (n === 1 ? " byte" : " bytes"); }
|
|
2131
|
+
function _dumpNodes(n) { return n + (n === 1 ? " node" : " nodes"); }
|
|
2132
|
+
function _dumpRemaining(n) { return _dumpBytes(n) + (n === 1 ? " that remains." : " that remain."); }
|
|
2133
|
+
|
|
2134
|
+
/** @internal The node cap records the offset it stopped at, not only that it fired: a report that
|
|
2135
|
+
* covers part of a file has to name where that part ends or a reader cannot tell what it covers. */
|
|
2136
|
+
function _dumpCapped(st, offset) {
|
|
2137
|
+
st.capped = true;
|
|
2138
|
+
st.capOffset = offset;
|
|
2139
|
+
return -1;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/** @internal Renders a tree the strict decoder produced. It applies the node cap and not the depth
|
|
2143
|
+
* cap, because the decode that built this tree was given the same depth cap and refuses a deeper
|
|
2144
|
+
* one, so no node reaching here is past it. */
|
|
2145
|
+
function _dumpNode(node, depth, st) {
|
|
2146
|
+
if (st.capped) return;
|
|
2147
|
+
if (st.nodes >= st.maxNodes) { _dumpCapped(st, node.header.start); return; }
|
|
2148
|
+
st.nodes += 1;
|
|
2149
|
+
var hl = node.header.end - node.header.start;
|
|
2150
|
+
var name = _dumpTagName(node.tagClass, node.tagNumber, node.length);
|
|
2151
|
+
/** @internal `content` and `children` are read without a fallback: a decode gives every primitive
|
|
2152
|
+
* a Buffer content, empty where the value is, and every constructed node a children array. A
|
|
2153
|
+
* fallback for either would be dead code standing in for a shape the decoder guarantees. */
|
|
2154
|
+
if (!node.constructed) {
|
|
2155
|
+
st.lines.push(_dumpLine(node.header.start, depth, hl, node.length, false, name,
|
|
2156
|
+
_dumpValue(node.tagClass, node.tagNumber, node.content, st.maxValueBytes)));
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
st.lines.push(_dumpLine(node.header.start, depth, hl, node.length, true, name, null));
|
|
2160
|
+
var kids = node.children;
|
|
2161
|
+
for (var i = 0; i < kids.length; i++) {
|
|
2162
|
+
_dumpNode(kids[i], depth + 1, st);
|
|
2163
|
+
if (st.capped) return;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/** @internal The header-by-header walk that runs when the strict decoder refuses the input. It reads
|
|
2168
|
+
* a tag and a length, renders the node, and on the first byte it cannot read records the offset and
|
|
2169
|
+
* the reason and unwinds. It returns the offset one past the value, or -1 once a stop or the node cap
|
|
2170
|
+
* has been recorded. It produces text and never a node, so nothing a lax read accepted can reach a
|
|
2171
|
+
* verify path, and it carries the caller's depth and node caps because it walks hostile nesting
|
|
2172
|
+
* exactly as a parse does. */
|
|
2173
|
+
function _dumpWalk(buf, start, limit, depth, st) {
|
|
2174
|
+
if (st.stop !== null || st.capped) return -1;
|
|
2175
|
+
if (depth > st.maxDepth) {
|
|
2176
|
+
st.stop = { offset: start, reason: "nesting exceeds the depth cap " + st.maxDepth + "." };
|
|
2177
|
+
return -1;
|
|
2178
|
+
}
|
|
2179
|
+
if (st.nodes >= st.maxNodes) return _dumpCapped(st, start);
|
|
2180
|
+
var p = start;
|
|
2181
|
+
if (p >= limit) {
|
|
2182
|
+
st.stop = { offset: start, reason: "the input ends where an identifier octet was expected." };
|
|
2183
|
+
return -1;
|
|
2184
|
+
}
|
|
2185
|
+
var first = buf[p]; p += 1;
|
|
2186
|
+
var constructed = (first & 0x20) !== 0;
|
|
2187
|
+
var tagClass = _DUMP_CLASS_OF[first & 0xc0];
|
|
2188
|
+
var tagNumber = first & 0x1f;
|
|
2189
|
+
if (tagNumber === 0x1f) {
|
|
2190
|
+
tagNumber = 0;
|
|
2191
|
+
var seen = 0;
|
|
2192
|
+
for (;;) {
|
|
2193
|
+
if (p >= limit) {
|
|
2194
|
+
st.stop = { offset: start, reason: "the high-tag-number form runs past the end of the input." };
|
|
2195
|
+
return -1;
|
|
2196
|
+
}
|
|
2197
|
+
var tb = buf[p]; p += 1;
|
|
2198
|
+
tagNumber = (tagNumber * 128) + (tb & 0x7f);
|
|
2199
|
+
seen += 1;
|
|
2200
|
+
if ((tb & 0x80) === 0) break;
|
|
2201
|
+
if (seen >= _DUMP_MAX_TAG_OCTETS) {
|
|
2202
|
+
st.stop = { offset: start, reason: "the tag number uses more than " + _DUMP_MAX_TAG_OCTETS + " octets." };
|
|
2203
|
+
return -1;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (p >= limit) {
|
|
2208
|
+
st.stop = { offset: start, reason: "the input ends where a length octet was expected." };
|
|
2209
|
+
return -1;
|
|
2210
|
+
}
|
|
2211
|
+
var lenByte = buf[p]; p += 1;
|
|
2212
|
+
var indefinite = false;
|
|
2213
|
+
var length = 0;
|
|
2214
|
+
if (lenByte === 0x80) {
|
|
2215
|
+
if (!constructed) {
|
|
2216
|
+
st.stop = { offset: start,
|
|
2217
|
+
reason: "an indefinite length on a primitive value has no end-of-contents octets to find." };
|
|
2218
|
+
return -1;
|
|
2219
|
+
}
|
|
2220
|
+
indefinite = true;
|
|
2221
|
+
} else if ((lenByte & 0x80) !== 0) {
|
|
2222
|
+
var n = lenByte & 0x7f;
|
|
2223
|
+
if (n > _DUMP_MAX_LENGTH_OCTETS) {
|
|
2224
|
+
st.stop = { offset: start, reason: "the length uses more than " + _DUMP_MAX_LENGTH_OCTETS + " octets." };
|
|
2225
|
+
return -1;
|
|
2226
|
+
}
|
|
2227
|
+
if (p + n > limit) {
|
|
2228
|
+
st.stop = { offset: start, reason: "the long-form length runs past the end of the input." };
|
|
2229
|
+
return -1;
|
|
2230
|
+
}
|
|
2231
|
+
for (var j = 0; j < n; j++) length = (length * 256) + buf[p + j];
|
|
2232
|
+
p += n;
|
|
2233
|
+
} else {
|
|
2234
|
+
length = lenByte;
|
|
2235
|
+
}
|
|
2236
|
+
var hl = p - start;
|
|
2237
|
+
var contentStart = p;
|
|
2238
|
+
var name = _dumpTagName(tagClass, tagNumber, indefinite ? -1 : length);
|
|
2239
|
+
|
|
2240
|
+
if (!constructed) {
|
|
2241
|
+
if (length > limit - contentStart) {
|
|
2242
|
+
st.stop = { offset: start,
|
|
2243
|
+
reason: "content length " + length + " overruns the " + _dumpRemaining(limit - contentStart) };
|
|
2244
|
+
return -1;
|
|
2245
|
+
}
|
|
2246
|
+
st.nodes += 1;
|
|
2247
|
+
st.lines.push(_dumpLine(start, depth, hl, length, false, name,
|
|
2248
|
+
_dumpValue(tagClass, tagNumber, buf.subarray(contentStart, contentStart + length), st.maxValueBytes)));
|
|
2249
|
+
return contentStart + length;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
st.nodes += 1;
|
|
2253
|
+
var cp = contentStart;
|
|
2254
|
+
var kid;
|
|
2255
|
+
if (indefinite) {
|
|
2256
|
+
st.lines.push(_dumpLine(start, depth, hl, "inf", true, name, null));
|
|
2257
|
+
for (;;) {
|
|
2258
|
+
if (cp + 2 > limit) {
|
|
2259
|
+
st.stop = { offset: start,
|
|
2260
|
+
reason: "the indefinite-length value has no end-of-contents octets before the end of the input." };
|
|
2261
|
+
return -1;
|
|
2262
|
+
}
|
|
2263
|
+
if (buf[cp] === 0x00 && buf[cp + 1] === 0x00) break;
|
|
2264
|
+
kid = _dumpWalk(buf, cp, limit, depth + 1, st);
|
|
2265
|
+
if (kid < 0) return -1;
|
|
2266
|
+
cp = kid;
|
|
2267
|
+
}
|
|
2268
|
+
/** @internal The end-of-contents octets are the one node the walk emits without recursing, so
|
|
2269
|
+
* both caps are applied to them here. Without the depth check an indefinite-length value with
|
|
2270
|
+
* no children renders a node one level past the cap and the report still says it completed. */
|
|
2271
|
+
if (depth + 1 > st.maxDepth) {
|
|
2272
|
+
st.stop = { offset: cp, reason: "nesting exceeds the depth cap " + st.maxDepth + "." };
|
|
2273
|
+
return -1;
|
|
2274
|
+
}
|
|
2275
|
+
if (st.nodes >= st.maxNodes) return _dumpCapped(st, cp);
|
|
2276
|
+
st.nodes += 1;
|
|
2277
|
+
st.lines.push(_dumpLine(cp, depth + 1, 2, 0, false, _DUMP_TAG_NAMES[0], null));
|
|
2278
|
+
return cp + 2;
|
|
2279
|
+
}
|
|
2280
|
+
st.lines.push(_dumpLine(start, depth, hl, length, true, name, null));
|
|
2281
|
+
var contentEnd = contentStart + length;
|
|
2282
|
+
var childLimit = contentEnd > limit ? limit : contentEnd;
|
|
2283
|
+
while (cp < childLimit) {
|
|
2284
|
+
kid = _dumpWalk(buf, cp, childLimit, depth + 1, st);
|
|
2285
|
+
if (kid < 0) return -1;
|
|
2286
|
+
cp = kid;
|
|
2287
|
+
}
|
|
2288
|
+
if (contentEnd > limit) {
|
|
2289
|
+
st.stop = { offset: start,
|
|
2290
|
+
reason: "content length " + length + " overruns the " + _dumpRemaining(limit - contentStart) };
|
|
2291
|
+
return -1;
|
|
2292
|
+
}
|
|
2293
|
+
return contentEnd;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
var KNOWN_DUMP_OPTS = Object.assign(Object.create(null),
|
|
2297
|
+
{ maxBytes: 1, maxDepth: 1, maxNodes: 1, maxValueBytes: 1 });
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* @primitive pki.inspect.asn1
|
|
2301
|
+
* @signature pki.inspect.asn1(input, opts?) -> string
|
|
2302
|
+
* @since 0.8.31
|
|
2303
|
+
* @status stable
|
|
2304
|
+
* @spec X.690, ISO/IEC 8825-1
|
|
2305
|
+
* @defends ASN.1-parser-DoS (CWE-400)
|
|
2306
|
+
* @related pki.asn1.decode, pki.inspect.any
|
|
2307
|
+
*
|
|
2308
|
+
* Render the TLV tree of `input` (a PEM string or DER Buffer) as a structural dump, one
|
|
2309
|
+
* line per value, in the shape `openssl asn1parse` prints: the byte offset, the nesting
|
|
2310
|
+
* depth, the header length, the content length, whether the value is constructed, the tag
|
|
2311
|
+
* name, and for a primitive its rendered content. An OBJECT IDENTIFIER carries its dotted
|
|
2312
|
+
* form and its registry name. A UTCTime or GeneralizedTime renders the bytes as written
|
|
2313
|
+
* rather than a formatted date, so a malformed time is visible instead of normalized away.
|
|
2314
|
+
*
|
|
2315
|
+
* Two routes produce a report and the first line names which one ran. `pki.asn1.decode`
|
|
2316
|
+
* reads the input first, so a conforming file is rendered from the same decoder the rest of
|
|
2317
|
+
* the toolkit parses with. When that decode refuses the bytes, the report names the refusal
|
|
2318
|
+
* and a header-by-header walk renders what it can read: a truncated file, trailing bytes, a
|
|
2319
|
+
* non-minimal length, an indefinite length, a segmented string. The last line of every report
|
|
2320
|
+
* states the outcome: the offset where the structure ends, or the value the walk could not read
|
|
2321
|
+
* through and why, or the bytes that follow the top-level value, or the cap that stopped the
|
|
2322
|
+
* report and where the next value begins. A partial dump cannot be mistaken for a whole
|
|
2323
|
+
* structure. The walk returns text and never a node tree, and it carries the depth and node
|
|
2324
|
+
* caps, so tolerating a broken encoding here relaxes nothing the parsers enforce.
|
|
2325
|
+
*
|
|
2326
|
+
* No value is withheld here. Where the other reports name a key and print none of it, this
|
|
2327
|
+
* one prints bytes, private key material included, which is what a byte-level dump is for.
|
|
2328
|
+
* The caps bound how much of a value is rendered, not whether it is. `pki.inspect.any` and
|
|
2329
|
+
* `pki.inspect.pkcs8` are the reports that name a key without printing it.
|
|
2330
|
+
*
|
|
2331
|
+
* @opts
|
|
2332
|
+
* maxBytes: number, // default and ceiling: C.LIMITS.DER_MAX_BYTES (16 MiB); refuses a
|
|
2333
|
+
* // longer input, and tightens only -- a larger value is refused
|
|
2334
|
+
* maxDepth: number, // default: C.LIMITS.DER_MAX_DEPTH (64); stops at this nesting depth
|
|
2335
|
+
* maxNodes: number, // default: C.LIMITS.DUMP_MAX_NODES; stops after this many nodes
|
|
2336
|
+
* maxValueBytes: number, // default: C.LIMITS.DUMP_MAX_VALUE_BYTES; renders at most this
|
|
2337
|
+
* // many bytes of one value and counts the rest
|
|
2338
|
+
*
|
|
2339
|
+
* @example
|
|
2340
|
+
* var b = pki.asn1.build;
|
|
2341
|
+
* var der = b.sequence([b.integer(1), b.oid("2.5.4.3"), b.nullValue()]);
|
|
2342
|
+
* console.log(pki.inspect.asn1(der));
|
|
2343
|
+
* // ASN.1 structure: 12 bytes, decoded as DER
|
|
2344
|
+
* // 0:d=0 hl=2 l= 10 cons: SEQUENCE
|
|
2345
|
+
* // 2:d=1 hl=2 l= 1 prim: INTEGER :1
|
|
2346
|
+
* // 5:d=1 hl=2 l= 3 prim: OBJECT IDENTIFIER :2.5.4.3 (commonName)
|
|
2347
|
+
* // 10:d=1 hl=2 l= 0 prim: NULL
|
|
2348
|
+
* // Complete: the structure ends at offset 12, the end of the input.
|
|
2349
|
+
* console.log(pki.inspect.asn1(der.subarray(0, 7)));
|
|
2350
|
+
* // ASN.1 structure: 7 bytes, header walk
|
|
2351
|
+
* // DER decode refused: asn1/truncated: content length overruns the buffer
|
|
2352
|
+
* // 0:d=0 hl=2 l= 10 cons: SEQUENCE
|
|
2353
|
+
* // 2:d=1 hl=2 l= 1 prim: INTEGER :1
|
|
2354
|
+
* // Incomplete: the walk could not read the value at offset 5: content length 3 overruns the 0 bytes that remain.
|
|
2355
|
+
*/
|
|
2356
|
+
function asn1Dump(input, opts) {
|
|
2357
|
+
if (opts === undefined || opts === null) opts = {};
|
|
2358
|
+
if (typeof opts !== "object" || Array.isArray(opts)) {
|
|
2359
|
+
throw _err("inspect/bad-input", "pki.inspect.asn1 options must be an object");
|
|
2360
|
+
}
|
|
2361
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_DUMP_OPTS, _err, "inspect/bad-input",
|
|
2362
|
+
"pki.inspect.asn1 has an unknown option ");
|
|
2363
|
+
var o = guard.identifier.snapshotOptions(opts, KNOWN_DUMP_OPTS);
|
|
2364
|
+
/** @internal `maxBytes` tightens and never raises. The shared parse door carries its own byte
|
|
2365
|
+
* ceiling and applies it before this verb sees the bytes, so a larger value could not take effect;
|
|
2366
|
+
* bounding it here refuses that value instead of accepting it and then contradicting it. */
|
|
2367
|
+
var maxBytes = guard.limits.cap(o.maxBytes, "maxBytes", constants.LIMITS.DER_MAX_BYTES,
|
|
2368
|
+
{ max: constants.LIMITS.DER_MAX_BYTES });
|
|
2369
|
+
var maxDepth = guard.limits.depthCap(o.maxDepth, "maxDepth", constants.LIMITS.DER_MAX_DEPTH);
|
|
2370
|
+
var maxNodes = guard.limits.cap(o.maxNodes, "maxNodes", constants.LIMITS.DUMP_MAX_NODES);
|
|
2371
|
+
var maxValueBytes = guard.limits.cap(o.maxValueBytes, "maxValueBytes", constants.LIMITS.DUMP_MAX_VALUE_BYTES);
|
|
2372
|
+
|
|
2373
|
+
var der;
|
|
2374
|
+
try { der = pkix.coerceToDer(input, _INSPECT_ENTRY); }
|
|
2375
|
+
catch (e) { throw _err("inspect/bad-input", "input must be a DER Buffer or a PEM block", e); }
|
|
2376
|
+
/** @internal Read once, and safe to read off `der` because `pkix.coerceToDer` above has already
|
|
2377
|
+
* normalized the caller's input: an own `length` property shadowing the real byte count cannot reach
|
|
2378
|
+
* here. The cap below is therefore a cap on the bytes the walk decodes. */
|
|
2379
|
+
var derLen = der.length;
|
|
2380
|
+
if (derLen === 0) throw _err("inspect/bad-input", "input is empty");
|
|
2381
|
+
if (derLen > maxBytes) {
|
|
2382
|
+
throw _err("inspect/bad-input", "input " + derLen + " bytes exceeds the cap " + maxBytes);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
var head = ["ASN.1 structure: " + derLen + " bytes, decoded as DER"];
|
|
2386
|
+
var node = null;
|
|
2387
|
+
var refusal = null;
|
|
2388
|
+
/** @internal The decoder builds the whole node tree before the renderer counts a line, so the
|
|
2389
|
+
* report's node cap reaches it as an item cap or a file of small values allocates a tree the
|
|
2390
|
+
* report will never print. It is given one node more than the report prints, so a structure just
|
|
2391
|
+
* past the cap is still read strictly and the report says it stopped at the cap; further past it,
|
|
2392
|
+
* the decoder refuses, the report names that refusal, and the walk renders the bounded prefix. */
|
|
2393
|
+
try {
|
|
2394
|
+
node = asn1.decode(der, { maxBytes: maxBytes, maxDepth: maxDepth, maxItems: maxNodes + 1 });
|
|
2395
|
+
}
|
|
2396
|
+
catch (e) {
|
|
2397
|
+
refusal = e;
|
|
2398
|
+
head = ["ASN.1 structure: " + der.length + " bytes, header walk",
|
|
2399
|
+
"DER decode refused: " + (e.code || "error") + ": " + e.message];
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
var st = { lines: [], nodes: 0, maxNodes: maxNodes, maxDepth: maxDepth,
|
|
2403
|
+
maxValueBytes: maxValueBytes, capped: false, capOffset: 0, stop: null };
|
|
2404
|
+
var end;
|
|
2405
|
+
if (refusal === null) {
|
|
2406
|
+
_dumpNode(node, 0, st);
|
|
2407
|
+
end = node.header.start + node.bytes.length;
|
|
2408
|
+
} else {
|
|
2409
|
+
end = _dumpWalk(der, 0, der.length, 0, st);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/** @internal Every stop names the value the walk could not read THROUGH, which is not the offset
|
|
2413
|
+
* reading had reached: an unterminated indefinite length is read to the end of the input and the
|
|
2414
|
+
* value at fault is still the one that opened it. */
|
|
2415
|
+
var tail;
|
|
2416
|
+
if (st.stop !== null) {
|
|
2417
|
+
tail = "Incomplete: the walk could not read the value at offset " + st.stop.offset + ": " +
|
|
2418
|
+
st.stop.reason;
|
|
2419
|
+
} else if (st.capped) {
|
|
2420
|
+
tail = "Incomplete: the report stopped after " + _dumpNodes(maxNodes) + ", the cap on one dump; " +
|
|
2421
|
+
"the next value begins at offset " + st.capOffset + ".";
|
|
2422
|
+
} else if (end < der.length) {
|
|
2423
|
+
tail = "Incomplete: " + _dumpBytes(der.length - end) + " at offset " + end +
|
|
2424
|
+
" follow the top-level value.";
|
|
2425
|
+
} else {
|
|
2426
|
+
tail = "Complete: the structure ends at offset " + end + ", the end of the input.";
|
|
2427
|
+
}
|
|
2428
|
+
return head.concat(st.lines, [tail]).join("\n");
|
|
2429
|
+
}
|
|
2430
|
+
|
|
1986
2431
|
var _INSPECT_BY_FORMAT = Object.assign(Object.create(null), {
|
|
1987
2432
|
x509: certificate, crl: crlReport, csr: csrReport, cms: cmsReport,
|
|
1988
2433
|
pkcs8: pkcs8Report, pkcs12: pkcs12Report, crmf: crmfReport, cmp: cmpReport,
|
|
@@ -2051,6 +2496,7 @@ module.exports = {
|
|
|
2051
2496
|
ocspResponse: ocspResponseReport,
|
|
2052
2497
|
tsp: tspReport,
|
|
2053
2498
|
attrcert: attrCertReport,
|
|
2499
|
+
asn1: asn1Dump,
|
|
2054
2500
|
any: any,
|
|
2055
2501
|
renderedExtensions: Object.keys(EXT_RENDERERS),
|
|
2056
2502
|
};
|