@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +411 -0
  2. package/README.md +67 -16
  3. package/bin/pki.js +716 -30
  4. package/examples/issue-a-certificate.js +90 -0
  5. package/examples/post-quantum.js +66 -0
  6. package/examples/read-a-certificate.js +56 -0
  7. package/examples/revoke-and-check.js +86 -0
  8. package/examples/sign-and-verify-cms.js +63 -0
  9. package/index.js +18 -5
  10. package/lib/acme.js +223 -74
  11. package/lib/alt-sig.js +441 -0
  12. package/lib/asn1-der.js +48 -25
  13. package/lib/attrcert-sign.js +27 -19
  14. package/lib/cbor-det.js +24 -14
  15. package/lib/cmc-build.js +9 -2
  16. package/lib/cmc-verify.js +14 -3
  17. package/lib/cmp-build.js +17 -5
  18. package/lib/cmp-session.js +24 -11
  19. package/lib/cmp-verify.js +28 -19
  20. package/lib/cms-compress.js +3 -1
  21. package/lib/cms-decrypt.js +102 -7
  22. package/lib/cms-encrypt.js +59 -7
  23. package/lib/cms-sign.js +31 -9
  24. package/lib/cms-verify.js +22 -18
  25. package/lib/composite-kem.js +30 -18
  26. package/lib/composite-sig.js +29 -14
  27. package/lib/constants.js +28 -0
  28. package/lib/crl-sign.js +111 -19
  29. package/lib/crmf-sign.js +167 -15
  30. package/lib/csr-sign.js +117 -6
  31. package/lib/ct.js +939 -48
  32. package/lib/est.js +29 -16
  33. package/lib/framework-error.js +12 -0
  34. package/lib/guard-async.js +1 -1
  35. package/lib/guard-bytes.js +103 -20
  36. package/lib/guard-crypto.js +205 -3
  37. package/lib/guard-encoding.js +6 -1
  38. package/lib/guard-identifier.js +55 -8
  39. package/lib/guard-intrinsic.js +43 -0
  40. package/lib/guard-json.js +122 -9
  41. package/lib/guard-limits.js +12 -1
  42. package/lib/guard-list.js +63 -8
  43. package/lib/guard-name.js +167 -20
  44. package/lib/guard-range.js +43 -5
  45. package/lib/guard-text.js +41 -0
  46. package/lib/guard-verdict.js +39 -8
  47. package/lib/hpke.js +688 -42
  48. package/lib/http-transport.js +156 -4
  49. package/lib/identity-match.js +4 -4
  50. package/lib/inspect.js +448 -2
  51. package/lib/jose.js +93 -25
  52. package/lib/key.js +18 -13
  53. package/lib/lint.js +165 -1
  54. package/lib/merkle.js +233 -27
  55. package/lib/ocsp.js +27 -14
  56. package/lib/oid.js +14 -4
  57. package/lib/path-validate.js +143 -61
  58. package/lib/pkcs12-build.js +7 -2
  59. package/lib/pki-build.js +31 -7
  60. package/lib/possession.js +575 -0
  61. package/lib/related-cert.js +708 -0
  62. package/lib/revocation-fetch.js +6 -6
  63. package/lib/scep.js +15 -5
  64. package/lib/schema-c509.js +5 -2
  65. package/lib/schema-cmc.js +1 -1
  66. package/lib/schema-cmp.js +8 -8
  67. package/lib/schema-cms.js +2 -15
  68. package/lib/schema-csr.js +69 -0
  69. package/lib/schema-engine.js +10 -3
  70. package/lib/schema-pkix.js +267 -4
  71. package/lib/schema-tsp.js +7 -6
  72. package/lib/sign-scheme.js +22 -9
  73. package/lib/sigstore.js +684 -168
  74. package/lib/sleep.js +8 -1
  75. package/lib/smime.js +22 -1
  76. package/lib/tlog.js +1235 -0
  77. package/lib/tls-cert-compress.js +25 -8
  78. package/lib/tsp-sign.js +120 -35
  79. package/lib/tuf.js +1395 -0
  80. package/lib/validator-tpm.js +4 -1
  81. package/lib/verify-seam.js +7 -1
  82. package/lib/webauthn-mds.js +41 -4
  83. package/lib/webauthn.js +43 -25
  84. package/lib/webcrypto.js +48 -46
  85. package/lib/x509-sign.js +225 -19
  86. package/package.json +2 -1
  87. 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 = (typeof decoded.authorityCertSerialNumber === "bigint"
423
- ? decoded.authorityCertSerialNumber : BigInt(decoded.authorityCertSerialNumber)).toString(16);
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
  };