@blamejs/pki 0.8.28 → 0.8.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/inspect.js CHANGED
@@ -31,6 +31,15 @@ var x509 = require("./schema-x509");
31
31
  var crl = require("./schema-crl");
32
32
  var csr = require("./schema-csr");
33
33
  var cms = require("./schema-cms");
34
+ var pkcs8 = require("./schema-pkcs8");
35
+ var pkcs12 = require("./schema-pkcs12");
36
+ var crmf = require("./schema-crmf");
37
+ var cmp = require("./schema-cmp");
38
+ var csrattrs = require("./schema-csrattrs");
39
+ var trustanchor = require("./schema-trustanchor");
40
+ var ocsp = require("./schema-ocsp");
41
+ var tsp = require("./schema-tsp");
42
+ var attrcert = require("./schema-attrcert");
34
43
  var schemaAll = require("./schema-all");
35
44
  var pkix = require("./schema-pkix");
36
45
  var guard = require("./guard-all");
@@ -84,7 +93,10 @@ function _algName(a) { return (a && (a.name || a.oid)) || "unknown"; }
84
93
 
85
94
 
86
95
  function _serial(cert, indent) {
87
- var hex = cert.serialNumberHex || "";
96
+ /** @internal Only a hex string is read as one. A caller's pre-parsed object can carry anything here, and
97
+ * the byte reader below faults on a value that is not a string rather than reporting an unreadable one. */
98
+ var hex = typeof cert.serialNumberHex === "string" && _isHexDigits(cert.serialNumberHex)
99
+ ? cert.serialNumberHex : "";
88
100
  if (hex.length % 2) hex = "0" + hex;
89
101
  var buf = Buffer.from(hex, "hex");
90
102
  if (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80)) buf = buf.subarray(1);
@@ -485,6 +497,15 @@ function _extension(ext, pad) {
485
497
  if (body != null) return header + "\n" + body;
486
498
  } catch (_e) { }
487
499
  }
500
+ /** @internal An extension whose own parser decoded it, which is how the attribute-certificate and
501
+ * revocation-list tables reach here: the certificate decoders above do not know those OIDs, and without
502
+ * this the decoded value fell through to a hex dump of the bytes it was decoded from. */
503
+ if (ext.decoded && typeof ext.decoded === "object" && ext.decoded.opaque !== true) {
504
+ var fromParser = _decodedRecord(ext.decoded, inner);
505
+ if (fromParser) return header + "\n" + fromParser;
506
+ }
507
+ var enriched = _extraFields(ext, _ROW_PLUMBING, inner);
508
+ if (enriched) return header + "\n" + enriched;
488
509
  return header + "\n" + _fallback(ext.value, inner);
489
510
  }
490
511
 
@@ -596,6 +617,7 @@ function _parseCms(input) { return _coerce(input, { marker: "contentType", looks
596
617
 
597
618
 
598
619
  var OID_EXTENSION_REQUEST = oid.byName("extensionRequest");
620
+ var OID_SUBJECT_KEY_ID = oid.byName("subjectKeyIdentifier");
599
621
  var OID_CONTENT_TYPE = oid.byName("contentType");
600
622
  var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
601
623
  var OID_SIGNING_TIME = oid.byName("signingTime");
@@ -686,6 +708,75 @@ function crlReport(input) {
686
708
  }
687
709
 
688
710
 
711
+ /** @internal Whether a value is one of the four shapes the name renderer reads: a parsed Name, a
712
+ * GeneralNames list, a decoded GeneralName, or a raw one still carrying its context tag. Anything else is a
713
+ * record and is rendered as its fields. */
714
+ function _looksLikeName(v) {
715
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
716
+ if (v.dn !== undefined || Array.isArray(v.rdns) || Array.isArray(v.names)) return true;
717
+ return typeof v.tagNumber === "number" && (v.value !== undefined || Buffer.isBuffer(v.bytes));
718
+ }
719
+
720
+ /** @internal A decoded record as label/value lines. Each field is rendered by the vocabulary that already
721
+ * exists for its kind, because the alternative is string conversion, which prints a decoded value as its
722
+ * object notation. Depth is bounded so a record carrying a parsed structure cannot walk the whole of it. */
723
+ function _decodedRecord(rec, pad, depth) {
724
+ if (depth === undefined) depth = 3;
725
+ var lines = [];
726
+ Object.keys(rec).forEach(function (k) {
727
+ var v = rec[k];
728
+ if (v === null || v === undefined || k === "opaque") return;
729
+ if (Buffer.isBuffer(v)) { lines.push(pad + k + ": " + _hexColon(v, {})); return; }
730
+ if (v instanceof Date) { lines.push(pad + k + ": " + _date(v)); return; }
731
+ if (typeof v !== "object") { lines.push(pad + k + ": " + _clean(String(v))); return; }
732
+ /** @internal Only a value that LOOKS like a name is rendered as one. Asked of every record, the name
733
+ * renderer answers a non-name with the truthy `tag<undefined>:`, and that answer then stood in for the
734
+ * record's own fields: a group attribute rendered as that string instead of its memberships. */
735
+ if (_looksLikeName(v)) {
736
+ var asName = _nameOrGn(v);
737
+ if (asName && asName !== "(none)" && asName !== "(empty)" && asName !== "(unrecognized name form)") {
738
+ lines.push(pad + k + ": " + asName);
739
+ return;
740
+ }
741
+ }
742
+ if (depth <= 0) return;
743
+ if (Array.isArray(v)) {
744
+ if (!v.length) return;
745
+ lines.push(pad + k + ":");
746
+ v.forEach(function (m) {
747
+ if (m === null || typeof m !== "object" || Buffer.isBuffer(m)) {
748
+ lines.push(pad + " " + (Buffer.isBuffer(m) ? _hexColon(m, {}) : _clean(String(m))));
749
+ } else {
750
+ var sub = _decodedRecord(m, pad + " ", depth - 1);
751
+ if (sub) lines.push(sub);
752
+ }
753
+ });
754
+ return;
755
+ }
756
+ var nested = _decodedRecord(v, pad + " ", depth - 1);
757
+ if (nested) { lines.push(pad + k + ":"); lines.push(nested); }
758
+ });
759
+ return lines.join("\n");
760
+ }
761
+
762
+ /** @internal The fields a parser ENRICHED a row with, which are whatever it carries beyond the plumbing every
763
+ * row has. A table that decodes its own values attaches them here rather than under `decoded`, and routing
764
+ * such a row through a renderer that knows only the plumbing printed the bytes the parser had already read:
765
+ * an EST key-size constraint as its DER, an OCSP archive cutoff as a GeneralizedTime's octets. */
766
+ function _extraFields(row, skip, pad) {
767
+ var extra = Object.create(null), any = false;
768
+ Object.keys(row).forEach(function (k) {
769
+ if (skip.indexOf(k) !== -1) return;
770
+ var v = row[k];
771
+ if (v === null || v === undefined || v === false) return;
772
+ extra[k] = v;
773
+ any = true;
774
+ });
775
+ return any ? _decodedRecord(extra, pad) : "";
776
+ }
777
+ var _ROW_PLUMBING = ["oid", "name", "critical", "value", "valueBytes", "bytes", "scope", "index",
778
+ "containerIndex", "state", "code", "decoded", "kind", "values", "type", "extensions"];
779
+
689
780
  function _attribute(attr, pad) {
690
781
  var inner = pad + " ";
691
782
  if (attr.type === OID_EXTENSION_REQUEST && Array.isArray(attr.extensions)) {
@@ -693,8 +784,27 @@ function _attribute(attr, pad) {
693
784
  attr.extensions.forEach(function (ext) { lines.push(_extension(ext, inner)); });
694
785
  return lines.join("\n");
695
786
  }
696
- var header = pad + (attr.name || oid.name(attr.type) || attr.type) + ":";
697
- var vals = (attr.values || []).map(function (v) { return _attrValue(attr.type, v, inner); });
787
+ /** @internal The registry lookup takes a dotted identifier, and a caller's object can carry anything in
788
+ * `type`: asked about a number it refuses with its own code, which is not this verb's contract. */
789
+ var registryName = null;
790
+ if (!attr.name && typeof attr.type === "string") {
791
+ try { registryName = oid.name(attr.type); }
792
+ // allow:swallow-unverified drop-silent display fallback: an unreadable type prints as itself
793
+ catch (_notAnIdentifier) { registryName = null; }
794
+ }
795
+ var header = pad + (attr.name || registryName || String(attr.type)) + ":";
796
+ /** @internal Where the parser DECODED the value, render that rather than the bytes it decoded from: an
797
+ * attribute certificate's whole content is the attributes it asserts, and a role naming a URI printed as
798
+ * a hex run said nothing a reader could act on. An entry the decoder left opaque still prints as bytes. */
799
+ var vals;
800
+ if (Array.isArray(attr.decoded) && attr.decoded.length === _list(attr.values).length) {
801
+ vals = attr.decoded.map(function (d, i) {
802
+ if (!d || typeof d !== "object" || d.opaque === true) return _attrValue(attr.type, attr.values[i], inner);
803
+ return _decodedRecord(d, inner) || _attrValue(attr.type, attr.values[i], inner);
804
+ });
805
+ } else {
806
+ vals = _list(attr.values).map(function (v) { return _attrValue(attr.type, v, inner); });
807
+ }
698
808
  return vals.length ? header + "\n" + vals.join("\n") : header;
699
809
  }
700
810
 
@@ -864,7 +974,1440 @@ function cmsReport(input) {
864
974
  }
865
975
 
866
976
 
867
- var _INSPECT_BY_FORMAT = Object.assign(Object.create(null), { x509: certificate, crl: crlReport, csr: csrReport, cms: cmsReport });
977
+ /** @internal The remaining detected formats, each composing the vocabulary above and each a named verb as
978
+ * well as a route from `any`.
979
+ *
980
+ * NO REPORT PRINTS PRIVATE KEY MATERIAL. A PKCS#8 and a PKCS#12 both carry a private key, and a renderer
981
+ * that wrote one out would make an inspection tool a way of reading a key out of a file somebody opened
982
+ * only to look at it. Each shows the algorithm, the public half where the structure carries one, and that
983
+ * a private key is present, never its bytes. `inspect-formats.test.js` searches each report for byte runs
984
+ * of the key in hex, base64 and colon-hex, so the rule is measured rather than stated here. */
985
+
986
+ /** @internal Each predicate asks whether the object has the STRUCTURE its parser returns, not whether it
987
+ * carries the marker property, because an object that merely has the marker is a caller's own object and
988
+ * a report rendered from one says what the caller wrote rather than what any bytes encode. The marker
989
+ * selects the predicate; the predicate is what admits the object. */
990
+ function _looksParsedPkcs8(o) {
991
+ return typeof o.version === "number" && o.privateKeyAlgorithm && typeof o.privateKeyAlgorithm === "object" &&
992
+ Buffer.isBuffer(o.privateKey) && Array.isArray(o.attributes);
993
+ }
994
+ function _looksParsedPkcs12(o) {
995
+ return typeof o.version === "number" && typeof o.integrityMode === "string" &&
996
+ Array.isArray(o.safeBags) && Array.isArray(o.encryptedSafes);
997
+ }
998
+ /** @internal Every message has to carry the structure the renderer reads. Checking only that `messages` is
999
+ * an array admitted `{ messages: [{}] }`, and the renderer then dereferenced a certTemplate that was not
1000
+ * there, so a caller's own object left an untyped TypeError where this door promises inspect/bad-input. */
1001
+ function _looksParsedCrmf(o) {
1002
+ if (!Array.isArray(o.messages) || !o.messages.length) return false;
1003
+ return o.messages.every(function (m) {
1004
+ return !!m && typeof m === "object" && !!m.certReq && typeof m.certReq === "object" &&
1005
+ !!m.certReq.certTemplate && typeof m.certReq.certTemplate === "object";
1006
+ });
1007
+ }
1008
+ function _looksParsedCmp(o) {
1009
+ return o.header && typeof o.header === "object" && o.body && typeof o.body === "object" &&
1010
+ typeof o.body.arm === "string";
1011
+ }
1012
+ /** @internal Every item has to be one of the two shapes the parser produces, a bare object identifier or an
1013
+ * attribute carrying a values array, because the report reads both: an array of nullish or wrong-typed
1014
+ * members faulted the renderer where this door promises a typed refusal. */
1015
+ function _looksParsedCsrattrs(o) {
1016
+ if (!Array.isArray(o.items)) return false;
1017
+ return o.items.every(function (it) {
1018
+ if (!it || typeof it !== "object") return false;
1019
+ if (it.kind === "oid") return true;
1020
+ return it.kind === "attribute" && (it.values === undefined || it.values === null || Array.isArray(it.values));
1021
+ });
1022
+ }
1023
+ /** @internal Each anchor has to carry the member its own kind names, because that is what the report reads:
1024
+ * an anchor with a kind and nothing under it faulted the renderer where this door promises a typed
1025
+ * refusal. */
1026
+ function _looksParsedTrustAnchor(o) {
1027
+ if (!Array.isArray(o.anchors) || !o.anchors.length) return false;
1028
+ return o.anchors.every(function (a) {
1029
+ if (!a || typeof a !== "object" || typeof a.kind !== "string") return false;
1030
+ var held = a.kind === "certificate" ? a.certificate : (a.kind === "tbsCert" ? a.tbsCert : a.taInfo);
1031
+ return !!held && typeof held === "object";
1032
+ });
1033
+ }
1034
+ function _looksParsedOcspReq(o) {
1035
+ if (typeof o.version !== "number" || !Array.isArray(o.requestList) || !Buffer.isBuffer(o.tbsRequestBytes)) return false;
1036
+ return o.requestList.every(function (r) { return !!r && typeof r === "object" && !!r.certID && typeof r.certID === "object"; });
1037
+ }
1038
+ function _looksParsedOcspResp(o) {
1039
+ var st = o.responseStatus;
1040
+ if (!st || typeof st !== "object" || typeof st.code !== "number" || typeof st.name !== "string") return false;
1041
+ var br = o.basicResponse;
1042
+ /** @internal A response with no basic response is a status-only answer the report handles, but one that
1043
+ * HAS a basic response has to carry the answers the report walks: a responderID and a responses list whose
1044
+ * entries name a CertID and a status. */
1045
+ if (br === null || br === undefined) return true;
1046
+ if (typeof br !== "object" || !br.responderID || typeof br.responderID !== "object") return false;
1047
+ if (!Array.isArray(br.responses)) return false;
1048
+ return br.responses.every(function (r) {
1049
+ return !!r && typeof r === "object" && !!r.certID && typeof r.certID === "object" &&
1050
+ !!r.certStatus && typeof r.certStatus === "object";
1051
+ });
1052
+ }
1053
+ /** @internal A rejected or waiting response carries `timeStampToken: null`, which is the shape RFC 3161
1054
+ * sec. 2.4.2 gives a non-granted status, so the token is required to be PRESENT as a key and not to be an
1055
+ * object. Requiring an object here refused a parsed rejection that the same bytes rendered fine. */
1056
+ /** @internal RFC 3161 sec. 2.4.2's PKIStatus. */
1057
+ var TSP_STATUS_NAMES = Object.assign(Object.create(null), {
1058
+ 0: "granted", 1: "grantedWithMods", 2: "rejection", 3: "waiting",
1059
+ 4: "revocationWarning", 5: "revocationNotification",
1060
+ });
1061
+
1062
+ function _looksParsedTsp(o) {
1063
+ if (typeof o.status !== "number" || !("timeStampToken" in o)) return false;
1064
+ /** @internal PKIFailureInfo is a bit record whose asserted names are an array, so a present failInfo of any
1065
+ * other shape is a caller's own object and not this parser's output. The readers below still fault on
1066
+ * nothing, and this makes an obviously wrong shape a typed refusal rather than an empty line. */
1067
+ if (o.failInfo !== null && o.failInfo !== undefined) {
1068
+ if (typeof o.failInfo !== "object" || !Array.isArray(o.failInfo.bits)) return false;
1069
+ }
1070
+ var tok = o.timeStampToken;
1071
+ return tok === null || (!!tok && typeof tok === "object" && !!tok.tstInfo && typeof tok.tstInfo === "object");
1072
+ }
1073
+ function _looksParsedAttrCert(o) {
1074
+ return typeof o.version === "number" && o.holder && typeof o.holder === "object" &&
1075
+ !!o.issuer && typeof o.issuer === "object" &&
1076
+ Array.isArray(o.attributes) && o.signatureAlgorithm && typeof o.signatureAlgorithm === "object";
1077
+ }
1078
+
1079
+ function _parsePkcs8(input) { return _coerce(input, { marker: "privateKeyAlgorithm", looksParsed: _looksParsedPkcs8, parsedName: "pki.schema.pkcs8.parse", parse: pkcs8.parse, badCode: "inspect/bad-pkcs8", what: "PKCS#8 private key" }); }
1080
+ function _parsePkcs12(input) { return _coerce(input, { marker: "safeBags", looksParsed: _looksParsedPkcs12, parsedName: "pki.schema.pkcs12.parse", parse: pkcs12.parse, badCode: "inspect/bad-pkcs12", what: "PKCS#12 store" }); }
1081
+ function _parseCrmf(input) { return _coerce(input, { marker: "messages", looksParsed: _looksParsedCrmf, parsedName: "pki.schema.crmf.parse", parse: crmf.parse, badCode: "inspect/bad-crmf", what: "CRMF certificate request message" }); }
1082
+ function _parseCmp(input) { return _coerce(input, { marker: "header", looksParsed: _looksParsedCmp, parsedName: "pki.schema.cmp.parse", parse: cmp.parse, badCode: "inspect/bad-cmp", what: "CMP message" }); }
1083
+ function _parseCsrattrs(input) { return _coerce(input, { marker: "items", looksParsed: _looksParsedCsrattrs, parsedName: "pki.schema.csrattrs.parse", parse: csrattrs.parse, badCode: "inspect/bad-csrattrs", what: "CSR attributes response" }); }
1084
+ function _parseTrustAnchor(input) { return _coerce(input, { marker: "anchors", looksParsed: _looksParsedTrustAnchor, parsedName: "pki.schema.trustanchor.parse", parse: trustanchor.parse, badCode: "inspect/bad-trustanchor", what: "trust anchor list" }); }
1085
+ function _parseOcspReq(input) { return _coerce(input, { marker: "requestList", looksParsed: _looksParsedOcspReq, parsedName: "pki.schema.ocsp.parseRequest", parse: ocsp.parseRequest, badCode: "inspect/bad-ocsp-request", what: "OCSP request" }); }
1086
+ function _parseOcspResp(input) { return _coerce(input, { marker: "responseStatus", looksParsed: _looksParsedOcspResp, parsedName: "pki.schema.ocsp.parseResponse", parse: ocsp.parseResponse, badCode: "inspect/bad-ocsp-response", what: "OCSP response" }); }
1087
+ function _parseTsp(input) { return _coerce(input, { marker: "timeStampToken", looksParsed: _looksParsedTsp, parsedName: "pki.schema.tsp.parseResponse", parse: tsp.parseResponse, badCode: "inspect/bad-tsp", what: "timestamp response" }); }
1088
+ function _parseAttrCert(input) { return _coerce(input, { marker: "holder", looksParsed: _looksParsedAttrCert, parsedName: "pki.schema.attrcert.parse", parse: attrcert.parse, badCode: "inspect/bad-attrcert", what: "attribute certificate" }); }
1089
+
1090
+ /** @internal A GeneralName, a distinguished name, a GeneralNames list, or nothing, whichever the field
1091
+ * carries. These structures reach a name through four different shapes and a report reads one the same
1092
+ * way wherever it sits. The list form arrives with each member still raw, carrying its context tag and
1093
+ * its bytes and no decoded value, so each is read through the attribute-certificate module's own
1094
+ * GeneralName reader rather than rendered from the wrapper, which is what printed the whole list as one
1095
+ * hex run under a tag number the wrapper does not have. */
1096
+ function _nameOrGn(v) {
1097
+ if (!v || typeof v !== "object") return "(none)";
1098
+ if (v.dn !== undefined || Array.isArray(v.rdns)) return _dnString(v) || "(empty)";
1099
+ if (Array.isArray(v.names)) {
1100
+ var parts = v.names.map(function (n) {
1101
+ if (n && n.value !== undefined) return _gn(n);
1102
+ if (n && Buffer.isBuffer(n.bytes)) {
1103
+ try { return _gn(attrcert.readGeneralName(n.bytes)); }
1104
+ // allow:swallow-unverified drop-silent display fallback: an unreadable name prints as its bytes
1105
+ catch (_unreadable) { return _gnRaw(n.bytes); }
1106
+ }
1107
+ return "";
1108
+ }).filter(function (s) { return s !== ""; });
1109
+ return parts.length ? parts.join(", ") : "(empty)";
1110
+ }
1111
+ /** @internal A STANDALONE GeneralName arrives raw from some parsers exactly as a list member does, and it
1112
+ * needs the same decode: passed straight to the renderer, a directoryName printed as its label with no
1113
+ * name after it and every other form printed its bytes. The list path had this and the single path did
1114
+ * not, which is one rule applied to one of its two callers. */
1115
+ if (v.value === undefined && Buffer.isBuffer(v.bytes) && typeof v.tagNumber === "number") {
1116
+ try { return _gn(attrcert.readGeneralName(v.bytes)); }
1117
+ // allow:swallow-unverified drop-silent display fallback: an unreadable name prints as its bytes
1118
+ catch (_unreadableSingle) { return _gnRaw(v.bytes); }
1119
+ }
1120
+ var g = _gn(v);
1121
+ return g || "(unrecognized name form)";
1122
+ }
1123
+ function _optLine(L, pad, label, v) { if (v !== null && v !== undefined) L.push(pad + label + ": " + v); }
1124
+
1125
+ /** @internal Join a short list the way a sentence does. */
1126
+ function _joinAnd(list) {
1127
+ if (list.length <= 1) return list.join("");
1128
+ return list.slice(0, -1).join(", ") + " and " + list[list.length - 1];
1129
+ }
1130
+
1131
+ /** @internal A list is a list only when it IS one. `x && x.length` is true of a string and of anything else
1132
+ * carrying a length, and `(x || [])` passes a truthy non-array straight through, so both admitted a value
1133
+ * the following forEach then faulted on. These reports take a caller's pre-parsed object, whose members no
1134
+ * door can validate to arbitrary depth, so a report must not fault on any shape it is handed. */
1135
+ function _list(v) { return Array.isArray(v) ? v : []; }
1136
+ /** @internal The same for a member a loop body dereferences. */
1137
+ function _obj(v) { return (v !== null && typeof v === "object") ? v : {}; }
1138
+
1139
+ /** @internal A serial reads the same wherever a report carries one, through the certificate report's own
1140
+ * presentation: small values as decimal with the hex beside them, long ones as wrapped colon-hex. Printed
1141
+ * as bare hex, the serial 99 renders "63" and reads as the decimal 63. */
1142
+ function _serialLine(hexOrValue, pad) {
1143
+ if (hexOrValue === null || hexOrValue === undefined) return null;
1144
+ /** @internal Only the two forms a parser produces, a hex string or a bigint. Anything else is a caller's
1145
+ * own value, and `toString(16)` on it reaches the hex reader with something that is not hex. */
1146
+ var hex;
1147
+ if (typeof hexOrValue === "string") hex = hexOrValue;
1148
+ else if (typeof hexOrValue === "bigint" || typeof hexOrValue === "number") hex = hexOrValue.toString(16);
1149
+ else return null;
1150
+ if (!_isHexDigits(hex)) return null;
1151
+ return pad + _serial({ serialNumberHex: hex }, pad.length + 4);
1152
+ }
1153
+ /** @internal Whether every character is a hex digit, by character code so no regex is involved. */
1154
+ function _isHexDigits(s) {
1155
+ if (typeof s !== "string" || s.length === 0) return false;
1156
+ for (var i = 0; i < s.length; i++) {
1157
+ var c = s.charCodeAt(i);
1158
+ var ok = (c >= 0x30 && c <= 0x39) || (c >= 0x61 && c <= 0x66) || (c >= 0x41 && c <= 0x46);
1159
+ if (!ok) return false;
1160
+ }
1161
+ return true;
1162
+ }
1163
+ function _serialInto(L, pad, hexOrValue) {
1164
+ var line = _serialLine(hexOrValue, pad);
1165
+ if (line !== null) L.push(line);
1166
+ }
1167
+
1168
+ /**
1169
+ * @primitive pki.inspect.pkcs8
1170
+ * @signature pki.inspect.pkcs8(input) -> string
1171
+ * @since 0.8.29
1172
+ * @status stable
1173
+ * @spec RFC 5958
1174
+ * @related pki.schema.pkcs8.parse, pki.inspect.pkcs12
1175
+ *
1176
+ * Render a PKCS#8 private key as a text report: the version, the key algorithm and
1177
+ * its parameters, the presence and length of the private key and of a public half,
1178
+ * and the type of each attribute. No byte string from inside the file is rendered.
1179
+ * That is structural rather than a screen on the private key, because RFC 5958 has
1180
+ * the parser check that a v2 `OneAsymmetricKey` HAS a public half and well formed
1181
+ * attributes, never that either is unrelated to the private key: private bytes can
1182
+ * sit in the public `BIT STRING` or in any attribute value, so a report that wrote
1183
+ * either out could be made to write the key out. `pki.key.export` is the verb for
1184
+ * the bytes themselves.
1185
+ * `input` is a PEM string, a DER Buffer, or a `pki.schema.pkcs8.parse` result; a
1186
+ * non-PKCS#8 throws `inspect/bad-pkcs8`, a wrong-type input `inspect/bad-input`.
1187
+ *
1188
+ * @example
1189
+ * var pair = await pki.key.generate("Ed25519");
1190
+ * pki.inspect.pkcs8(await pki.key.export(pair.privateKey)).split("\n")[0]; // "PKCS#8 Private Key:"
1191
+ */
1192
+ function pkcs8Report(input) {
1193
+ var k = _parsePkcs8(input);
1194
+ var L = ["PKCS#8 Private Key:", " Version: " + k.version];
1195
+ L.push(" Private Key Algorithm: " + _algName(k.privateKeyAlgorithm));
1196
+ /** @internal The algorithm's PARAMETERS say which curve an EC key is on, and without them an EC report
1197
+ * named only `ecPublicKey` and identified nothing. They are public: an AlgorithmIdentifier's parameters
1198
+ * describe the algorithm, not the key, so rendering them discloses nothing the key itself holds. */
1199
+ var alg = k.privateKeyAlgorithm;
1200
+ if (alg && Buffer.isBuffer(alg.parameters)) {
1201
+ var paramName;
1202
+ try { paramName = oid.name(asn1.read.oid(asn1.decode(alg.parameters))); }
1203
+ // allow:swallow-unverified drop-silent display fallback: parameters that are not an OID are not named
1204
+ catch (_notAnOid) { paramName = null; }
1205
+ /** @internal Named or not rendered. An AlgorithmIdentifier's parameters are whatever the file's author
1206
+ * put there, so a hex fallback here is another way to write the private key out: the parameters of an
1207
+ * unknown algorithm can hold a copy of it and the parser has no reason to refuse that. */
1208
+ L.push(" Algorithm Parameters: " + (paramName ||
1209
+ ("present, " + alg.parameters.length + " bytes (not rendered)")));
1210
+ }
1211
+ L.push(" Private Key: present, " + k.privateKey.length + " bytes (not rendered)");
1212
+ /** @internal The public half is rendered only when the bytes are actually there: an RFC 5958 OneAsymmetricKey
1213
+ * carries it as a BIT STRING record and a caller's object can carry anything, and the byte renderer faults
1214
+ * on a value that is not a buffer rather than reporting one it cannot read. */
1215
+ /** @internal THIS REPORT RENDERS NO BYTES FROM THE FILE, and the rule is structural rather than a
1216
+ * comparison because a comparison loses. RFC 5958's parser checks that a v2 OneAsymmetricKey HAS a public
1217
+ * half and that its attributes are well formed, never that either is unrelated to the private key, so
1218
+ * private bytes can sit in the public BIT STRING or in any attribute value. Screening each against the
1219
+ * private key needs the key's algorithm-specific inner encoding, since an Ed25519 privateKey is
1220
+ * `04 20 || seed` and a crafted public half carrying the seed alone matches neither direction of a
1221
+ * whole-buffer compare; deriving the public half instead would be a key operation, which this synchronous
1222
+ * renderer cannot do. So nothing from inside the file is written out: the report names the algorithm, its
1223
+ * parameters, what is present and how long, and the attribute types, and there is nothing left to screen.
1224
+ * `pki.key.export` is the verb for the bytes themselves. */
1225
+ var pubBytes = Buffer.isBuffer(k.publicKey) ? k.publicKey : _obj(k.publicKey).bytes;
1226
+ if (Buffer.isBuffer(pubBytes)) {
1227
+ L.push(" Public Key: present, " + pubBytes.length + " bytes (not rendered)");
1228
+ } else if (k.publicKey) {
1229
+ L.push(" Public Key: present, not readable as bytes");
1230
+ }
1231
+ L.push(" Attributes:");
1232
+ if (_list(k.attributes).length) {
1233
+ _list(k.attributes).forEach(function (raw) {
1234
+ var a = _obj(raw);
1235
+ /** @internal The registry takes a dotted identifier and refuses anything else with its own code, which
1236
+ * is not this verb's contract: a caller's object can carry any `type`. */
1237
+ var known = null;
1238
+ if (!a.name && typeof a.type === "string") {
1239
+ try { known = oid.name(a.type); }
1240
+ // allow:swallow-unverified drop-silent display fallback: an unreadable type prints as itself
1241
+ catch (_notAnIdentifier) { known = null; }
1242
+ }
1243
+ L.push(" " + (a.name || known || String(a.type)) + ": " +
1244
+ _list(a.values).length + " value(s) (not rendered)");
1245
+ });
1246
+ } else {
1247
+ L.push(" (none)");
1248
+ }
1249
+ return L.join("\n") + "\n";
1250
+ }
1251
+
1252
+ /**
1253
+ * @primitive pki.inspect.pkcs12
1254
+ * @signature pki.inspect.pkcs12(input) -> string
1255
+ * @since 0.8.29
1256
+ * @status stable
1257
+ * @spec RFC 7292
1258
+ * @related pki.schema.pkcs12.parse, pki.pkcs12.open
1259
+ *
1260
+ * Render a PKCS#12 store's outer structure as a text report: the version, the
1261
+ * integrity mode, the MAC algorithm and its iteration count, and one line per safe
1262
+ * bag giving its type. No private key is rendered, and an encrypted safe's contents
1263
+ * are not decrypted, because this verb takes no password: the report names
1264
+ * `pki.pkcs12.open` as what reads the contents. `input` is a PEM string, a DER
1265
+ * Buffer, or a `pki.schema.pkcs12.parse` result; a non-PKCS#12 throws
1266
+ * `inspect/bad-pkcs12`, a wrong-type input `inspect/bad-input`.
1267
+ *
1268
+ * @example
1269
+ * var pair = await pki.key.generate("Ed25519");
1270
+ * var key = await pki.key.export(pair.privateKey);
1271
+ * var cert = await pki.x509.sign({ subject: "p12.example", subjectPublicKey: await pki.key.export(pair.publicKey),
1272
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1273
+ * var store = await pki.pkcs12.build({ safeContents: [{ bags: [{ type: "cert", cert: cert }] }] }, { password: "1234" });
1274
+ * pki.inspect.pkcs12(store).split("\n")[0]; // "PKCS#12 Store:"
1275
+ */
1276
+ /** @internal A salt's LENGTH is what says anything about the protection; its value says nothing a reader acts
1277
+ * on. And `pki.pkcs12.build` takes a caller-supplied salt, so a store can carry the private key bytes there
1278
+ * and a report printing the value would write the key out. Length only, for the same reason the key report
1279
+ * renders no bytes at all. */
1280
+ function _saltLine(L, salt) {
1281
+ if (Buffer.isBuffer(salt)) L.push(" Salt: " + salt.length + " bytes (not rendered)");
1282
+ }
1283
+
1284
+ /** @internal Whether any signer of a signed authSafe has NO certificate embedded beside it. A store can embed
1285
+ * a chain certificate while leaving the signer's own out, so counting the certificates answered the wrong
1286
+ * question: verification needs the certificate that MATCHES the signer. A SignerInfo names itself by issuer
1287
+ * and serial or by subject key identifier, and an embedded certificate arrives raw, so each is parsed to
1288
+ * compare. A certificate this parser refuses simply matches nothing. */
1289
+ function _someSignerUnmatched(signed) {
1290
+ var signers = _list(signed.signerInfos);
1291
+ if (!signers.length) return false;
1292
+ var parsed = [];
1293
+ _list(signed.certificates).forEach(function (raw) {
1294
+ var bytes = Buffer.isBuffer(raw) ? raw : _obj(raw).bytes;
1295
+ if (!Buffer.isBuffer(bytes)) return;
1296
+ // allow:swallow-unverified drop-silent display fallback: a certificate this parser refuses matches nothing
1297
+ try { parsed.push(x509.parse(bytes)); } catch (_unreadableCert) { return; }
1298
+ });
1299
+ return signers.some(function (rawSigner) {
1300
+ var sid = _obj(_obj(rawSigner).sid);
1301
+ return !parsed.some(function (c) { return _certMatchesSid(c, sid); });
1302
+ });
1303
+ }
1304
+ function _certMatchesSid(cert, sid) {
1305
+ if (Buffer.isBuffer(sid.subjectKeyIdentifier)) {
1306
+ var ski = _list(cert.extensions).filter(function (e) { return _obj(e).oid === OID_SUBJECT_KEY_ID; })[0];
1307
+ if (!ski) return false;
1308
+ // allow:swallow-unverified drop-silent display fallback: an unreadable key id matches nothing
1309
+ try { return asn1.read.octetString(asn1.decode(_obj(ski).value)).equals(sid.subjectKeyIdentifier); }
1310
+ catch (_unreadableSki) { return false; }
1311
+ }
1312
+ if (!sid.issuer || sid.serialNumberHex === undefined) return false;
1313
+ if (cert.serialNumberHex !== sid.serialNumberHex) return false;
1314
+ /** @internal The RFC 5280 sec. 7.1 canonical comparison, through the shared home, because two encodings of
1315
+ * one name are one name: compared as bytes, a certificate whose issuer DN is encoded differently from the
1316
+ * SignerInfo's matched nothing and the report asked for a certificate the store already had. */
1317
+ var a = _obj(cert.issuer).rdns, b2 = _obj(sid.issuer).rdns;
1318
+ if (Array.isArray(a) && Array.isArray(b2)) {
1319
+ // allow:swallow-unverified drop-silent display fallback: a name the comparison refuses falls back to bytes
1320
+ try { return guard.name.dnEqual(a, b2, _err, "inspect/bad-input", "the signer name") === true; }
1321
+ catch (_notComparable) { return false; }
1322
+ }
1323
+ var issuerBytes = _obj(sid.issuer).bytes;
1324
+ return Buffer.isBuffer(issuerBytes) && Buffer.isBuffer(_obj(cert.issuer).bytes) &&
1325
+ _obj(cert.issuer).bytes.equals(issuerBytes);
1326
+ }
1327
+
1328
+ function pkcs12Report(input) {
1329
+ var p = _parsePkcs12(input);
1330
+ var L = ["PKCS#12 Store:", " Version: " + p.version];
1331
+ L.push(" Integrity Mode: " + p.integrityMode);
1332
+ if (p.mac) {
1333
+ L.push(" MAC:");
1334
+ L.push(" Kind: " + p.mac.kind);
1335
+ /** @internal RFC 9579's PBMAC1 carries the parameters that actually protect the store inside its own
1336
+ * structure, and the outer MacData fields then describe nothing: read from there, a store with 2048
1337
+ * PBKDF2 iterations reported one iteration, an empty salt, and "pbmac1" as its digest. */
1338
+ if (p.mac.pbmac1) {
1339
+ var kdf = p.mac.pbmac1.kdf || {};
1340
+ _optLine(L, " ", "Digest", p.mac.pbmac1.schemeName || p.mac.pbmac1.schemeOid);
1341
+ _optLine(L, " ", "Key Derivation", kdf.prfName || kdf.prfOid);
1342
+ _optLine(L, " ", "Iterations", kdf.iterationCount);
1343
+ _optLine(L, " ", "Key Length", kdf.keyLength);
1344
+ _saltLine(L, kdf.salt);
1345
+ } else {
1346
+ _optLine(L, " ", "Digest", p.mac.hashName || p.mac.hashOid);
1347
+ _optLine(L, " ", "Iterations", p.mac.iterations);
1348
+ _saltLine(L, p.mac.macSalt);
1349
+ }
1350
+ } else {
1351
+ L.push(" MAC: (none)");
1352
+ }
1353
+ L.push(" Safe Bags: " + _list(p.safeBags).length);
1354
+ /** @internal RFC 7292 sec. 4.2.6's safeContentsBag holds further bags, and reporting only the container
1355
+ * made a container of keys and certificates read the same as an empty one. The no-key-material rule
1356
+ * carries into the nested level, because a key inside a container is still a key. */
1357
+ (function walkBags(bags, pad) {
1358
+ _list(bags).forEach(function (rawBag, i) {
1359
+ var bag = _obj(rawBag);
1360
+ /** @internal The PARSER's vocabulary, which is the ASN.1 bag-type name, not the builder's shorthand:
1361
+ * keyed on `shroudedKey` and `key` the marker never once fired, because a parsed bag reports itself as
1362
+ * `pkcs8ShroudedKeyBag` or `keyBag`. */
1363
+ var isKey = bag.type === "pkcs8ShroudedKeyBag" || bag.type === "keyBag";
1364
+ L.push(pad + "[" + i + "] " + bag.type + (isKey ? " (private key, not rendered)" : "") +
1365
+ (Array.isArray(bag.nested) ? " (" + bag.nested.length + " nested)" : ""));
1366
+ if (Array.isArray(bag.nested) && bag.nested.length) walkBags(bag.nested, pad + " ");
1367
+ });
1368
+ })(p.safeBags, " ");
1369
+ L.push(" Encrypted Safes: " + _list(p.encryptedSafes).length);
1370
+ var needsRecipientKey = false, needsSafePassword = false;
1371
+ _list(p.encryptedSafes).forEach(function (rawSafe, i) {
1372
+ var safe = _obj(rawSafe);
1373
+ if (safe.type === "envelopedData") needsRecipientKey = true;
1374
+ else needsSafePassword = true;
1375
+ /** @internal An encrypted safe is a CMS EncryptedData, so the algorithm that protects it sits on that
1376
+ * structure's encryptedContentInfo rather than on the safe. Read off the safe it was absent and every
1377
+ * encrypted store reported its algorithm as unknown. */
1378
+ var eci = _obj(safe.content).encryptedContentInfo;
1379
+ /** @internal The safe's TYPE says which credential opens it, so the row names it: RFC 7292 sec. 4.1's
1380
+ * password privacy is an EncryptedData and its public-key privacy an EnvelopedData, and the two are
1381
+ * opened by different things. */
1382
+ L.push(" [" + i + "] " + (safe.type ? safe.type + ": " : "") +
1383
+ _algName(_obj(eci).contentEncryptionAlgorithm) + " (not decrypted)");
1384
+ });
1385
+ /** @internal WHICH credential opens the contents, because naming the wrong one sends an operator to look
1386
+ * for something the store does not want: RFC 7292 sec. 4.1's public-key privacy is opened with a recipient
1387
+ * key and `pki.pkcs12.open` refuses it with pkcs12/no-recipient-key, not with a password prompt. */
1388
+ var needs = [];
1389
+ /** @internal Derived from what the store IS, not from whether it has encrypted safes: a public-key-integrity
1390
+ * store of plaintext bags is opened by its signature, a MAC-less one needs opts.allowUnauthenticated, and a
1391
+ * password is wanted for the MAC, for a password-encrypted safe, or for a shrouded key bag. Read off an
1392
+ * empty encryptedSafes list, the report asked for a password from stores that do not want one. */
1393
+ var hasShroudedKey = (function anyShrouded(bags) {
1394
+ return _list(bags).some(function (raw) {
1395
+ var bag = _obj(raw);
1396
+ return bag.type === "pkcs8ShroudedKeyBag" || anyShrouded(bag.nested);
1397
+ });
1398
+ })(p.safeBags);
1399
+ if (p.integrityMode === "password" || needsSafePassword || hasShroudedKey) needs.push("the password");
1400
+ if (needsRecipientKey) needs.push("opts.recipientKey for the enveloped safe");
1401
+ /** @internal A store can want a credential AND be unauthenticated, and `pki.pkcs12.open` checks both: a
1402
+ * MAC-less store holding a shrouded key needs the password for the key and the opt for the missing MAC.
1403
+ * Offered as an alternative to the others, the opt went unmentioned for exactly those stores. */
1404
+ if (p.integrityMode === "none") needs.push("opts.allowUnauthenticated, since the store carries no MAC");
1405
+ /** @internal A public-key-integrity store is verified against the signer's certificate, and its SignedData
1406
+ * may carry none: then the certificate has to be supplied, and a report naming only the privacy credentials
1407
+ * left out an input without which opening fails. */
1408
+ if (p.integrityMode === "public-key" && _someSignerUnmatched(_obj(p.authSafeSigned))) {
1409
+ needs.push("opts.signerCerts, since the signed authSafe carries no certificate matching its signer");
1410
+ }
1411
+ if (needs.length) {
1412
+ L.push(" The bag contents are read with pki.pkcs12.open, which needs " + _joinAnd(needs) + ".");
1413
+ } else {
1414
+ L.push(" The bag contents are read with pki.pkcs12.open. This store's integrity is its signature and its bags are not encrypted, so it needs no password.");
1415
+ }
1416
+ return L.join("\n") + "\n";
1417
+ }
1418
+
1419
+ /**
1420
+ * @primitive pki.inspect.crmf
1421
+ * @signature pki.inspect.crmf(input) -> string
1422
+ * @since 0.8.29
1423
+ * @status stable
1424
+ * @spec RFC 4211
1425
+ * @related pki.schema.crmf.parse, pki.inspect.csr
1426
+ *
1427
+ * Render CRMF certificate request messages as a text report: one block per message
1428
+ * giving its request id, the certificate template's subject, issuer, validity and
1429
+ * public key, the requested extensions, and whether a proof of possession and
1430
+ * registration controls are present. `input` is a PEM string, a DER Buffer, or a
1431
+ * `pki.schema.crmf.parse` result; a non-CRMF throws `inspect/bad-crmf`, a
1432
+ * wrong-type input `inspect/bad-input`.
1433
+ *
1434
+ * @example
1435
+ * var pair = await pki.key.generate("Ed25519");
1436
+ * var der = await pki.crmf.build({ certReqId: 0, certTemplate: { subject: "crmf.example",
1437
+ * publicKey: await pki.key.export(pair.publicKey) } }, { key: await pki.key.export(pair.privateKey) });
1438
+ * pki.inspect.crmf(der).split("\n")[0]; // "CRMF Certificate Request Messages:"
1439
+ */
1440
+ function crmfReport(input) {
1441
+ var m = _parseCrmf(input);
1442
+ var L = ["CRMF Certificate Request Messages:", " Messages: " + m.messages.length];
1443
+ m.messages.forEach(function (msg, i) {
1444
+ var req = msg.certReq, t = req.certTemplate;
1445
+ L.push(" [" + i + "] Certificate Request:");
1446
+ _optLine(L, " ", "Request ID", req.certReqId === undefined ? null : String(req.certReqId));
1447
+ /** @internal RFC 4211 sec. 5 carries the DER value, so 2 names v3, exactly as a certificate's does. The
1448
+ * raw value read as a request for v2, which is the opposite of what the template asks for, and the
1449
+ * certificate report beside it writes the same number the other way. */
1450
+ _optLine(L, " ", "Version", t.version === undefined || t.version === null
1451
+ ? null : (Number(t.version) + 1) + " (0x" + Number(t.version).toString(16) + ")");
1452
+ if (t.serialNumber !== undefined && t.serialNumber !== null) L.push(" Serial Number: " + t.serialNumber);
1453
+ if (t.issuer) L.push(" Issuer: " + _nameOrGn(t.issuer));
1454
+ if (t.subject) L.push(" Subject: " + _nameOrGn(t.subject));
1455
+ if (t.signingAlg) L.push(" Signing Algorithm: " + _algName(t.signingAlg));
1456
+ if (t.validity) {
1457
+ L.push(" Validity:");
1458
+ _optLine(L, " ", "Not Before", t.validity.notBefore ? _date(t.validity.notBefore) : null);
1459
+ _optLine(L, " ", "Not After", t.validity.notAfter ? _date(t.validity.notAfter) : null);
1460
+ }
1461
+ if (t.publicKey) { L.push(" Subject Public Key Info:"); L.push(_keyBlock(t.publicKey, " ")); }
1462
+ if (_list(t.extensions).length) {
1463
+ L.push(" Requested Extensions:");
1464
+ _list(t.extensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1465
+ }
1466
+ /** @internal WHICH proof of possession a request carries is the difference between a key whose holder
1467
+ * proved possession by signing and one an RA vouched for, so the form is named rather than reported as
1468
+ * merely present. */
1469
+ L.push(" Proof of Possession: " + (msg.popo ? (msg.popo.type || "present") : "(none)"));
1470
+ L.push(" Registration Controls: " + ((req.controls && req.controls.length) || 0));
1471
+ L.push(" Registration Info: " + ((msg.regInfo && msg.regInfo.length) || 0));
1472
+ });
1473
+ return L.join("\n") + "\n";
1474
+ }
1475
+
1476
+ /**
1477
+ * @primitive pki.inspect.cmp
1478
+ * @signature pki.inspect.cmp(input) -> string
1479
+ * @since 0.8.29
1480
+ * @status stable
1481
+ * @spec RFC 9810
1482
+ * @related pki.schema.cmp.parse, pki.inspect.crmf
1483
+ *
1484
+ * Render a CMP message as a text report: the header's protocol version, sender and
1485
+ * recipient, transaction id and nonces, the body arm the message carries, and
1486
+ * whether protection and extra certificates are present. The body's inner content
1487
+ * is named rather than expanded, since each arm is its own structure that its own
1488
+ * verb reads. `input` is a PEM string, a DER Buffer, or a `pki.schema.cmp.parse`
1489
+ * result; a non-CMP throws `inspect/bad-cmp`, a wrong-type input
1490
+ * `inspect/bad-input`.
1491
+ *
1492
+ * @example
1493
+ * var pair = await pki.key.generate("Ed25519");
1494
+ * var key = await pki.key.export(pair.privateKey);
1495
+ * var cert = await pki.x509.sign({ subject: "cmp.example", subjectPublicKey: await pki.key.export(pair.publicKey),
1496
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1497
+ * var der = await pki.cmp.build({ header: { sender: { directoryName: "CN=cmp.example" },
1498
+ * recipient: { directoryName: "CN=A CA" } },
1499
+ * body: { ir: { certTemplate: { subject: [{ commonName: "cmp.example" }], publicKey: await pki.key.export(pair.publicKey) } } } },
1500
+ * { cert: cert, key: key });
1501
+ * pki.inspect.cmp(der).split("\n")[0]; // "CMP Message:"
1502
+ */
1503
+ function cmpReport(input) {
1504
+ var m = _parseCmp(input);
1505
+ var h = m.header, body = m.body;
1506
+ var L = ["CMP Message:", " Header:"];
1507
+ _optLine(L, " ", "Protocol Version", h.pvno);
1508
+ L.push(" Sender: " + _nameOrGn(h.sender));
1509
+ L.push(" Recipient: " + _nameOrGn(h.recipient));
1510
+ if (h.messageTime) L.push(" Message Time: " + _date(h.messageTime));
1511
+ if (h.protectionAlg) L.push(" Protection Algorithm: " + _algName(h.protectionAlg));
1512
+ if (Buffer.isBuffer(h.transactionID)) L.push(" Transaction ID: " + _hexColon(h.transactionID, {}));
1513
+ if (Buffer.isBuffer(h.senderNonce)) L.push(" Sender Nonce: " + _hexColon(h.senderNonce, {}));
1514
+ if (Buffer.isBuffer(h.recipNonce)) L.push(" Recipient Nonce: " + _hexColon(h.recipNonce, {}));
1515
+ if (Buffer.isBuffer(h.senderKID)) L.push(" Sender Key ID: " + _hexColon(h.senderKID, {}));
1516
+ L.push(" General Info: " + ((h.generalInfo && h.generalInfo.length) || 0));
1517
+ L.push(" Body:");
1518
+ L.push(" Arm: " + body.arm);
1519
+ /** @internal The error arm is expanded where the others are only named, because its CONTENT is the whole
1520
+ * diagnostic and no other verb renders it: a reader of an error message wants the status, the failure bits
1521
+ * and the code, and naming the arm alone told them only that something went wrong. */
1522
+ if (body.arm === "error" && body.decoded && typeof body.decoded === "object") {
1523
+ var errText = _decodedRecord(body.decoded, " ");
1524
+ if (errText) L.push(errText);
1525
+ }
1526
+ L.push(" Protection: " + (m.protection ? "present" : "(none)"));
1527
+ L.push(" Extra Certificates: " + ((m.extraCerts && m.extraCerts.length) || 0));
1528
+ return L.join("\n") + "\n";
1529
+ }
1530
+
1531
+ /**
1532
+ * @primitive pki.inspect.csrattrs
1533
+ * @signature pki.inspect.csrattrs(input) -> string
1534
+ * @since 0.8.29
1535
+ * @status stable
1536
+ * @spec RFC 7030, RFC 8951
1537
+ * @related pki.schema.csrattrs.parse, pki.inspect.csr
1538
+ *
1539
+ * Render an EST CSR Attributes response as a text report: one line per item, each
1540
+ * either a bare object identifier the server asks the client to supply or an
1541
+ * attribute with its type and values. `input` is a PEM string, a DER Buffer, or a
1542
+ * `pki.schema.csrattrs.parse` result; a non-CsrAttrs throws
1543
+ * `inspect/bad-csrattrs`, a wrong-type input `inspect/bad-input`.
1544
+ *
1545
+ * @example
1546
+ * var b = pki.asn1.build;
1547
+ * var der = b.sequence([b.oid("1.2.840.113549.1.9.7")]);
1548
+ * pki.inspect.csrattrs(der).split("\n")[0]; // "EST CSR Attributes:"
1549
+ */
1550
+ function csrattrsReport(input) {
1551
+ var a = _parseCsrattrs(input);
1552
+ var L = ["EST CSR Attributes:", " Items: " + a.items.length];
1553
+ a.items.forEach(function (item, i) {
1554
+ var label = item.kind === "oid" ? "OID" : "Attribute";
1555
+ var name = item.name || item.oid || "unknown";
1556
+ L.push(" [" + i + "] " + label + ": " + name + (item.name && item.oid ? " (" + item.oid + ")" : ""));
1557
+ /** @internal The CONSTRAINT a client has to act on is what the parser added beside the raw value: a key
1558
+ * size, a curve set, a template. Printed as its DER, a request for a 2048-bit key read as four bytes. */
1559
+ var constraint = _extraFields(item, _ROW_PLUMBING, " ");
1560
+ if (constraint) L.push(constraint);
1561
+ if (item.kind !== "oid" && _list(item.values).length) {
1562
+ L.push(" Values: " + _list(item.values).length);
1563
+ _list(item.values).forEach(function (v) {
1564
+ /** @internal An attribute value a server chose is escaped for the reason a distinguished name is. */
1565
+ L.push(" " + (Buffer.isBuffer(v) ? _hexColon(v, {}) : _clean(String(v))));
1566
+ });
1567
+ }
1568
+ });
1569
+ return L.join("\n") + "\n";
1570
+ }
1571
+
1572
+ /**
1573
+ * @primitive pki.inspect.trustanchor
1574
+ * @signature pki.inspect.trustanchor(input) -> string
1575
+ * @since 0.8.29
1576
+ * @status stable
1577
+ * @spec RFC 5914
1578
+ * @related pki.schema.trustanchor.parse, pki.inspect.certificate
1579
+ *
1580
+ * Render an RFC 5914 trust anchor list as a text report: one block per anchor
1581
+ * giving which of the three forms it takes, its public key, key identifier and
1582
+ * title where present, and the certificate path controls that bound what it may
1583
+ * anchor. `input` is a PEM string, a DER Buffer, or a
1584
+ * `pki.schema.trustanchor.parse` result; a non-list throws
1585
+ * `inspect/bad-trustanchor`, a wrong-type input `inspect/bad-input`.
1586
+ *
1587
+ * @example
1588
+ * var pair = await pki.key.generate("Ed25519");
1589
+ * var b = pki.asn1.build;
1590
+ * var spki = await pki.key.export(pair.publicKey);
1591
+ * var der = b.sequence([b.explicit(2, b.sequence([b.raw(spki), b.octetString(Buffer.alloc(20, 1))]))]);
1592
+ * pki.inspect.trustanchor(der).split("\n")[0]; // "Trust Anchor List:"
1593
+ */
1594
+ function trustAnchorReport(input) {
1595
+ var t = _parseTrustAnchor(input);
1596
+ var L = ["Trust Anchor List:", " Anchors: " + t.anchors.length];
1597
+ t.anchors.forEach(function (a, i) {
1598
+ L.push(" [" + i + "] Form: " + a.kind);
1599
+ /** @internal RFC 5914 sec. 3 gives three forms, and only the TrustAnchorInfo one carries its key and
1600
+ * name at the top: the other two hold a certificate, whose subject and subjectPublicKeyInfo are what
1601
+ * name the anchor. Read as though every form were TrustAnchorInfo, a certificate anchor rendered its
1602
+ * form and nothing else, so two different anchors produced the same report. */
1603
+ var held = a.kind === "certificate" ? a.certificate : (a.kind === "tbsCert" ? a.tbsCert : null);
1604
+ if (held) {
1605
+ if (held.subject) L.push(" Subject: " + _dnString(held.subject));
1606
+ if (held.issuer) L.push(" Issuer: " + _dnString(held.issuer));
1607
+ if (held.serialNumberHex) _serialInto(L, " ", held.serialNumberHex);
1608
+ if (held.subjectPublicKeyInfo) {
1609
+ L.push(" Subject Public Key Info:");
1610
+ L.push(_keyBlock(held.subjectPublicKeyInfo, " "));
1611
+ }
1612
+ /** @internal Rendered, not counted, and for the same reason the TrustAnchorInfo arm renders its own:
1613
+ * the extensions bound what trusting the anchor means, so a count made a path-limited anchor read the
1614
+ * same as an unlimited one. One rule, both arms. */
1615
+ if (_list(held.extensions).length) {
1616
+ L.push(" Extensions:");
1617
+ _list(held.extensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1618
+ }
1619
+ return;
1620
+ }
1621
+ var info = a.taInfo;
1622
+ if (info.pubKey) { L.push(" Subject Public Key Info:"); L.push(_keyBlock(info.pubKey, " ")); }
1623
+ if (Buffer.isBuffer(info.keyId)) L.push(" Key Identifier: " + _hexColon(info.keyId, {}));
1624
+ /** @internal A taTitle is text whoever wrote the anchor list chose, so it is escaped as the rest are. */
1625
+ _optLine(L, " ", "Title", info.taTitle == null ? null : _clean(String(info.taTitle)));
1626
+ /** @internal RFC 5914 sec. 3's exts carry certificate extensions that bound what trusting the anchor
1627
+ * means, a basicConstraints path length among them. Skipped, an anchor carrying one read the same as one
1628
+ * carrying none. */
1629
+ if (_list(info.exts).length) {
1630
+ L.push(" Extensions:");
1631
+ _list(info.exts).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1632
+ }
1633
+ /** @internal RFC 5914 sec. 3 excludes some extension types from `exts`, and the parser drops one it finds
1634
+ * there while recording its identifier. Unmentioned, an anchor that tried to carry a name constraint in
1635
+ * that field read the same as one that carried nothing, and the constraint it named is not in force. */
1636
+ if (_list(info.excludedExtensions).length) {
1637
+ L.push(" Extensions the profile excludes here, dropped and not in force:");
1638
+ _list(info.excludedExtensions).forEach(function (o) {
1639
+ L.push(" " + (oid.name(String(o)) || String(o)) + " (" + String(o) + ")");
1640
+ });
1641
+ }
1642
+ var cpc = info.certPath;
1643
+ if (cpc) {
1644
+ L.push(" Certificate Path Controls:");
1645
+ if (cpc.taName) L.push(" Name: " + _nameOrGn(cpc.taName));
1646
+ _optLine(L, " ", "Path Length", cpc.pathLenConstraint);
1647
+ /** @internal The policy IDENTIFIERS, because a count made two anchors restricted to different single
1648
+ * policies read the same, and which policies an anchor is restricted to is what a reader asks. */
1649
+ if (_list(cpc.policySet).length) {
1650
+ L.push(" Policies:");
1651
+ _list(cpc.policySet).forEach(function (pol) {
1652
+ var p = _obj(pol);
1653
+ L.push(" " + (p.name ? p.name + " (" + p.policyIdentifier + ")" : String(p.policyIdentifier)));
1654
+ });
1655
+ }
1656
+ /** @internal RFC 5914 sec. 3's policyFlags is a BIT STRING the parser decodes to a record of named
1657
+ * booleans, so it has no length: read as an array the line never ran and a trust anchor's active
1658
+ * policy restrictions were absent from the report rather than shown as off. */
1659
+ if (cpc.policyFlags) {
1660
+ var onFlags = Object.keys(cpc.policyFlags).filter(function (n) { return cpc.policyFlags[n] === true; });
1661
+ L.push(" Policy Flags: " + (onFlags.length ? onFlags.join(", ") : "(none set)"));
1662
+ }
1663
+ /** @internal The subtrees an anchor may certify within are what a reader inspects an anchor to learn,
1664
+ * and omitted they made a restricted anchor read identically to an unrestricted one. The shape is the
1665
+ * certificate extension's own, so the extension renderer formats it rather than a second copy. */
1666
+ if (cpc.nameConstr) {
1667
+ var ncText = EXT_RENDERERS.nameConstraints(cpc.nameConstr, " ");
1668
+ if (ncText) { L.push(" Name Constraints:"); L.push(ncText); }
1669
+ }
1670
+ }
1671
+ });
1672
+ return L.join("\n") + "\n";
1673
+ }
1674
+
1675
+ /**
1676
+ * @primitive pki.inspect.ocspRequest
1677
+ * @signature pki.inspect.ocspRequest(input) -> string
1678
+ * @since 0.8.29
1679
+ * @status stable
1680
+ * @spec RFC 6960
1681
+ * @related pki.schema.ocsp.parseRequest, pki.inspect.ocspResponse
1682
+ *
1683
+ * Render an OCSP request as a text report: the version, the requestor name where
1684
+ * one is given, one block per requested certificate giving the CertID's hash
1685
+ * algorithm and the issuer name and key hashes with the serial asked about, the
1686
+ * request extensions, and whether the request is signed. `input` is a PEM string, a
1687
+ * DER Buffer, or a `pki.schema.ocsp.parseRequest` result; a non-request throws
1688
+ * `inspect/bad-ocsp-request`, a wrong-type input `inspect/bad-input`.
1689
+ *
1690
+ * @example
1691
+ * var pair = await pki.key.generate("Ed25519");
1692
+ * var key = await pki.key.export(pair.privateKey);
1693
+ * var cert = await pki.x509.sign({ subject: "ocsp.example", subjectPublicKey: await pki.key.export(pair.publicKey),
1694
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1695
+ * var der = await pki.ocsp.buildRequest({ cert: cert, issuer: cert });
1696
+ * pki.inspect.ocspRequest(der).split("\n")[0]; // "OCSP Request:"
1697
+ */
1698
+ function ocspRequestReport(input) {
1699
+ var r = _parseOcspReq(input);
1700
+ var L = ["OCSP Request:", " Version: " + r.version];
1701
+ L.push(" Requestor Name: " + (r.requestorName ? _nameOrGn(r.requestorName) : "(none)"));
1702
+ L.push(" Requests: " + r.requestList.length);
1703
+ r.requestList.forEach(function (req, i) {
1704
+ var id = req.certID;
1705
+ L.push(" [" + i + "] Certificate ID:");
1706
+ L.push(" Hash Algorithm: " + _algName(id.hashAlgorithm));
1707
+ if (Buffer.isBuffer(id.issuerNameHash)) L.push(" Issuer Name Hash: " + _hexColon(id.issuerNameHash, {}));
1708
+ if (Buffer.isBuffer(id.issuerKeyHash)) L.push(" Issuer Key Hash: " + _hexColon(id.issuerKeyHash, {}));
1709
+ _serialInto(L, " ", id.serialNumberHex || id.serialNumber);
1710
+ if (_list(req.singleRequestExtensions).length) {
1711
+ L.push(" Single Request Extensions:");
1712
+ _list(req.singleRequestExtensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1713
+ }
1714
+ });
1715
+ if (_list(r.requestExtensions).length) {
1716
+ L.push(" Request Extensions:");
1717
+ _list(r.requestExtensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1718
+ } else {
1719
+ L.push(" Request Extensions: (none)");
1720
+ }
1721
+ L.push(" Signature: " + (r.optionalSignature ? "present" : "(unsigned)"));
1722
+ return L.join("\n") + "\n";
1723
+ }
1724
+
1725
+ /**
1726
+ * @primitive pki.inspect.ocspResponse
1727
+ * @signature pki.inspect.ocspResponse(input) -> string
1728
+ * @since 0.8.29
1729
+ * @status stable
1730
+ * @spec RFC 6960
1731
+ * @related pki.schema.ocsp.parseResponse, pki.inspect.ocspRequest
1732
+ *
1733
+ * Render an OCSP response as a text report: the response status, and for a basic
1734
+ * response the responder id, the time it was produced, one block per answer giving
1735
+ * the CertID, the certificate's status with its revocation time and reason where it
1736
+ * is revoked, and the validity window of the answer, plus the response extensions
1737
+ * and the certificates carried for the relying party to chain. `input` is a PEM
1738
+ * string, a DER Buffer, or a `pki.schema.ocsp.parseResponse` result; a non-response
1739
+ * throws `inspect/bad-ocsp-response`, a wrong-type input `inspect/bad-input`.
1740
+ *
1741
+ * @example
1742
+ * var pair = await pki.key.generate("Ed25519");
1743
+ * var key = await pki.key.export(pair.privateKey);
1744
+ * var cert = await pki.x509.sign({ subject: "ocsp.example", subjectPublicKey: await pki.key.export(pair.publicKey),
1745
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1746
+ * var der = await pki.ocsp.sign({ responderID: "byName",
1747
+ * responses: [{ cert: cert, issuer: cert, status: "good", thisUpdate: new Date("2026-01-02T00:00:00Z") }] },
1748
+ * { cert: cert, key: key });
1749
+ * pki.inspect.ocspResponse(der).split("\n")[0]; // "OCSP Response:"
1750
+ */
1751
+ function ocspResponseReport(input) {
1752
+ var r = _parseOcspResp(input);
1753
+ var st = r.responseStatus;
1754
+ var L = ["OCSP Response:", " Response Status: " + st.name];
1755
+ if (r.responseBytes) L.push(" Response Type: " + (r.responseBytes.responseTypeName || r.responseBytes.responseType));
1756
+ var br = r.basicResponse;
1757
+ if (!br) { L.push(" (no basic response to render)"); return L.join("\n") + "\n"; }
1758
+ L.push(" Version: " + br.version);
1759
+ /** @internal RFC 6960 sec. 4.2.1 gives ResponderID two forms, and the key form is a hash rather than a
1760
+ * name: passed to the name renderer it printed neither, so a responder that identifies itself by key was
1761
+ * not identified in the report at all. */
1762
+ L.push(" Responder ID: " + (Buffer.isBuffer(br.responderID.byKey)
1763
+ ? "keyHash:" + _hexColon(br.responderID.byKey, {})
1764
+ : _nameOrGn(br.responderID.byName)));
1765
+ if (br.producedAt) L.push(" Produced At: " + _date(br.producedAt));
1766
+ L.push(" Responses: " + br.responses.length);
1767
+ br.responses.forEach(function (one, i) {
1768
+ var id = one.certID;
1769
+ L.push(" [" + i + "] Certificate ID:");
1770
+ L.push(" Hash Algorithm: " + _algName(id.hashAlgorithm));
1771
+ if (Buffer.isBuffer(id.issuerNameHash)) L.push(" Issuer Name Hash: " + _hexColon(id.issuerNameHash, {}));
1772
+ if (Buffer.isBuffer(id.issuerKeyHash)) L.push(" Issuer Key Hash: " + _hexColon(id.issuerKeyHash, {}));
1773
+ _serialInto(L, " ", id.serialNumberHex || id.serialNumber);
1774
+ var status = one.certStatus;
1775
+ L.push(" Cert Status: " + status.type);
1776
+ if (status.revocationTime) L.push(" Revocation Time: " + _date(status.revocationTime));
1777
+ _optLine(L, " ", "Revocation Reason", status.revocationReason);
1778
+ if (one.thisUpdate) L.push(" This Update: " + _date(one.thisUpdate));
1779
+ L.push(" Next Update: " + (one.nextUpdate ? _date(one.nextUpdate) : "NONE"));
1780
+ if (_list(one.singleExtensions).length) {
1781
+ L.push(" Single Extensions:");
1782
+ _list(one.singleExtensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1783
+ }
1784
+ });
1785
+ if (_list(br.responseExtensions).length) {
1786
+ L.push(" Response Extensions:");
1787
+ _list(br.responseExtensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1788
+ }
1789
+ if (br.signatureAlgorithm) L.push(" Signature Algorithm: " + _algName(br.signatureAlgorithm));
1790
+ L.push(" Certificates: " + ((br.certs && br.certs.length) || 0));
1791
+ return L.join("\n") + "\n";
1792
+ }
1793
+
1794
+ /**
1795
+ * @primitive pki.inspect.tsp
1796
+ * @signature pki.inspect.tsp(input) -> string
1797
+ * @since 0.8.29
1798
+ * @status stable
1799
+ * @spec RFC 3161
1800
+ * @related pki.schema.tsp.parseResponse, pki.inspect.cms
1801
+ *
1802
+ * Render an RFC 3161 timestamp response as a text report: the PKI status with its
1803
+ * status strings and failure info, and the TSTInfo inside the token, giving the
1804
+ * policy, the message imprint, the serial, the time it was stamped with its
1805
+ * accuracy and ordering, the nonce, and the TSA name. The token's own CMS structure,
1806
+ * its signer identifier and its signed attributes, is what `pki.inspect.cms`
1807
+ * renders, and a bare timestamp TOKEN is a CMS ContentInfo rather than a response,
1808
+ * so that verb reads one of those directly. `input` is a PEM string, a DER Buffer, or a
1809
+ * `pki.schema.tsp.parseResponse` result; a non-response throws `inspect/bad-tsp`,
1810
+ * a wrong-type input `inspect/bad-input`.
1811
+ *
1812
+ * @example
1813
+ * var pair = await pki.key.generate("Ed25519");
1814
+ * var key = await pki.key.export(pair.privateKey), spki = await pki.key.export(pair.publicKey);
1815
+ * var ca = await pki.x509.sign({ subject: "tsa.example", subjectPublicKey: spki,
1816
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
1817
+ * extensions: { keyUsage: ["digitalSignature"], extendedKeyUsage: ["timeStamping"], extendedKeyUsageCritical: true } },
1818
+ * { key: key });
1819
+ * var imprint = { hashAlgorithm: "sha256", hashedMessage: Buffer.alloc(32, 7) };
1820
+ * var token = await pki.tsp.sign(imprint, { cert: ca, key: key }, { policy: "1.2.3", serialNumber: 1 });
1821
+ * pki.inspect.tsp(pki.tsp.response(token, {})).split("\n")[0]; // "Timestamp Response:"
1822
+ */
1823
+ function tspReport(input) {
1824
+ var r = _parseTsp(input);
1825
+ /** @internal RFC 3161 sec. 2.4.2's PKIStatus values need different handling from each other, and a bare
1826
+ * integer said which only to a reader who knows the table: a rejection and a response still being waited
1827
+ * on both read as a number. */
1828
+ var statusName = TSP_STATUS_NAMES[r.status];
1829
+ var L = ["Timestamp Response:",
1830
+ " Status: " + r.status + (statusName ? " (" + statusName + ")" : "")];
1831
+ /** @internal A statusString is text a responder chose, so it reaches the report through the same escape
1832
+ * every other caller-controlled string does: copied raw, a newline forges a report line and a terminal
1833
+ * escape rewrites the screen around it. */
1834
+ if (r.statusString) {
1835
+ var strings = Array.isArray(r.statusString) ? r.statusString : [r.statusString];
1836
+ L.push(" Status String: " + strings.map(function (s) { return _clean(String(s)); }).join("; "));
1837
+ }
1838
+ /** @internal PKIFailureInfo is a BIT STRING, and the parser decodes it to a record carrying the named
1839
+ * bits it asserts in `bits`. Read as an array or a string it rendered as its own object notation and the
1840
+ * reason a request was rejected, which is the one thing this line is for, was lost. */
1841
+ if (r.failInfo) L.push(" Failure Info: " + _list(_obj(r.failInfo).bits).join(", "));
1842
+ var tok = r.timeStampToken;
1843
+ if (!tok) { L.push(" Token: (none)"); return L.join("\n") + "\n"; }
1844
+ var t = tok.tstInfo;
1845
+ L.push(" TSTInfo:");
1846
+ _optLine(L, " ", "Version", t.version);
1847
+ _optLine(L, " ", "Policy", t.policyName ? t.policyName + " (" + t.policy + ")" : t.policy);
1848
+ if (t.messageImprint) {
1849
+ L.push(" Message Imprint:");
1850
+ L.push(" Hash Algorithm: " + _algName(t.messageImprint.hashAlgorithm));
1851
+ if (Buffer.isBuffer(t.messageImprint.hashedMessage)) {
1852
+ L.push(" Hashed Message:");
1853
+ L.push(_hexColon(t.messageImprint.hashedMessage, { wrap: 16, indent: 16 }));
1854
+ }
1855
+ }
1856
+ _serialInto(L, " ", t.serialNumberHex || t.serialNumber);
1857
+ /** @internal A timestamp's value is the instant it names, and RFC 3161 sec. 2.4.2 puts the sub-second part
1858
+ * in the GeneralizedTime's fraction, which the shared date renderer does not carry. Dropped, two stamps a
1859
+ * fraction of a second apart read as the same instant. */
1860
+ if (t.genTime) {
1861
+ L.push(" Gen Time: " + _date(t.genTime) +
1862
+ (t.genTimeFraction ? " (fraction: ." + t.genTimeFraction + ")" : ""));
1863
+ }
1864
+ if (t.accuracy) {
1865
+ var acc = [];
1866
+ if (t.accuracy.seconds != null) acc.push(t.accuracy.seconds + "s");
1867
+ if (t.accuracy.millis != null) acc.push(t.accuracy.millis + "ms");
1868
+ if (t.accuracy.micros != null) acc.push(t.accuracy.micros + "us");
1869
+ L.push(" Accuracy: " + (acc.length ? acc.join(" ") : "(empty)"));
1870
+ } else {
1871
+ L.push(" Accuracy: (none stated)");
1872
+ }
1873
+ L.push(" Ordering: " + (t.ordering === true));
1874
+ if (t.nonceHex) L.push(" Nonce: " + t.nonceHex);
1875
+ if (t.tsa) L.push(" TSA: " + _nameOrGn(t.tsa));
1876
+ if (_list(t.extensions).length) {
1877
+ L.push(" Extensions:");
1878
+ _list(t.extensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1879
+ }
1880
+ L.push(" Certificates: " + _list(tok.certificates).length);
1881
+ return L.join("\n") + "\n";
1882
+ }
1883
+
1884
+ /**
1885
+ * @primitive pki.inspect.attrcert
1886
+ * @signature pki.inspect.attrcert(input) -> string
1887
+ * @since 0.8.29
1888
+ * @status stable
1889
+ * @spec RFC 5755
1890
+ * @related pki.schema.attrcert.parse, pki.inspect.certificate
1891
+ *
1892
+ * Render an RFC 5755 attribute certificate as a text report: the version, the
1893
+ * holder and the issuer in whichever form each takes, the serial, the validity
1894
+ * period, the attributes the certificate asserts, its extensions, and the signature
1895
+ * algorithm. `input` is a PEM string, a DER Buffer, or a
1896
+ * `pki.schema.attrcert.parse` result; a non-attribute-certificate throws
1897
+ * `inspect/bad-attrcert`, a wrong-type input `inspect/bad-input`. An
1898
+ * X.509-1997 v1 attribute certificate is not parsed by this build, and the report
1899
+ * surfaces that refusal rather than rendering a partial one.
1900
+ *
1901
+ * @example
1902
+ * var pair = await pki.key.generate("Ed25519");
1903
+ * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
1904
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
1905
+ * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
1906
+ * { name: "CN=An AA", publicKey: await pki.key.export(pair.publicKey), key: await pki.key.export(pair.privateKey) });
1907
+ * pki.inspect.attrcert(der).split("\n")[0]; // "Attribute Certificate:"
1908
+ */
1909
+ function attrCertReport(input) {
1910
+ var a = _parseAttrCert(input);
1911
+ var L = ["Attribute Certificate:", " Data:"];
1912
+ L.push(" Version: " + a.version);
1913
+ var holder = a.holder;
1914
+ L.push(" Holder:");
1915
+ if (holder.entityName) L.push(" Entity Name: " + _nameOrGn(holder.entityName));
1916
+ if (holder.baseCertificateID) {
1917
+ var bid = holder.baseCertificateID;
1918
+ L.push(" Base Certificate ID:");
1919
+ if (bid.issuer) L.push(" Issuer: " + _nameOrGn(bid.issuer));
1920
+ /** @internal RFC 5755's IssuerSerial carries an optional issuerUID, which distinguishes two issuers that
1921
+ * share a name, so a holder identified with one was reported without the field that identifies it. */
1922
+ if (bid.issuerUID) {
1923
+ var uid = Buffer.isBuffer(bid.issuerUID) ? bid.issuerUID : _obj(bid.issuerUID).bytes;
1924
+ if (Buffer.isBuffer(uid)) L.push(" Issuer Unique ID: " + _hexColon(uid, {}));
1925
+ }
1926
+ /** @internal RFC 5755's IssuerSerial names its field `serial`, which the parser surfaces as `serial`
1927
+ * and `serialHex`, not as the `serialNumber` a certificate carries. Read by the certificate's names
1928
+ * both operands were undefined and the holder's serial was dropped, leaving two holders under one
1929
+ * issuer indistinguishable in the report. */
1930
+ _serialInto(L, " ", bid.serialHex || bid.serial);
1931
+ }
1932
+ /** @internal The DIGEST is what identifies a holder named this way, so the algorithm alone left every
1933
+ * holder identified by one digest algorithm reading the same. The object type says what was hashed. */
1934
+ if (holder.objectDigestInfo) {
1935
+ var odi = holder.objectDigestInfo;
1936
+ L.push(" Object Digest Info:");
1937
+ L.push(" Digested Object Type: " +
1938
+ ((odi.digestedObjectType && odi.digestedObjectType.name) || "unspecified"));
1939
+ if (odi.otherObjectTypeID) L.push(" Other Object Type: " + odi.otherObjectTypeID);
1940
+ L.push(" Digest Algorithm: " + _algName(odi.digestAlgorithm));
1941
+ var digestBytes = odi.objectDigest && (Buffer.isBuffer(odi.objectDigest) ? odi.objectDigest : odi.objectDigest.bytes);
1942
+ if (Buffer.isBuffer(digestBytes)) {
1943
+ L.push(" Digest:");
1944
+ L.push(_hexColon(digestBytes, { wrap: 16, indent: 20 }));
1945
+ }
1946
+ }
1947
+ /** @internal RFC 5755 sec. 4.2.3's V2Form names the issuer by name, by a base certificate id, or by an
1948
+ * object digest, and only the first was rendered: an issuer identified either other way appeared as
1949
+ * nothing at all. The v1Form is a GeneralNames list, which the name renderer reads. */
1950
+ var iss = a.issuer;
1951
+ var issuedBy = iss.v2Form ? _nameOrGn(iss.v2Form.issuerName) : _nameOrGn(iss.v1Form || iss);
1952
+ if (issuedBy === "(none)" && iss.v2Form) {
1953
+ var v2 = iss.v2Form;
1954
+ if (v2.baseCertificateID) {
1955
+ issuedBy = "baseCertificateID " + _nameOrGn(v2.baseCertificateID.issuer) +
1956
+ (v2.baseCertificateID.serialHex ? " serial " + v2.baseCertificateID.serialHex : "");
1957
+ } else if (v2.objectDigestInfo) {
1958
+ issuedBy = "objectDigest " + _algName(v2.objectDigestInfo.digestAlgorithm);
1959
+ }
1960
+ }
1961
+ L.push(" Issuer: " + issuedBy);
1962
+ if (iss.form) L.push(" Issuer Form: " + iss.form);
1963
+ L.push(" " + _serial(a, 12));
1964
+ if (a.validity) {
1965
+ L.push(" Validity:");
1966
+ if (a.validity.notBeforeTime) L.push(" Not Before: " + _date(a.validity.notBeforeTime));
1967
+ if (a.validity.notAfterTime) L.push(" Not After : " + _date(a.validity.notAfterTime));
1968
+ }
1969
+ /** @internal RFC 5755's optional top-level issuerUniqueID, which tells two issuers sharing a name apart the
1970
+ * way the holder's own does, and which the report left out entirely. */
1971
+ if (a.issuerUniqueID) {
1972
+ var acUid = Buffer.isBuffer(a.issuerUniqueID) ? a.issuerUniqueID : _obj(a.issuerUniqueID).bytes;
1973
+ if (Buffer.isBuffer(acUid)) L.push(" Issuer Unique ID: " + _hexColon(acUid, {}));
1974
+ }
1975
+ L.push(" Attributes:");
1976
+ if (_list(a.attributes).length) _list(a.attributes).forEach(function (at) { L.push(_attribute(_obj(at), " ")); });
1977
+ else L.push(" (none)");
1978
+ if (_list(a.extensions).length) {
1979
+ L.push(" Extensions:");
1980
+ _list(a.extensions).forEach(function (e) { L.push(_extension(_obj(e), " ")); });
1981
+ }
1982
+ L.push(" Signature Algorithm: " + _algName(a.signatureAlgorithm));
1983
+ return L.join("\n") + "\n";
1984
+ }
1985
+
1986
+ /** @internal The tag names, inverted out of `asn1.TAGS` so the dump cannot name a universal type
1987
+ * the codec does not have. Tag 0 is the end-of-contents marker X.690 sec. 8.1.5 reserves; the codec
1988
+ * consumes it rather than naming it, and the header walk renders it as a node of its own. */
1989
+ var _DUMP_TAG_NAMES = (function () {
1990
+ var out = Object.create(null);
1991
+ out[0] = "EOC";
1992
+ Object.keys(asn1.TAGS).forEach(function (key) {
1993
+ var name = "";
1994
+ for (var i = 0; i < key.length; i++) name += key[i] === "_" ? " " : key[i];
1995
+ out[asn1.TAGS[key]] = name;
1996
+ });
1997
+ return out;
1998
+ })();
1999
+ var _DUMP_CLASS_LABEL = Object.assign(Object.create(null), {
2000
+ universal: "UNIVERSAL", application: "APPLICATION", private: "PRIVATE",
2001
+ });
2002
+ var _DUMP_CLASS_OF = Object.assign(Object.create(null), {
2003
+ 0x00: "universal", 0x40: "application", 0x80: "context", 0xc0: "private",
2004
+ });
2005
+ var _DUMP_TEXT_TAGS = Object.assign(Object.create(null),
2006
+ { 12: 1, 18: 1, 19: 1, 20: 1, 22: 1, 23: 1, 24: 1, 26: 1, 27: 1, 28: 1, 30: 1 });
2007
+ var _DUMP_MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
2008
+ var _DUMP_NAME_COLUMN = 26;
2009
+ var _DUMP_OFFSET_COLUMN = 5;
2010
+ var _DUMP_LENGTH_COLUMN = 4;
2011
+ var _DUMP_MAX_TAG_OCTETS = 4;
2012
+ var _DUMP_MAX_LENGTH_OCTETS = 6;
2013
+
2014
+ function _padLeft(s, w) { return s.length >= w ? s : " ".repeat(w - s.length) + s; }
2015
+
2016
+ /** @internal `length` decides one name: X.690 sec. 8.1.5 gives the end-of-contents value a zero
2017
+ * length, so a tag-0 value carrying content is not one and is named for its class and number.
2018
+ * Pass -1 where the length is indefinite, which an end-of-contents value never is. */
2019
+ function _dumpTagName(tagClass, tagNumber, length) {
2020
+ if (tagClass === "context") return "[" + tagNumber + "]";
2021
+ if (tagClass === "universal" && tagNumber === 0 && length !== 0) return _DUMP_CLASS_LABEL.universal + " [0]";
2022
+ if (tagClass === "universal" && _DUMP_TAG_NAMES[tagNumber] !== undefined) return _DUMP_TAG_NAMES[tagNumber];
2023
+ return _DUMP_CLASS_LABEL[tagClass] + " [" + tagNumber + "]";
2024
+ }
2025
+
2026
+ function _dumpLine(offset, depth, hl, len, constructed, name, value) {
2027
+ var head = _padLeft(String(offset), _DUMP_OFFSET_COLUMN) + ":d=" + depth + " hl=" + hl +
2028
+ " l=" + _padLeft(String(len), _DUMP_LENGTH_COLUMN) + (constructed ? " cons:" : " prim:");
2029
+ var field = " ".repeat(depth) + name;
2030
+ if (value === null) return head + " " + field;
2031
+ /** @internal One space always separates the name from the value, so a field the column cannot
2032
+ * hold (a long type name at depth) gets that space rather than running into the colon. */
2033
+ var pad = _DUMP_NAME_COLUMN - field.length;
2034
+ return head + " " + field + (pad > 0 ? " ".repeat(pad) : " ") + ":" + value;
2035
+ }
2036
+
2037
+ function _dumpPrefixed(prefix, omitted) {
2038
+ if (omitted === 0) return prefix;
2039
+ var note = "(" + omitted + " more byte" + (omitted === 1 ? "" : "s") + " not rendered)";
2040
+ return prefix.length === 0 ? note : prefix + " " + note;
2041
+ }
2042
+
2043
+ function _dumpHex(buf, limit) {
2044
+ if (buf.length === 0) return "(empty)";
2045
+ var keep = buf.length <= limit ? buf.length : limit;
2046
+ return _dumpPrefixed(_hexColon(buf.subarray(0, keep), {}), buf.length - keep);
2047
+ }
2048
+
2049
+ function _dumpText(buf, limit) {
2050
+ var keep = buf.length <= limit ? buf.length : limit;
2051
+ return _dumpPrefixed(buf.subarray(0, keep).toString("latin1"), buf.length - keep);
2052
+ }
2053
+
2054
+ /** @internal The magnitude read from the content bytes as a two's-complement BigInt in one pass,
2055
+ * behind the same per-value byte cap `asn1.read.integer` applies, because a byte-at-a-time
2056
+ * accumulate over attacker-length content is quadratic. Past that cap the bytes are rendered and
2057
+ * no magnitude is computed. The hex beside a magnitude is the content as written, so a leading zero
2058
+ * octet survives; a magnitude rendered back to hex would drop it. */
2059
+ function _dumpInteger(content, limit) {
2060
+ if (content.length === 0) return "(empty)";
2061
+ if (content.length > constants.LIMITS.DER_MAX_INTEGER_BYTES + 1) return _dumpHex(content, limit);
2062
+ var mag = BigInt("0x" + content.toString("hex"));
2063
+ var v = (content[0] & 0x80) !== 0 ? mag - (1n << BigInt(content.length * 8)) : mag;
2064
+ var text = v.toString();
2065
+ if (v <= _DUMP_MAX_SAFE && v >= -_DUMP_MAX_SAFE) return text;
2066
+ return text + " (0x" + content.toString("hex") + ")";
2067
+ }
2068
+
2069
+ function _dumpBitString(content, limit) {
2070
+ if (content.length === 0) return "(empty)";
2071
+ return content[0] + " unused bits, " + _dumpHex(content.subarray(1), limit);
2072
+ }
2073
+
2074
+ function _dumpOid(content, limit) {
2075
+ var dotted;
2076
+ try { dotted = asn1.decodeOidContent(content); }
2077
+ catch (_e) { return _dumpHex(content, limit); }
2078
+ var name = oid.name(dotted);
2079
+ return name === undefined ? dotted : dotted + " (" + name + ")";
2080
+ }
2081
+
2082
+ /** @internal `maxValueBytes` bounds every rendering whose size grows with the content length. A hex
2083
+ * dump and a text rendering are truncated and the omitted count stated, because a prefix of either
2084
+ * is still true of the value. A magnitude and a dotted OID are read from the whole content and
2085
+ * cannot be rendered from a prefix at all, so past the cap the bytes are what the report can
2086
+ * honestly show. A decoded form of fixed size renders whatever the cap is, since it cannot grow the
2087
+ * report: a BOOLEAN's verdict, and a BIT STRING's unused-bit count above its capped body. */
2088
+ function _dumpValue(tagClass, tagNumber, content, limit) {
2089
+ if (tagClass !== "universal") return _dumpHex(content, limit);
2090
+ if (tagNumber === asn1.TAGS.BOOLEAN) {
2091
+ if (content.length === 1 && content[0] === 0xff) return "TRUE";
2092
+ if (content.length === 1 && content[0] === 0x00) return "FALSE";
2093
+ return _dumpHex(content, limit);
2094
+ }
2095
+ if (tagNumber === asn1.TAGS.INTEGER || tagNumber === asn1.TAGS.ENUMERATED) {
2096
+ return content.length <= limit ? _dumpInteger(content, limit) : _dumpHex(content, limit);
2097
+ }
2098
+ if (tagNumber === asn1.TAGS.BIT_STRING) return _dumpBitString(content, limit);
2099
+ if (tagNumber === asn1.TAGS.NULL) return content.length === 0 ? null : _dumpHex(content, limit);
2100
+ if (tagNumber === asn1.TAGS.OBJECT_IDENTIFIER) {
2101
+ return content.length <= limit ? _dumpOid(content, limit) : _dumpHex(content, limit);
2102
+ }
2103
+ if (_DUMP_TEXT_TAGS[tagNumber] === 1 && _printable(content)) return _dumpText(content, limit);
2104
+ return _dumpHex(content, limit);
2105
+ }
2106
+
2107
+ function _dumpBytes(n) { return n + (n === 1 ? " byte" : " bytes"); }
2108
+ function _dumpNodes(n) { return n + (n === 1 ? " node" : " nodes"); }
2109
+ function _dumpRemaining(n) { return _dumpBytes(n) + (n === 1 ? " that remains." : " that remain."); }
2110
+
2111
+ /** @internal The node cap records the offset it stopped at, not only that it fired: a report that
2112
+ * covers part of a file has to name where that part ends or a reader cannot tell what it covers. */
2113
+ function _dumpCapped(st, offset) {
2114
+ st.capped = true;
2115
+ st.capOffset = offset;
2116
+ return -1;
2117
+ }
2118
+
2119
+ /** @internal Renders a tree the strict decoder produced. It applies the node cap and not the depth
2120
+ * cap, because the decode that built this tree was given the same depth cap and refuses a deeper
2121
+ * one, so no node reaching here is past it. */
2122
+ function _dumpNode(node, depth, st) {
2123
+ if (st.capped) return;
2124
+ if (st.nodes >= st.maxNodes) { _dumpCapped(st, node.header.start); return; }
2125
+ st.nodes += 1;
2126
+ var hl = node.header.end - node.header.start;
2127
+ var name = _dumpTagName(node.tagClass, node.tagNumber, node.length);
2128
+ /** @internal `content` and `children` are read without a fallback: a decode gives every primitive
2129
+ * a Buffer content, empty where the value is, and every constructed node a children array. A
2130
+ * fallback for either would be dead code standing in for a shape the decoder guarantees. */
2131
+ if (!node.constructed) {
2132
+ st.lines.push(_dumpLine(node.header.start, depth, hl, node.length, false, name,
2133
+ _dumpValue(node.tagClass, node.tagNumber, node.content, st.maxValueBytes)));
2134
+ return;
2135
+ }
2136
+ st.lines.push(_dumpLine(node.header.start, depth, hl, node.length, true, name, null));
2137
+ var kids = node.children;
2138
+ for (var i = 0; i < kids.length; i++) {
2139
+ _dumpNode(kids[i], depth + 1, st);
2140
+ if (st.capped) return;
2141
+ }
2142
+ }
2143
+
2144
+ /** @internal The header-by-header walk that runs when the strict decoder refuses the input. It reads
2145
+ * a tag and a length, renders the node, and on the first byte it cannot read records the offset and
2146
+ * the reason and unwinds. It returns the offset one past the value, or -1 once a stop or the node cap
2147
+ * has been recorded. It produces text and never a node, so nothing a lax read accepted can reach a
2148
+ * verify path, and it carries the caller's depth and node caps because it walks hostile nesting
2149
+ * exactly as a parse does. */
2150
+ function _dumpWalk(buf, start, limit, depth, st) {
2151
+ if (st.stop !== null || st.capped) return -1;
2152
+ if (depth > st.maxDepth) {
2153
+ st.stop = { offset: start, reason: "nesting exceeds the depth cap " + st.maxDepth + "." };
2154
+ return -1;
2155
+ }
2156
+ if (st.nodes >= st.maxNodes) return _dumpCapped(st, start);
2157
+ var p = start;
2158
+ if (p >= limit) {
2159
+ st.stop = { offset: start, reason: "the input ends where an identifier octet was expected." };
2160
+ return -1;
2161
+ }
2162
+ var first = buf[p]; p += 1;
2163
+ var constructed = (first & 0x20) !== 0;
2164
+ var tagClass = _DUMP_CLASS_OF[first & 0xc0];
2165
+ var tagNumber = first & 0x1f;
2166
+ if (tagNumber === 0x1f) {
2167
+ tagNumber = 0;
2168
+ var seen = 0;
2169
+ for (;;) {
2170
+ if (p >= limit) {
2171
+ st.stop = { offset: start, reason: "the high-tag-number form runs past the end of the input." };
2172
+ return -1;
2173
+ }
2174
+ var tb = buf[p]; p += 1;
2175
+ tagNumber = (tagNumber * 128) + (tb & 0x7f);
2176
+ seen += 1;
2177
+ if ((tb & 0x80) === 0) break;
2178
+ if (seen >= _DUMP_MAX_TAG_OCTETS) {
2179
+ st.stop = { offset: start, reason: "the tag number uses more than " + _DUMP_MAX_TAG_OCTETS + " octets." };
2180
+ return -1;
2181
+ }
2182
+ }
2183
+ }
2184
+ if (p >= limit) {
2185
+ st.stop = { offset: start, reason: "the input ends where a length octet was expected." };
2186
+ return -1;
2187
+ }
2188
+ var lenByte = buf[p]; p += 1;
2189
+ var indefinite = false;
2190
+ var length = 0;
2191
+ if (lenByte === 0x80) {
2192
+ if (!constructed) {
2193
+ st.stop = { offset: start,
2194
+ reason: "an indefinite length on a primitive value has no end-of-contents octets to find." };
2195
+ return -1;
2196
+ }
2197
+ indefinite = true;
2198
+ } else if ((lenByte & 0x80) !== 0) {
2199
+ var n = lenByte & 0x7f;
2200
+ if (n > _DUMP_MAX_LENGTH_OCTETS) {
2201
+ st.stop = { offset: start, reason: "the length uses more than " + _DUMP_MAX_LENGTH_OCTETS + " octets." };
2202
+ return -1;
2203
+ }
2204
+ if (p + n > limit) {
2205
+ st.stop = { offset: start, reason: "the long-form length runs past the end of the input." };
2206
+ return -1;
2207
+ }
2208
+ for (var j = 0; j < n; j++) length = (length * 256) + buf[p + j];
2209
+ p += n;
2210
+ } else {
2211
+ length = lenByte;
2212
+ }
2213
+ var hl = p - start;
2214
+ var contentStart = p;
2215
+ var name = _dumpTagName(tagClass, tagNumber, indefinite ? -1 : length);
2216
+
2217
+ if (!constructed) {
2218
+ if (length > limit - contentStart) {
2219
+ st.stop = { offset: start,
2220
+ reason: "content length " + length + " overruns the " + _dumpRemaining(limit - contentStart) };
2221
+ return -1;
2222
+ }
2223
+ st.nodes += 1;
2224
+ st.lines.push(_dumpLine(start, depth, hl, length, false, name,
2225
+ _dumpValue(tagClass, tagNumber, buf.subarray(contentStart, contentStart + length), st.maxValueBytes)));
2226
+ return contentStart + length;
2227
+ }
2228
+
2229
+ st.nodes += 1;
2230
+ var cp = contentStart;
2231
+ var kid;
2232
+ if (indefinite) {
2233
+ st.lines.push(_dumpLine(start, depth, hl, "inf", true, name, null));
2234
+ for (;;) {
2235
+ if (cp + 2 > limit) {
2236
+ st.stop = { offset: start,
2237
+ reason: "the indefinite-length value has no end-of-contents octets before the end of the input." };
2238
+ return -1;
2239
+ }
2240
+ if (buf[cp] === 0x00 && buf[cp + 1] === 0x00) break;
2241
+ kid = _dumpWalk(buf, cp, limit, depth + 1, st);
2242
+ if (kid < 0) return -1;
2243
+ cp = kid;
2244
+ }
2245
+ /** @internal The end-of-contents octets are the one node the walk emits without recursing, so
2246
+ * both caps are applied to them here. Without the depth check an indefinite-length value with
2247
+ * no children renders a node one level past the cap and the report still says it completed. */
2248
+ if (depth + 1 > st.maxDepth) {
2249
+ st.stop = { offset: cp, reason: "nesting exceeds the depth cap " + st.maxDepth + "." };
2250
+ return -1;
2251
+ }
2252
+ if (st.nodes >= st.maxNodes) return _dumpCapped(st, cp);
2253
+ st.nodes += 1;
2254
+ st.lines.push(_dumpLine(cp, depth + 1, 2, 0, false, _DUMP_TAG_NAMES[0], null));
2255
+ return cp + 2;
2256
+ }
2257
+ st.lines.push(_dumpLine(start, depth, hl, length, true, name, null));
2258
+ var contentEnd = contentStart + length;
2259
+ var childLimit = contentEnd > limit ? limit : contentEnd;
2260
+ while (cp < childLimit) {
2261
+ kid = _dumpWalk(buf, cp, childLimit, depth + 1, st);
2262
+ if (kid < 0) return -1;
2263
+ cp = kid;
2264
+ }
2265
+ if (contentEnd > limit) {
2266
+ st.stop = { offset: start,
2267
+ reason: "content length " + length + " overruns the " + _dumpRemaining(limit - contentStart) };
2268
+ return -1;
2269
+ }
2270
+ return contentEnd;
2271
+ }
2272
+
2273
+ var KNOWN_DUMP_OPTS = Object.assign(Object.create(null),
2274
+ { maxBytes: 1, maxDepth: 1, maxNodes: 1, maxValueBytes: 1 });
2275
+
2276
+ /**
2277
+ * @primitive pki.inspect.asn1
2278
+ * @signature pki.inspect.asn1(input, opts?) -> string
2279
+ * @since 0.8.31
2280
+ * @status stable
2281
+ * @spec X.690, ISO/IEC 8825-1
2282
+ * @defends ASN.1-parser-DoS (CWE-400)
2283
+ * @related pki.asn1.decode, pki.inspect.any
2284
+ *
2285
+ * Render the TLV tree of `input` (a PEM string or DER Buffer) as a structural dump, one
2286
+ * line per value, in the shape `openssl asn1parse` prints: the byte offset, the nesting
2287
+ * depth, the header length, the content length, whether the value is constructed, the tag
2288
+ * name, and for a primitive its rendered content. An OBJECT IDENTIFIER carries its dotted
2289
+ * form and its registry name. A UTCTime or GeneralizedTime renders the bytes as written
2290
+ * rather than a formatted date, so a malformed time is visible instead of normalized away.
2291
+ *
2292
+ * Two routes produce a report and the first line names which one ran. `pki.asn1.decode`
2293
+ * reads the input first, so a conforming file is rendered from the same decoder the rest of
2294
+ * the toolkit parses with. When that decode refuses the bytes, the report names the refusal
2295
+ * and a header-by-header walk renders what it can read: a truncated file, trailing bytes, a
2296
+ * non-minimal length, an indefinite length, a segmented string. The last line of every report
2297
+ * states the outcome: the offset where the structure ends, or the value the walk could not read
2298
+ * through and why, or the bytes that follow the top-level value, or the cap that stopped the
2299
+ * report and where the next value begins. A partial dump cannot be mistaken for a whole
2300
+ * structure. The walk returns text and never a node tree, and it carries the depth and node
2301
+ * caps, so tolerating a broken encoding here relaxes nothing the parsers enforce.
2302
+ *
2303
+ * No value is withheld here. Where the other reports name a key and print none of it, this
2304
+ * one prints bytes, private key material included, which is what a byte-level dump is for.
2305
+ * The caps bound how much of a value is rendered, not whether it is. `pki.inspect.any` and
2306
+ * `pki.inspect.pkcs8` are the reports that name a key without printing it.
2307
+ *
2308
+ * @opts
2309
+ * maxBytes: number, // default and ceiling: C.LIMITS.DER_MAX_BYTES (16 MiB); refuses a
2310
+ * // longer input, and tightens only -- a larger value is refused
2311
+ * maxDepth: number, // default: C.LIMITS.DER_MAX_DEPTH (64); stops at this nesting depth
2312
+ * maxNodes: number, // default: C.LIMITS.DUMP_MAX_NODES; stops after this many nodes
2313
+ * maxValueBytes: number, // default: C.LIMITS.DUMP_MAX_VALUE_BYTES; renders at most this
2314
+ * // many bytes of one value and counts the rest
2315
+ *
2316
+ * @example
2317
+ * var b = pki.asn1.build;
2318
+ * var der = b.sequence([b.integer(1), b.oid("2.5.4.3"), b.nullValue()]);
2319
+ * console.log(pki.inspect.asn1(der));
2320
+ * // ASN.1 structure: 12 bytes, decoded as DER
2321
+ * // 0:d=0 hl=2 l= 10 cons: SEQUENCE
2322
+ * // 2:d=1 hl=2 l= 1 prim: INTEGER :1
2323
+ * // 5:d=1 hl=2 l= 3 prim: OBJECT IDENTIFIER :2.5.4.3 (commonName)
2324
+ * // 10:d=1 hl=2 l= 0 prim: NULL
2325
+ * // Complete: the structure ends at offset 12, the end of the input.
2326
+ * console.log(pki.inspect.asn1(der.subarray(0, 7)));
2327
+ * // ASN.1 structure: 7 bytes, header walk
2328
+ * // DER decode refused: asn1/truncated: content length overruns the buffer
2329
+ * // 0:d=0 hl=2 l= 10 cons: SEQUENCE
2330
+ * // 2:d=1 hl=2 l= 1 prim: INTEGER :1
2331
+ * // Incomplete: the walk could not read the value at offset 5: content length 3 overruns the 0 bytes that remain.
2332
+ */
2333
+ function asn1Dump(input, opts) {
2334
+ if (opts === undefined || opts === null) opts = {};
2335
+ if (typeof opts !== "object" || Array.isArray(opts)) {
2336
+ throw _err("inspect/bad-input", "pki.inspect.asn1 options must be an object");
2337
+ }
2338
+ guard.identifier.assertKnownKeys(opts, KNOWN_DUMP_OPTS, _err, "inspect/bad-input",
2339
+ "pki.inspect.asn1 has an unknown option ");
2340
+ var o = guard.identifier.snapshotOptions(opts, KNOWN_DUMP_OPTS);
2341
+ /** @internal `maxBytes` tightens and never raises. The shared parse door carries its own byte
2342
+ * ceiling and applies it before this verb sees the bytes, so a larger value could not take effect;
2343
+ * bounding it here refuses that value instead of accepting it and then contradicting it. */
2344
+ var maxBytes = guard.limits.cap(o.maxBytes, "maxBytes", constants.LIMITS.DER_MAX_BYTES,
2345
+ { max: constants.LIMITS.DER_MAX_BYTES });
2346
+ var maxDepth = guard.limits.depthCap(o.maxDepth, "maxDepth", constants.LIMITS.DER_MAX_DEPTH);
2347
+ var maxNodes = guard.limits.cap(o.maxNodes, "maxNodes", constants.LIMITS.DUMP_MAX_NODES);
2348
+ var maxValueBytes = guard.limits.cap(o.maxValueBytes, "maxValueBytes", constants.LIMITS.DUMP_MAX_VALUE_BYTES);
2349
+
2350
+ var der;
2351
+ try { der = pkix.coerceToDer(input, _INSPECT_ENTRY); }
2352
+ catch (e) { throw _err("inspect/bad-input", "input must be a DER Buffer or a PEM block", e); }
2353
+ if (der.length === 0) throw _err("inspect/bad-input", "input is empty");
2354
+ if (der.length > maxBytes) {
2355
+ throw _err("inspect/bad-input", "input " + der.length + " bytes exceeds the cap " + maxBytes);
2356
+ }
2357
+
2358
+ var head = ["ASN.1 structure: " + der.length + " bytes, decoded as DER"];
2359
+ var node = null;
2360
+ var refusal = null;
2361
+ /** @internal The decoder builds the whole node tree before the renderer counts a line, so the
2362
+ * report's node cap reaches it as an item cap or a file of small values allocates a tree the
2363
+ * report will never print. It is given one node more than the report prints, so a structure just
2364
+ * past the cap is still read strictly and the report says it stopped at the cap; further past it,
2365
+ * the decoder refuses, the report names that refusal, and the walk renders the bounded prefix. */
2366
+ try {
2367
+ node = asn1.decode(der, { maxBytes: maxBytes, maxDepth: maxDepth, maxItems: maxNodes + 1 });
2368
+ }
2369
+ catch (e) {
2370
+ refusal = e;
2371
+ head = ["ASN.1 structure: " + der.length + " bytes, header walk",
2372
+ "DER decode refused: " + (e.code || "error") + ": " + e.message];
2373
+ }
2374
+
2375
+ var st = { lines: [], nodes: 0, maxNodes: maxNodes, maxDepth: maxDepth,
2376
+ maxValueBytes: maxValueBytes, capped: false, capOffset: 0, stop: null };
2377
+ var end;
2378
+ if (refusal === null) {
2379
+ _dumpNode(node, 0, st);
2380
+ end = node.header.start + node.bytes.length;
2381
+ } else {
2382
+ end = _dumpWalk(der, 0, der.length, 0, st);
2383
+ }
2384
+
2385
+ /** @internal Every stop names the value the walk could not read THROUGH, which is not the offset
2386
+ * reading had reached: an unterminated indefinite length is read to the end of the input and the
2387
+ * value at fault is still the one that opened it. */
2388
+ var tail;
2389
+ if (st.stop !== null) {
2390
+ tail = "Incomplete: the walk could not read the value at offset " + st.stop.offset + ": " +
2391
+ st.stop.reason;
2392
+ } else if (st.capped) {
2393
+ tail = "Incomplete: the report stopped after " + _dumpNodes(maxNodes) + ", the cap on one dump; " +
2394
+ "the next value begins at offset " + st.capOffset + ".";
2395
+ } else if (end < der.length) {
2396
+ tail = "Incomplete: " + _dumpBytes(der.length - end) + " at offset " + end +
2397
+ " follow the top-level value.";
2398
+ } else {
2399
+ tail = "Complete: the structure ends at offset " + end + ", the end of the input.";
2400
+ }
2401
+ return head.concat(st.lines, [tail]).join("\n");
2402
+ }
2403
+
2404
+ var _INSPECT_BY_FORMAT = Object.assign(Object.create(null), {
2405
+ x509: certificate, crl: crlReport, csr: csrReport, cms: cmsReport,
2406
+ pkcs8: pkcs8Report, pkcs12: pkcs12Report, crmf: crmfReport, cmp: cmpReport,
2407
+ csrattrs: csrattrsReport, trustanchor: trustAnchorReport,
2408
+ "ocsp-request": ocspRequestReport, "ocsp-response": ocspResponseReport,
2409
+ tsp: tspReport, attrcert: attrCertReport,
2410
+ });
868
2411
 
869
2412
  /**
870
2413
  * @primitive pki.inspect.any
@@ -875,11 +2418,12 @@ var _INSPECT_BY_FORMAT = Object.assign(Object.create(null), { x509: certificate,
875
2418
  * @related pki.schema.detectFormat, pki.inspect.certificate
876
2419
  *
877
2420
  * Detect which PKI format `input` (a PEM string or DER Buffer) encodes and render
878
- * it with the matching report, the inspect analogue of `pki.schema.parse`. Routes
879
- * a certificate / CRL / CSR / CMS to `certificate` / `crl` / `csr` / `cms`; a
880
- * detected but out-of-scope format (OCSP, TSP, PKCS#8/#12, CRMF, CMP, ...) throws
881
- * `inspect/unsupported-format` naming it, and an unrecognized input
882
- * `inspect/bad-input`.
2421
+ * it with the matching report, the inspect analogue of `pki.schema.parse`. Every
2422
+ * format `pki.schema.all()` detects has a report and its own named verb, so the
2423
+ * same bytes render the same whether the format is detected or asked for by name.
2424
+ * An X.509-1997 v1 attribute certificate is detected so it can be named, and it is
2425
+ * refused with the reason its own parser gives rather than as an unsupported
2426
+ * format. An unrecognized input throws `inspect/bad-input`.
883
2427
  *
884
2428
  * @example
885
2429
  * var pair = await pki.key.generate("Ed25519");
@@ -896,7 +2440,17 @@ function any(input) {
896
2440
  } catch (e) { throw _err("inspect/bad-input", "input is not a decodable DER Buffer or PEM string", e); }
897
2441
  if (fmt === null) throw _err("inspect/bad-input", "input does not match any registered PKI format");
898
2442
  var render = _INSPECT_BY_FORMAT[fmt];
899
- if (!render) throw _err("inspect/unsupported-format", "inspect does not support the detected format \"" + fmt + "\" (supported: certificate, crl, csr, cms)");
2443
+ /** @internal A format with no renderer is one whose parser refuses the bytes outright, and the reason
2444
+ * it refuses is what a reader of that file needs. `attrcert-v1` is the case: an X.509-1997 attribute
2445
+ * certificate is detected so it can be named rather than misread as the v2 form, and its parser
2446
+ * throws `attrcert/legacy-v1-not-supported`. Reporting "inspect does not support this format" would
2447
+ * send an operator looking for a missing feature instead of telling them which form they hold, so the
2448
+ * format's own parse runs and its error propagates. */
2449
+ if (!render) {
2450
+ schemaAll.parse(der);
2451
+ throw _err("inspect/unsupported-format", "inspect does not support the detected format \"" + fmt +
2452
+ "\" (supported: " + Object.keys(_INSPECT_BY_FORMAT).sort().join(", ") + ")");
2453
+ }
900
2454
  return render(der);
901
2455
  }
902
2456
 
@@ -905,6 +2459,17 @@ module.exports = {
905
2459
  crl: crlReport,
906
2460
  csr: csrReport,
907
2461
  cms: cmsReport,
2462
+ pkcs8: pkcs8Report,
2463
+ pkcs12: pkcs12Report,
2464
+ crmf: crmfReport,
2465
+ cmp: cmpReport,
2466
+ csrattrs: csrattrsReport,
2467
+ trustanchor: trustAnchorReport,
2468
+ ocspRequest: ocspRequestReport,
2469
+ ocspResponse: ocspResponseReport,
2470
+ tsp: tspReport,
2471
+ attrcert: attrCertReport,
2472
+ asn1: asn1Dump,
908
2473
  any: any,
909
2474
  renderedExtensions: Object.keys(EXT_RENDERERS),
910
2475
  };