@blamejs/pki 0.8.31 → 0.8.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/README.md +66 -15
  3. package/bin/pki.js +716 -30
  4. package/examples/issue-a-certificate.js +90 -0
  5. package/examples/post-quantum.js +66 -0
  6. package/examples/read-a-certificate.js +56 -0
  7. package/examples/revoke-and-check.js +86 -0
  8. package/examples/sign-and-verify-cms.js +63 -0
  9. package/index.js +18 -5
  10. package/lib/acme.js +223 -74
  11. package/lib/alt-sig.js +441 -0
  12. package/lib/asn1-der.js +48 -25
  13. package/lib/attrcert-sign.js +27 -19
  14. package/lib/cbor-det.js +24 -14
  15. package/lib/cmc-build.js +9 -2
  16. package/lib/cmc-verify.js +14 -3
  17. package/lib/cmp-build.js +17 -5
  18. package/lib/cmp-session.js +24 -11
  19. package/lib/cmp-verify.js +28 -19
  20. package/lib/cms-compress.js +3 -1
  21. package/lib/cms-decrypt.js +102 -7
  22. package/lib/cms-encrypt.js +59 -7
  23. package/lib/cms-sign.js +31 -9
  24. package/lib/cms-verify.js +22 -18
  25. package/lib/composite-kem.js +30 -18
  26. package/lib/composite-sig.js +29 -14
  27. package/lib/constants.js +26 -0
  28. package/lib/crl-sign.js +111 -19
  29. package/lib/crmf-sign.js +167 -15
  30. package/lib/csr-sign.js +117 -6
  31. package/lib/ct.js +1008 -58
  32. package/lib/est.js +29 -16
  33. package/lib/framework-error.js +12 -0
  34. package/lib/guard-async.js +1 -1
  35. package/lib/guard-bytes.js +103 -20
  36. package/lib/guard-crypto.js +205 -3
  37. package/lib/guard-encoding.js +6 -1
  38. package/lib/guard-identifier.js +55 -8
  39. package/lib/guard-intrinsic.js +43 -0
  40. package/lib/guard-json.js +122 -9
  41. package/lib/guard-limits.js +12 -1
  42. package/lib/guard-list.js +63 -8
  43. package/lib/guard-name.js +167 -20
  44. package/lib/guard-range.js +43 -5
  45. package/lib/guard-text.js +41 -0
  46. package/lib/guard-verdict.js +39 -8
  47. package/lib/hpke.js +688 -42
  48. package/lib/http-transport.js +156 -4
  49. package/lib/identity-match.js +4 -4
  50. package/lib/inspect.js +33 -6
  51. package/lib/jose.js +93 -25
  52. package/lib/key.js +18 -13
  53. package/lib/lint.js +165 -1
  54. package/lib/merkle.js +233 -27
  55. package/lib/ocsp.js +27 -14
  56. package/lib/oid.js +14 -4
  57. package/lib/path-validate.js +143 -61
  58. package/lib/pkcs12-build.js +7 -2
  59. package/lib/pki-build.js +31 -7
  60. package/lib/possession.js +575 -0
  61. package/lib/related-cert.js +708 -0
  62. package/lib/revocation-fetch.js +6 -6
  63. package/lib/scep.js +15 -5
  64. package/lib/schema-c509.js +5 -2
  65. package/lib/schema-cmc.js +1 -1
  66. package/lib/schema-cmp.js +8 -8
  67. package/lib/schema-cms.js +2 -15
  68. package/lib/schema-csr.js +69 -0
  69. package/lib/schema-engine.js +10 -3
  70. package/lib/schema-pkix.js +228 -2
  71. package/lib/schema-tsp.js +7 -6
  72. package/lib/sign-scheme.js +22 -9
  73. package/lib/sigstore.js +684 -168
  74. package/lib/sleep.js +8 -1
  75. package/lib/smime.js +22 -1
  76. package/lib/tlog.js +1235 -0
  77. package/lib/tls-cert-compress.js +25 -8
  78. package/lib/tsp-sign.js +120 -35
  79. package/lib/tuf.js +1395 -0
  80. package/lib/validator-tpm.js +4 -1
  81. package/lib/verify-seam.js +7 -1
  82. package/lib/webauthn-mds.js +41 -4
  83. package/lib/webauthn.js +43 -25
  84. package/lib/webcrypto.js +48 -46
  85. package/lib/x509-sign.js +225 -19
  86. package/package.json +2 -1
  87. package/sbom.cdx.json +6 -6
package/bin/pki.js CHANGED
@@ -3,13 +3,22 @@
3
3
  // Copyright (c) blamejs contributors
4
4
  "use strict";
5
5
  /**
6
- * pki — command-line front-end for @blamejs/pki.
6
+ * pki: the command-line front-end for @blamejs/pki.
7
7
  *
8
8
  * pki version print the package version
9
9
  * pki oid <dotted|name> resolve an OID <-> name
10
10
  * pki parse <cert> parse an X.509 certificate to JSON
11
- * pki inspect <cert> render a certificate as text (openssl x509 -text style)
12
- * pki lint <file> [--profile P] lint a certificate, CRL or OCSP response; exit non-zero on an error finding
11
+ * pki inspect <file> [--asn1] render whatever the file holds as text, or dump its TLV tree
12
+ * pki keygen --out <key> [--pub <spki>] generate a key pair; the private key to a file, never stdout
13
+ * [--alg NAME] [--pem]
14
+ * pki csr --key <k> --subject <dn> a PKCS#10 request over the key's public half
15
+ * [--san a,b] [--out F] [--pem]
16
+ * pki issue (--key <k> --subject <dn> a certificate, self-signed or from a supplied issuer
17
+ * | --csr <req>) [--ca]
18
+ * [--issuer-cert <c> --issuer-key <k>] [--days N] [--serial HEX] [--san a,b] [--out F] [--pem]
19
+ * pki fetch <https-url> --anchor <c> the chain a live endpoint presents; NOT a verification
20
+ * [--system] [--out F] [--der]
21
+ * pki lint <file> [--profile P] lint the structure the file holds; exit non-zero on an error finding
13
22
  * [--severity S] [--json]
14
23
  * pki convert <file> --to der|pem transcode a DER/PEM file between the two encodings
15
24
  * [--label LABEL]
@@ -19,7 +28,7 @@
19
28
  * [--detached] [--pss] [--digest D] [--pem] [--out F]
20
29
  *
21
30
  * The CLI is a thin operator convenience over the library surface: it validates its
22
- * arguments (entry-point tier — bad input exits non-zero with a message) and never does
31
+ * arguments (the entry-point tier, where bad input exits non-zero with a message) and never does
23
32
  * anything the public API cannot. inspect / lint / convert / verify / sign compose pki.inspect,
24
33
  * pki.lint, the per-format PEM codecs, pki.path.validate, and pki.cms.sign respectively.
25
34
  */
@@ -27,16 +36,61 @@
27
36
  var fs = require("node:fs");
28
37
  var pki = require("../index.js");
29
38
 
39
+ // One line stays one line. A path comes from the command line, and a carriage return or newline in one
40
+ // would end the line early and put whatever follows it where an operator reads a report of the tool's
41
+ // own, so those two bytes are shown rather than obeyed. Shared by every report: fifteen `fail` call
42
+ // sites interpolate a caller-supplied path, and escaping only the exit-handler's reports left those
43
+ // printing the raw byte. Measured: `keygen --pub "nl\na.pub"` over an existing file reported across two
44
+ // lines, the second reading as a line of this tool's own output.
45
+ function _oneLine(message) {
46
+ var text = String(message);
47
+ var one = "";
48
+ for (var i = 0; i < text.length; i++) {
49
+ var c = text.charCodeAt(i);
50
+ // TWO hex digits. `(0x0a).toString(16)` is "a", so a one-digit escape ran straight into the next
51
+ // character of the path: a newline followed by "a" printed as "\xaa", which reads as an escape of
52
+ // one byte while standing for two. The reader cannot tell those apart, so the width is fixed.
53
+ one += (c === 0x0a || c === 0x0d) ? (c === 0x0a ? "\\x0a" : "\\x0d") : text.charAt(i);
54
+ }
55
+ return one;
56
+ }
57
+
30
58
  function fail(msg) {
31
- process.stderr.write("pki: " + msg + "\n");
59
+ process.stderr.write("pki: " + _oneLine(msg) + "\n");
32
60
  process.exit(1);
33
61
  }
34
62
 
63
+ // A stdout that cannot be written -- a closed descriptor, a full pipe -- raises an `error` event on
64
+ // the stream rather than throwing where the write was called, and with nothing listening Node ended
65
+ // the process with its own stack trace. Two things follow. The message an operator gets is a
66
+ // `node:events` dump instead of a `pki:` line, and for a verb whose product is a FILE the exit code
67
+ // said failure about work that had completed: `keygen` wrote the key, could not print the line saying
68
+ // so, and exited non-zero with the key on disk, which is indistinguishable from the failure that
69
+ // leaves a secret behind.
70
+ //
71
+ // So the product's location decides the exit code. `wroteToDisk` is set by a verb once its output is
72
+ // on disk rather than on stdout; for every other verb stdout IS the product and a failure to write it
73
+ // is the command failing.
74
+ var wroteToDisk = false;
75
+ function _onOutputError(stream, e) {
76
+ if (stream !== "stderr") {
77
+ try {
78
+ process.stderr.write("pki: " + stream + " could not be written (" + e.message + ")" +
79
+ (wroteToDisk ? "; the output was written to disk and is intact" : "") + "\n");
80
+ } catch (_e) { /* allow:swallow-unverified nothing can be reported; the exit code is the record */ }
81
+ }
82
+ process.exit(wroteToDisk ? 0 : 1);
83
+ }
84
+ process.stdout.on("error", function (e) { _onOutputError("stdout", e); });
85
+ process.stderr.on("error", function (e) { _onOutputError("stderr", e); });
86
+
35
87
  // Minimal flag parser: `--flag value` for value-taking flags, `--flag` for booleans, the
36
88
  // rest are positionals in `_`. A value-taking flag whose value is absent or is itself another
37
89
  // flag is a usage error (never silently coerced to `true`, which would make e.g. `--time`
38
90
  // parse as `new Date(true)` = a real 1970 timestamp). No clustering, no `=value`.
39
- var VALUE_FLAGS = { to: 1, profile: 1, severity: 1, label: 1, anchor: 1, time: 1, cert: 1, key: 1, digest: 1, out: 1 };
91
+ var VALUE_FLAGS = { to: 1, profile: 1, severity: 1, label: 1, anchor: 1, time: 1, cert: 1, key: 1,
92
+ digest: 1, out: 1, alg: 1, pub: 1, subject: 1, san: 1, days: 1, serial: 1, csr: 1,
93
+ "issuer-cert": 1, "issuer-key": 1 };
40
94
  function parseArgs(argv) {
41
95
  var out = { _: [] };
42
96
  for (var i = 0; i < argv.length; i++) {
@@ -56,6 +110,14 @@ function readFileBytes(file) {
56
110
  try { return fs.readFileSync(file); } catch (e) { return fail("cannot read " + file + ": " + e.message); }
57
111
  }
58
112
 
113
+ // A verb taking a bare positional answers a missing argument with its own usage line. An explicit
114
+ // `--help` never reaches here: `main` answers it before the verb runs, so an argument that is absent
115
+ // is the only case left.
116
+ function usageOrArg(arg, usage) {
117
+ if (!arg) return fail(usage);
118
+ return arg;
119
+ }
120
+
59
121
  // For the library entry points (parse / inspect / lint / verify) that accept EITHER a DER
60
122
  // Buffer or a PEM string and own the decode + error handling: hand a Buffer when the bytes
61
123
  // are a well-formed DER structure, otherwise the text so the library pemDecodes it (and
@@ -63,10 +125,25 @@ function readFileBytes(file) {
63
125
  // which is what preserves the linter's never-throw survey -- malformed bytes become a fatal
64
126
  // lint/unparseable finding rather than a CLI hard-fail. DER-first is unambiguous (a PEM file
65
127
  // is ASCII text and never decodes as one DER TLV).
128
+ // The PEM grammar both readers match against: an uppercase A-Z0-9 label whose BEGIN and END agree.
129
+ // One definition, so the ambiguity check and the convert extractor cannot disagree about what a
130
+ // block is.
131
+ var PEM_BLOCK_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
132
+
66
133
  function readForLib(file) {
67
134
  var bytes = readFileBytes(file);
68
- try { pki.asn1.decode(bytes); return bytes; }
69
- catch (_derErr) { return bytes.toString("latin1"); } // not DER -- let the library pemDecode / report it
135
+ var isDer = true;
136
+ try { pki.asn1.decode(bytes); } catch (_derErr) { isDer = false; }
137
+ if (!isDer) return bytes.toString("latin1"); // not DER -- let the library pemDecode / report it
138
+ // A short DER header can be entirely printable, so a file can be one complete DER value AND a
139
+ // well-formed PEM block at once. Neither reading is wrong, so neither is chosen: reporting the
140
+ // DER reading's parse failure would send the reader after a fault in the wrong structure.
141
+ if (PEM_BLOCK_RE.test(bytes.toString("latin1"))) {
142
+ return fail(file + " reads as both one complete DER value and a PEM block, so which was meant " +
143
+ "cannot be read from the bytes: run `pki convert " + file + " --to pem` to settle it as the " +
144
+ "DER value, or delete the bytes before its -----BEGIN line to settle it as the PEM block");
145
+ }
146
+ return bytes;
70
147
  }
71
148
 
72
149
  // For `convert`, which transcodes RAW bytes and bypasses the library parse: extract DER
@@ -79,7 +156,7 @@ function readDer(file) {
79
156
  // Match the library's PEM grammar exactly (an uppercase A-Z0-9 label, and ONLY CR/LF/TAB/
80
157
  // space ignored in the body -- not every JS whitespace), so convert is not a looser
81
158
  // validation path than the codecs it composes.
82
- var m = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/.exec(bytes.toString("latin1"));
159
+ var m = PEM_BLOCK_RE.exec(bytes.toString("latin1"));
83
160
  if (!m) return fail(file + ": input is neither a well-formed DER structure nor a PEM block");
84
161
  var b64 = m[2].replace(/[\r\n\t ]+/g, "");
85
162
  // Enforce CANONICAL base64 (RFC 4648 sec. 3.5), matching the library's fail-closed PEM
@@ -91,12 +168,15 @@ function readDer(file) {
91
168
  return { der: der, label: m[1] };
92
169
  }
93
170
 
171
+ var VERSION_USAGE = "usage: pki version\n" +
172
+ " Prints the installed @blamejs/pki version. --version and -v are the same verb.";
94
173
  function cmdVersion() {
95
174
  process.stdout.write("@blamejs/pki v" + pki.version + "\n");
96
175
  }
97
176
 
177
+ var OID_USAGE = "usage: pki oid <dotted|name>";
98
178
  function cmdOid(arg) {
99
- if (!arg) fail("usage: pki oid <dotted|name>");
179
+ usageOrArg(arg, OID_USAGE);
100
180
  if (/^\d+(\.\d+)+$/.test(arg)) {
101
181
  var name = pki.oid.name(arg);
102
182
  process.stdout.write((name || "(unregistered)") + "\n");
@@ -107,8 +187,9 @@ function cmdOid(arg) {
107
187
  }
108
188
  }
109
189
 
190
+ var PARSE_USAGE = "usage: pki parse <cert.pem|cert.der>";
110
191
  function cmdParse(file) {
111
- if (!file) fail("usage: pki parse <cert.pem|cert.der>");
192
+ usageOrArg(file, PARSE_USAGE);
112
193
  var cert;
113
194
  try { cert = pki.schema.x509.parse(readForLib(file)); } catch (e) { return fail(e.code + ": " + e.message); }
114
195
  var view = {
@@ -125,26 +206,40 @@ function cmdParse(file) {
125
206
  process.stdout.write(JSON.stringify(view, null, 2) + "\n");
126
207
  }
127
208
 
128
- // pki inspect <cert> -- the human-readable certificate render (pki.inspect.certificate),
129
- // the pure-JS equivalent of `openssl x509 -text`.
130
- function cmdInspect(file) {
131
- if (!file) fail("usage: pki inspect <cert.pem|cert.der>");
132
- try { process.stdout.write(pki.inspect.certificate(readForLib(file))); }
209
+ // pki inspect <file> [--asn1] -- the human-readable render of whatever the file holds. It routes
210
+ // through pki.inspect.any, which detects the format and picks the report, so every format the
211
+ // library renders is reachable here. --asn1 asks for the structural TLV dump instead, which reads
212
+ // a file of any shape, including one no format detector recognizes.
213
+ var INSPECT_USAGE = "usage: pki inspect <file.pem|file.der> [--asn1]";
214
+ function cmdInspect(args) {
215
+ var file = args._[0];
216
+ if (!file) fail(INSPECT_USAGE);
217
+ try { process.stdout.write(args.asn1 ? pki.inspect.asn1(readForLib(file)) + "\n" : pki.inspect.any(readForLib(file))); }
133
218
  catch (e) { return fail(e.code + ": " + e.message); }
134
219
  }
135
220
 
136
221
  function pad(s, n) { while (s.length < n) s += " "; return s; }
137
222
 
138
- // The lint verb for each structure `pki lint` detects. A file holding anything else (a request, a
139
- // CMS message, a key) has no lint profile and is refused by the name the detector gives it.
140
- var LINT_VERB_FOR = { "x509": "certificate", "crl": "crl", "ocsp-response": "ocsp" };
223
+ // The lint verb for each structure `pki lint` detects, one entry per verb pki.lint ships. A file
224
+ // holding a structure with no lint verb (a key, a PKCS#12 store, an enrollment message) is refused
225
+ // by the name the detector gives it. `ocsp-request` has no verb: RFC 6960's linted profile is the
226
+ // response, and pki.lint.ocsp takes one.
227
+ var LINT_VERB_FOR = {
228
+ "x509": "certificate", "csr": "csr", "crl": "crl", "ocsp-response": "ocsp",
229
+ "cms": "cms", "tsp": "tsp", "attrcert": "attrcert",
230
+ };
231
+ // The usage line names the verbs from the table rather than from a list written beside it, so a
232
+ // verb added above cannot leave the usage naming a set the CLI does not have.
233
+ var LINT_USAGE = "usage: pki lint <" + Object.keys(LINT_VERB_FOR).map(function (k) {
234
+ return LINT_VERB_FOR[k];
235
+ }).join("|") + "> [--profile <name>] [--severity <floor>] [--json]";
141
236
 
142
237
  // pki lint <file> -- detect whether the file holds a certificate, a CRL or an OCSP response and
143
238
  // lint it against that structure's profiles. Prints one line per finding and exits non-zero
144
239
  // when any error/fatal finding is present (0 when the worst is advisory).
145
240
  function cmdLint(args) {
146
241
  var file = args._[0];
147
- if (!file) fail("usage: pki lint <cert|crl|ocsp-response> [--profile <name>] [--severity <floor>] [--json]");
242
+ if (!file) fail(LINT_USAGE);
148
243
  var input = readForLib(file);
149
244
  // Bytes no detector recognizes (garbage, or a structure outside the detector's set) fall
150
245
  // through to the certificate verb, whose never-throw data path reports them as a fatal
@@ -154,7 +249,10 @@ function cmdLint(args) {
154
249
  try { format = pki.schema.detectFormat(input); } catch (_e) { format = null; }
155
250
  if (format !== null) {
156
251
  verb = LINT_VERB_FOR[format];
157
- if (verb === undefined) return fail(file + ": holds a " + format + " structure, and pki lint covers a certificate, CRL, or OCSP response");
252
+ if (verb === undefined) {
253
+ return fail(file + ": holds a " + format + " structure, and pki lint covers " +
254
+ Object.keys(LINT_VERB_FOR).join(", "));
255
+ }
158
256
  }
159
257
  var report;
160
258
  // Config-time misuse (unknown profile / bad severity) throws a typed LintError; the data
@@ -169,16 +267,569 @@ function cmdLint(args) {
169
267
  });
170
268
  process.stdout.write("\n" + (report.findings.length || "no") + " finding(s); worst: " + (report.worst || "pass") + "\n");
171
269
  }
172
- // Set the exit CODE and let Node drain — process.exit() can truncate a buffered stdout
270
+ // Set the exit CODE and let Node drain: process.exit() can truncate a buffered stdout
173
271
  // write to a pipe before it flushes.
174
272
  process.exitCode = (report.counts.error || report.counts.fatal) ? 1 : 0;
175
273
  }
176
274
 
275
+ // The suite `keygen` produces with no --alg. ML-DSA-65 is the FIPS 204 middle level and the suite
276
+ // this toolkit leads with, so the post-quantum choice is the one an operator gets by default rather
277
+ // than the one they have to know to ask for.
278
+ var KEYGEN_DEFAULT_ALG = "ML-DSA-65";
279
+ // Owner-only on the platforms that enforce a file mode. On win32 Node does not apply the mode
280
+ // argument -- measured: a file created with 0o600 reports 0o666 -- so the file inherits the
281
+ // directory's ACL there and the help text says so rather than implying a guarantee.
282
+ var KEY_FILE_MODE = 0o600;
283
+ var KEYGEN_USAGE = "usage: pki keygen --out <key-file> [--pub <spki-file>] [--alg " +
284
+ KEYGEN_DEFAULT_ALG + "] [--pem]\n" +
285
+ " The private key is written to --out and never to stdout. An existing file is never written\n" +
286
+ " over. The file is created owner-only where the platform enforces a file mode; on Windows the\n" +
287
+ " mode is not applied and the file inherits the directory's permissions.";
288
+
289
+ // The files THIS invocation created, so a run that does not complete removes its own residue and
290
+ // nothing else. A pre-existing file is never listed and so is never removed: the failure an operator
291
+ // is most likely to hit is a name already taken, and destroying what is there would be the very thing
292
+ // refusing to write over it prevents.
293
+ //
294
+ // It hangs off `exit` rather than off each failure site because `fail` ends the process where it is
295
+ // called, so there is no scope around the sequence to clean up in. Reserving the public name before
296
+ // writing the private key fixed one residue and created another: a run that failed on `--out` left
297
+ // the empty public file behind and the corrected retry then refused for THAT name instead.
298
+ // Each entry records what the file looked like when this run created it, so cleanup can tell its own
299
+ // file from one that replaced it. Without that, a path swapped between the create and the cleanup had
300
+ // the REPLACEMENT deleted, which turns a tidy-up into data loss. The window is not closed by this,
301
+ // there being no unlink-by-handle to use, but a file whose size or timestamp has changed is left
302
+ // where it is and named.
303
+ var createdFiles = [];
304
+ // Two separate questions, recorded at two different moments.
305
+ //
306
+ // IDENTITY (`ino`/`dev`) is taken from the creating descriptor, at the moment the name becomes ours and
307
+ // before any content goes into it, so the record can tell our file from one that replaced it even when
308
+ // the write that followed never finished. Recorded only once the content was in, a write that failed
309
+ // left an identity-free record, and an identity-free record was deleted with no comparison at all: a
310
+ // replacement arriving in that window was removed. An unidentifiable file is exactly the one not to
311
+ // delete blindly.
312
+ //
313
+ // CONTENT (`size`/`mtimeMs`) is recorded only once a write returns, because that is the only point at
314
+ // which what is on disk is what this run meant to put there. A record with no content figure is a file
315
+ // this run created and did not finish, which is removed when its identity still matches: leaving a
316
+ // partially written private key behind is the residue the record exists to clear.
317
+ //
318
+ // `nlink` is the third: unlinking a path whose content has another link removes the name and keeps the
319
+ // content, so a file linked since this run created it is left where it is and named rather than
320
+ // half-removed. Where a platform reports no inode or no link count the figures compare equal and the
321
+ // size and timestamp carry the comparison alone, which cannot distinguish a replacement that matches
322
+ // both.
323
+ function _claimCreated(file, fd) {
324
+ var rec = { path: file, size: null, mtimeMs: null, ino: null, dev: null, nlink: null };
325
+ _identify(rec, fd);
326
+ createdFiles.push(rec);
327
+ return rec;
328
+ }
329
+ // Both are called with the descriptor still OPEN, so what they record is of the file this run holds
330
+ // rather than of whatever occupies the path afterwards. Taken by path after the close, a writer that
331
+ // replaced the file in between had its replacement's size and timestamp adopted as this run's own, and
332
+ // cleanup then deleted that replacement: the comparison meant to prevent exactly that was reading the
333
+ // wrong file.
334
+ function _identify(rec, fd) {
335
+ try {
336
+ var st = fs.fstatSync(fd);
337
+ rec.ino = st.ino; rec.dev = st.dev; rec.nlink = st.nlink;
338
+ } catch (_e) { /* allow:swallow-unverified unstattable; the record carries no identity and is kept */ }
339
+ }
340
+ function _sealCreated(rec, fd) {
341
+ try {
342
+ var st = fs.fstatSync(fd);
343
+ rec.size = st.size; rec.mtimeMs = st.mtimeMs;
344
+ rec.ino = st.ino; rec.dev = st.dev; rec.nlink = st.nlink;
345
+ } catch (_e) { /* allow:swallow-unverified unstattable; the claim-time identity stands */ }
346
+ }
347
+ // One line about one file, on the stream that reports them. A stream that cannot be written carries no
348
+ // report, and that is the end of it: the file is intact either way, which is the half that matters, and
349
+ // there is nowhere else to say so from inside an exit handler.
350
+ function _say(message) {
351
+ // Through the same escaper every report uses, so a path with a newline in it cannot forge a line
352
+ // here either.
353
+ try { process.stderr.write("pki: " + _oneLine(message) + "\n"); }
354
+ catch (_e) { /* allow:swallow-unverified nothing can be reported; the file is untouched */ }
355
+ }
356
+ process.on("exit", function (code) {
357
+ // `wroteToDisk` means the product is COMPLETE on disk, and then no exit path may remove it. A
358
+ // synchronous throw out of the final status write reaches `fail`, which exits non-zero, and cleanup
359
+ // keyed on the exit code alone deleted a key that had been written exactly as asked.
360
+ if (code === 0 || wroteToDisk) return;
361
+ for (var i = 0; i < createdFiles.length; i++) {
362
+ var rec = createdFiles[i];
363
+ var now = null, statErr = null;
364
+ try { now = fs.statSync(rec.path); }
365
+ catch (e0) { statErr = e0; }
366
+ if (statErr !== null) {
367
+ // Anything but ENOENT means the path could not be looked at, which is not the same as the file
368
+ // being gone: a file nobody could look at is one nobody removed, and that outcome used to be
369
+ // passed over in silence. ENOENT means nothing is there NOW, which this run did not necessarily
370
+ // do: a file it created and something else renamed leaves the same empty path, and the bytes are
371
+ // then somewhere an operator has not been told about.
372
+ if (statErr.code !== "ENOENT") {
373
+ _say(rec.path + " was created by this run and could not be examined, so it was left in " +
374
+ "place (" + statErr.message + ")");
375
+ } else if (rec.ino !== null) {
376
+ _say(rec.path + " was created by this run and is no longer there, so its content is " +
377
+ "wherever it was moved to");
378
+ }
379
+ continue;
380
+ }
381
+ // A different inode under the same name is a different file, whatever its size and timestamp say,
382
+ // so that comparison runs first where the platform reports one.
383
+ var known = rec.ino !== null && rec.ino !== 0 && now.ino !== 0;
384
+ var leave = null;
385
+ if (known && (now.ino !== rec.ino || now.dev !== rec.dev)) {
386
+ leave = " was replaced since this run created it and was left in place";
387
+ } else if (now.nlink > 1) {
388
+ // An exclusive create gives a file ONE name, so a second one was added by somebody else, and
389
+ // unlinking this name would remove the name and leave the content. Asked of what is on disk NOW
390
+ // rather than against the count this run recorded: recorded, a link added before the record was
391
+ // taken and removed afterwards made the count go DOWN, and a comparison looking for growth let
392
+ // the unlink through while another name still held the bytes.
393
+ leave = " has more than one link, so removing this name would leave the content, and it was " +
394
+ "left in place";
395
+ } else if (rec.size !== null && (now.size !== rec.size || now.mtimeMs !== rec.mtimeMs)) {
396
+ leave = " changed since this run created it and was left in place";
397
+ } else if (!known && rec.size === null) {
398
+ // No identity from the platform and no completed write: the file at this path cannot be told
399
+ // from one that replaced it, and deleting on a guess is the one outcome that cannot be undone.
400
+ leave = " was created by this run and cannot be identified on this filesystem, so it was " +
401
+ "left in place and is yours to remove";
402
+ }
403
+ if (leave !== null) { _say(rec.path + leave); continue; }
404
+ try { fs.unlinkSync(rec.path); }
405
+ catch (e) {
406
+ _say(rec.path + " was created by this run and could not be removed (" + e.message + ")");
407
+ }
408
+ }
409
+ });
410
+
411
+ // Write a file that must not exist yet, with its mode set in the call that creates it: a chmod
412
+ // after the write leaves a window where the key is readable by anyone who can open the file.
413
+ // Create a file that must not exist yet and write it through the DESCRIPTOR the create returned. Four
414
+ // rules meet here and each of them was learned the hard way:
415
+ //
416
+ // - the mode goes in the creating call, because a chmod afterwards leaves a window where the key is
417
+ // readable by anyone who can open the file;
418
+ // - only the descriptor is written through. Closing the exclusive create and reopening the PATH let
419
+ // the reservation be replaced in between, and the write would then follow a replacement symlink or
420
+ // truncate a replacement file, which is the no-overwrite guarantee defeating itself;
421
+ // - the claim is recorded BEFORE any content, and it carries the new file's identity from the
422
+ // creating descriptor, so a write that fails part way through still leaves a file the cleanup can
423
+ // recognize as its own and remove;
424
+ // - the descriptor is closed on every path before the cleanup can run, because Windows will not
425
+ // unlink a file something still has open.
426
+ function writeNewFile(file, bytes, mode) {
427
+ var fd;
428
+ try { fd = fs.openSync(file, "wx", mode); }
429
+ catch (e) {
430
+ if (e.code === "EEXIST") return fail(file + " already exists, and a key is never written over one");
431
+ return fail("cannot write " + file + ": " + e.message);
432
+ }
433
+ return _writeThrough(fd, file, bytes);
434
+ }
435
+
436
+ // The content, the claim, the seal and the close, for a descriptor the caller has already created.
437
+ function _writeThrough(fd, file, bytes) {
438
+ var rec = _claimCreated(file, fd);
439
+ var err = null;
440
+ try { fs.writeFileSync(fd, bytes); }
441
+ catch (e2) { err = e2; }
442
+ // Sealed BEFORE the close, while the descriptor still names the file this run wrote.
443
+ if (err === null) _sealCreated(rec, fd);
444
+ try { fs.closeSync(fd); }
445
+ catch (e3) { if (err === null) err = e3; }
446
+ if (err !== null) return fail("cannot write " + file + ": " + err.message);
447
+ }
448
+
449
+ // An output file for the verbs whose product is not a key: `csr`, `issue`, `fetch` and `sign` with
450
+ // `--out`. Overwriting a path that is already taken is what those verbs have always done, and a
451
+ // certificate or a request is not a secret whose only copy gets destroyed, so that stays. What the
452
+ // plain write did not do is let the cleanup know: a write that failed part way left a file this run
453
+ // had just created with nobody tracking it, which is the residue the record exists to clear. So a
454
+ // path that does NOT exist is created exclusively and written through its descriptor like a key is,
455
+ // and one that DOES is overwritten with no record kept, because a file this run did not create is
456
+ // never one it removes.
457
+ function writeOutputFile(file, bytes) {
458
+ // Two attempts, because "the path is taken" and "the path is free" are answers about the moment they
459
+ // were given. The overwrite arm opens the path WITHOUT permission to create it, so a path that stops
460
+ // existing between the two calls comes back as ENOENT and is retried as the exclusive create it now
461
+ // is. Written as a plain write, that window created a file this run owned with nobody tracking it.
462
+ for (var attempt = 0; attempt < 2; attempt++) {
463
+ var fd;
464
+ try { fd = fs.openSync(file, "wx"); }
465
+ catch (e) {
466
+ if (e.code !== "EEXIST") return fail("cannot write " + file + ": " + e.message);
467
+ var efd;
468
+ /** WRITE-ONLY, and without permission to create. A read-write open makes this process its own
469
+ * reader on a FIFO, so the open succeeds with nobody listening and the bytes go into a pipe no
470
+ * one drains: a small result is written, the descriptor closes, the command reports success and
471
+ * the output is gone, while a larger one blocks. A write-only open waits for a reader, which is
472
+ * what a caller naming a FIFO asked for. `O_TRUNC` is NOT set here: it is what made this open
473
+ * EINVAL on Windows, not `O_WRONLY`, and a regular file is truncated through its descriptor
474
+ * below where that is meaningful. */
475
+ try { efd = fs.openSync(file, fs.constants.O_WRONLY); }
476
+ catch (e2) {
477
+ if (e2.code === "ENOENT") continue;
478
+ /** A destination that refuses a write-only open without create is written by name, as every one
479
+ * of these verbs did before, and nothing is tracked for it because this run did not create it
480
+ * and will never remove it. A regular file always takes the descriptor path above. */
481
+ try { fs.writeFileSync(file, bytes); }
482
+ catch (e3) { return fail("cannot write " + file + ": " + e3.message); }
483
+ return undefined;
484
+ }
485
+ return _overwriteThrough(efd, file, bytes);
486
+ }
487
+ return _writeThrough(fd, file, bytes);
488
+ }
489
+ return fail("cannot write " + file + ": the path kept appearing and disappearing while it was " +
490
+ "being opened");
491
+ }
492
+
493
+ // The content of a file that was already there, written through the descriptor the open returned
494
+ // rather than by name, so what is truncated and what is written are the same file. A failure here has
495
+ // destroyed what was there, and the message says so: the caller asked for an overwrite and got a
496
+ // partial one, which is the one outcome a bare "cannot write" would not tell them.
497
+ function _overwriteThrough(fd, file, bytes) {
498
+ var err = null;
499
+ try {
500
+ // Truncated only where there is something to truncate: `ftruncate` refuses a character device or a
501
+ // FIFO, and an operator writes to `/dev/null`. Those hold no previous content for a short write to
502
+ // leave behind, so skipping it there costs nothing.
503
+ if (fs.fstatSync(fd).isFile()) fs.ftruncateSync(fd, 0);
504
+ // `writeFileSync` on the descriptor, not `writeSync`: a single write can return a SHORT count
505
+ // without throwing, and a caller that ignores the count closes a half-written file and reports
506
+ // success. This one owns the loop until every byte is out. The descriptor sits at offset zero and
507
+ // the truncate does not move it, so the bytes land at the front.
508
+ fs.writeFileSync(fd, bytes);
509
+ } catch (e) { err = e; }
510
+ try { fs.closeSync(fd); }
511
+ catch (e2) { if (err === null) err = e2; }
512
+ if (err !== null) {
513
+ return fail("cannot write " + file + ": " + err.message + "; the file that was at that path has " +
514
+ "been overwritten and what is there now is incomplete");
515
+ }
516
+ return undefined;
517
+ }
518
+
519
+ // pki keygen --out <file> -- generate a key pair and write the private key to a file. The public
520
+ // half goes to --pub when asked for, so an operator sending a public key does not have to hand over
521
+ // a file that also holds the private one.
522
+ function cmdKeygen(args) {
523
+ if (!args.out) return fail(KEYGEN_USAGE);
524
+ // One path for both halves would put the private key at the name the public one was asked for. The
525
+ // exclusive create refuses the second write, so the run already failed; refusing it here says why,
526
+ // before a key is generated, rather than reporting the destination as taken by something else.
527
+ if (args.pub && args.pub === args.out) {
528
+ return fail("--out and --pub name the same file, and the public half is not written where the " +
529
+ "private key goes");
530
+ }
531
+ var alg = args.alg || KEYGEN_DEFAULT_ALG;
532
+ return pki.key.generate(alg).then(function (pair) {
533
+ return Promise.all([pki.key.export(pair.privateKey), pki.key.export(pair.publicKey)]);
534
+ }, function (e) {
535
+ return fail("keygen: cannot generate " + alg + ": " + (e.code || e.message));
536
+ }).then(function (both) {
537
+ if (both === undefined) return undefined; // fail() already exited
538
+ var privDer = both[0], pubDer = both[1];
539
+ var priv = args.pem ? pki.schema.pkcs8.pemEncode(privDer, "PRIVATE KEY") : privDer;
540
+ // Both destinations are taken before either key is written. Writing the private half first left a
541
+ // freshly generated secret at --out whenever the public write failed, with the command reporting
542
+ // failure, and the retry an operator would run next then refused because --out existed. The public
543
+ // NAME is claimed first, by the same exclusive create that refuses an existing file, so a
544
+ // collision there is found before any key reaches the disk.
545
+ // The PUBLIC half goes first, which is what secures its name before any private key reaches the
546
+ // disk while still writing each file through its own descriptor. Writing the private key first
547
+ // left a freshly generated secret at --out whenever the public write failed, and reserving the
548
+ // public name as an empty file to be filled later meant reopening a path, which is the race above.
549
+ // A failure on the private key now leaves a complete PUBLIC file, and the exit handler removes it.
550
+ // There is no cleanup written by hand here: every failure exits non-zero and the handler removes
551
+ // what this run created, checking each file is still the one it wrote.
552
+ if (args.pub) writeNewFile(args.pub, args.pem ? _spkiPem(pubDer) : pubDer, undefined);
553
+ writeNewFile(args.out, priv, KEY_FILE_MODE);
554
+ // The product of this verb is the FILE, not the line that says so, so a stdout that cannot be
555
+ // written does not undo a key that is already on disk. See `wroteToDisk` above for what that
556
+ // changes: the message and the exit code, not the key.
557
+ wroteToDisk = true;
558
+ // Through the same escaper the refusals use. A SUCCESS line names the caller's paths too, so a
559
+ // newline in one ended the line early here as well, and this one goes to stdout where a script
560
+ // reading the tool's output is most likely to be parsing it.
561
+ process.stdout.write(_oneLine("wrote " + alg + " private key to " + args.out +
562
+ (args.pub ? " and its public key to " + args.pub : "")) + "\n");
563
+ });
564
+ }
565
+ // The public half has no PEM codec of its own on the schema surface (an SPKI is not a format the
566
+ // parsers detect), so its armor is written here from the same grammar the codecs read.
567
+ function _spkiPem(der) {
568
+ return "-----BEGIN PUBLIC KEY-----\n" +
569
+ der.toString("base64").replace(/(.{1,64})/g, "$1\n") + "-----END PUBLIC KEY-----\n";
570
+ }
571
+
572
+ var CSR_USAGE = "usage: pki csr --key <key.pkcs8> --subject <dn> [--san name,name] [--out <file>] [--pem]\n" +
573
+ " A key path on a command line is visible in the process table to every user on this machine\n" +
574
+ " for as long as the process runs.";
575
+ var ISSUE_USAGE = "usage: pki issue (--key <key.pkcs8> --subject <dn> | --csr <request>)\n" +
576
+ " [--issuer-cert <cert> --issuer-key <key>] [--days N] [--serial HEX]\n" +
577
+ " [--ca] [--san name,name] [--out <file>] [--pem]\n" +
578
+ " [--copy-requested-san] [--ignore-requested-extensions]\n" +
579
+ " With no issuer the certificate is self-signed by --key. An issuer needs both halves: a\n" +
580
+ " certificate with no key, or a key with no certificate, is refused rather than self-signed.\n" +
581
+ " A request asks for extensions, and what a CA issues is its own decision (RFC 2985 sec. 5.4.2),\n" +
582
+ " so a request that asks for any is refused until one of these two says what to do with them:\n" +
583
+ " --copy-requested-san writes the requested subjectAltName into the certificate, and\n" +
584
+ " --ignore-requested-extensions issues without what the request asked for.\n" +
585
+ " A key path on a command line is visible in the process table to every user on this machine\n" +
586
+ " for as long as the process runs.";
587
+ var ISSUE_DEFAULT_DAYS = 365;
588
+
589
+ // --san takes a comma-separated list and the library classifies each name as a DNS name, an
590
+ // address, an address or a URI, so the CLI does not carry a second classifier.
591
+ function sanList(arg) {
592
+ if (!arg) return undefined;
593
+ var names = String(arg).split(",").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
594
+ return names.length ? names : undefined;
595
+ }
596
+
597
+ // A DER or PEM file for a library entry point that takes either.
598
+ function _keyArg(file) { return _asPemOrDer(readFileBytes(file)); }
599
+
600
+ // The GeneralName forms `--copy-requested-san` writes into a certificate, by context tag. A decoded
601
+ // name of one of these carries the value the builder takes for that form: a string for the three
602
+ // text forms, a 4- or 16-octet Buffer for an address. directoryName [4] and otherName [0] are not
603
+ // here, so a request asking for one is named and refused rather than dropped from the copy.
604
+ var SAN_FORM_FOR_TAG = { 1: "rfc822Name", 2: "dNSName", 6: "uniformResourceIdentifier", 7: "iPAddress" };
605
+ // A requested subjectAltName as the names the certificate builder takes. Every name is converted,
606
+ // and a form with no conversion stops the issuance: a certificate carrying SOME of the names a
607
+ // request asked for is a different certificate from the one it asked for, and the operator asked for
608
+ // the copy.
609
+ // Whether the Name the certificate will carry is empty, read the way the certificate builder reads
610
+ // it: an encoded Name holding no RDNs. The builder takes the subjectAltName's criticality from that
611
+ // same answer, so the two agree on which form is being issued.
612
+ function subjectNameIsEmpty(subject) {
613
+ if (!Buffer.isBuffer(subject)) return false;
614
+ try { return pki.asn1.decode(subject).children.length === 0; }
615
+ catch (_e) { return false; }
616
+ }
617
+
618
+ function requestedSanNames(rows, subject) {
619
+ var san = rows.filter(function (r) { return r.name === "subjectAltName"; });
620
+ if (!san.length) return undefined;
621
+ if (san[0].state !== "decoded") {
622
+ return fail("issue: the request's subjectAltName cannot be read (" + (san[0].code || san[0].state) +
623
+ "), so --copy-requested-san has nothing to copy");
624
+ }
625
+ // RFC 5280 sec. 4.2.1.6 asks for a critical subjectAltName where the subject is empty and a
626
+ // non-critical one beside a subject, and the certificate builder writes whichever the subject it
627
+ // encodes calls for. So a critical request is answered as it was made when the subject is empty,
628
+ // which is the only conforming shape for a SAN-only request, and loses its criticality only beside
629
+ // a non-empty subject. That is the case this refuses.
630
+ if (san[0].critical === true && !subjectNameIsEmpty(subject)) {
631
+ return fail("issue: the request asks for a CRITICAL subjectAltName, which RFC 5280 sec. 4.2.1.6 " +
632
+ "asks for only where the subject is empty, and this request carries a subject; beside a " +
633
+ "subject this verb writes a non-critical subjectAltName, so copying it would answer a " +
634
+ "different request than the one that was made. State the names with --san, or build the " +
635
+ "certificate through pki.x509.sign, whose pre-encoded extensions array writes the " +
636
+ "criticality it is given");
637
+ }
638
+ var names = (san[0].decoded && san[0].decoded.names) || [];
639
+ var out = [], unconvertible = [];
640
+ for (var i = 0; i < names.length; i++) {
641
+ var form = Object.prototype.hasOwnProperty.call(SAN_FORM_FOR_TAG, names[i].tagNumber)
642
+ ? SAN_FORM_FOR_TAG[names[i].tagNumber] : null;
643
+ if (form === null) { unconvertible.push("[" + names[i].tagNumber + "]"); continue; }
644
+ var entry = {};
645
+ entry[form] = names[i].value;
646
+ out.push(entry);
647
+ }
648
+ if (unconvertible.length) {
649
+ return fail("issue: the request's subjectAltName holds " + unconvertible.length + " name" +
650
+ (unconvertible.length === 1 ? "" : "s") + " of a form --copy-requested-san does not write (" +
651
+ unconvertible.join(", ") + "); state the names with --san instead");
652
+ }
653
+ if (!out.length) return fail("issue: the request's subjectAltName holds no names");
654
+ return out;
655
+ }
656
+
657
+ // --subject is a distinguished name, parsed as one and handed to the builders as the raw Name DER
658
+ // they accept: they read a bare string as a common name, so `--subject CN=x` would otherwise certify
659
+ // a CN whose value is the text "CN=x". A component with no attribute type is refused by
660
+ // pki.x509.parseDn, whose message names the one at fault.
661
+ function subjectArg(arg) {
662
+ try { return pki.x509.parseDn(arg).bytes; }
663
+ catch (e) { return fail("--subject is a distinguished name such as \"CN=host.example, O=Org\": " + e.message); }
664
+ }
665
+
666
+ function writeOrPrint(args, bytes) {
667
+ if (!args.out) { process.stdout.write(bytes); return; }
668
+ writeOutputFile(args.out, bytes);
669
+ // The product is the FILE here too, for every verb that routes through this: `csr`, `issue` and
670
+ // `fetch` with `--out`. The rule arrived with `keygen` and applied to keygen alone, so a broken
671
+ // stdout made these exit non-zero over a file that was already written, which is the same
672
+ // contradiction one verb further on.
673
+ wroteToDisk = true;
674
+ process.stdout.write(_oneLine("wrote " + args.out) + "\n");
675
+ }
676
+
677
+ // pki csr --key <key> --subject <dn> -- a PKCS#10 certification request over the key's public half,
678
+ // which pki.key.publicFromPrivate derives so the caller supplies one file rather than two.
679
+ function cmdCsr(args) {
680
+ if (!args.key || !args.subject) return fail(CSR_USAGE);
681
+ var key = _keyArg(args.key);
682
+ return pki.key.publicFromPrivate(key).then(function (spki) {
683
+ var spec = { subject: subjectArg(args.subject), subjectPublicKey: spki };
684
+ // A request asks for extensions through the RFC 2985 extensionRequest attribute, which is what
685
+ // a CA copies into the certificate it issues; a request has no `extensions` of its own.
686
+ var san = sanList(args.san);
687
+ if (san) spec.extensionRequest = { subjectAltName: san };
688
+ return pki.csr.sign(spec, { key: key }, { pem: !!args.pem });
689
+ }).then(function (out) {
690
+ writeOrPrint(args, out);
691
+ }, function (e) { return fail((e.code || "csr/sign-error") + ": " + e.message); });
692
+ }
693
+
694
+ // pki issue -- a certificate over either a supplied key or the public half a request carries.
695
+ function cmdIssue(args) {
696
+ var haveKey = !!args.key, haveCsr = !!args.csr;
697
+ if (haveKey === haveCsr) return fail(ISSUE_USAGE);
698
+ if (haveKey && !args.subject) return fail(ISSUE_USAGE);
699
+ // Half an issuer would otherwise take the self-signed path, so a certificate asked to be signed
700
+ // by a CA would come back signed by its own subject key. Refuse instead.
701
+ if (args["issuer-cert"] && !args["issuer-key"]) return fail("issue: --issuer-cert needs --issuer-key");
702
+ if (args["issuer-key"] && !args["issuer-cert"]) return fail("issue: --issuer-key needs --issuer-cert");
703
+ // Both flags answer a question only a request asks, so on the --key route neither has anything to
704
+ // decide, and a flag that silently does nothing reads as one that was honored.
705
+ if (!haveCsr && (args["copy-requested-san"] || args["ignore-requested-extensions"])) {
706
+ return fail("issue: --copy-requested-san and --ignore-requested-extensions answer what a request " +
707
+ "asks for, so they need --csr");
708
+ }
709
+ var days = args.days === undefined ? ISSUE_DEFAULT_DAYS : Number(args.days);
710
+ if (!isFinite(days) || days <= 0 || Math.floor(days) !== days) return fail("issue: --days must be a positive whole number of days");
711
+
712
+ return Promise.resolve().then(function () {
713
+ if (!haveCsr) {
714
+ var key = _keyArg(args.key);
715
+ var subject = subjectArg(args.subject);
716
+ return pki.key.publicFromPrivate(key).then(function (spki) {
717
+ return { subject: subject, spki: spki, signerKey: key };
718
+ });
719
+ }
720
+ // A request whose signature does not verify proves nothing about who holds the key it carries,
721
+ // so it is verified before anything is certified from it.
722
+ var reqBytes = readForLib(args.csr);
723
+ return pki.csr.verify(reqBytes).then(function (v) {
724
+ if (v !== true && !(v && v.valid === true)) return fail("issue: the request's signature does not verify");
725
+ var req = pki.schema.csr.parse(reqBytes);
726
+ // What the request ASKS FOR is read here and answered below. RFC 2985 sec. 5.4.2 leaves to the
727
+ // CA which requested extensions to honor, so neither answer is taken by default: a copy would
728
+ // let the requester write its own names into the certificate, and a drop would issue a
729
+ // certificate the request did not ask for while reporting success.
730
+ var rows = pki.schema.csr.decodeExtensions(reqBytes);
731
+ // Both routes hand `subject` over as raw Name DER, which is one of the three forms the
732
+ // builders take. A parse result is not one of them, and neither is a DN string.
733
+ return { subject: req.subject.bytes, spki: req.subjectPublicKeyInfo.bytes, signerKey: null,
734
+ requested: rows };
735
+ }, function (e) { return fail("issue: the request's signature does not verify (" + (e.code || e.message) + ")"); });
736
+ }).then(function (from) {
737
+ if (from === undefined) return undefined; // fail() already exited
738
+ var now = new Date();
739
+ var spec = {
740
+ subject: from.subject,
741
+ subjectPublicKey: from.spki,
742
+ notBefore: now,
743
+ notAfter: new Date(now.getTime() + pki.C.TIME.days(days)),
744
+ };
745
+ if (args.serial) spec.serialNumber = BigInt("0x" + String(args.serial).replace(/^0x/, ""));
746
+ var exts = {};
747
+ var san = sanList(args.san);
748
+ // Every extension the request asked for has to be answered, and the answer is the operator's.
749
+ // What neither flag accounts for is named and refused: the certificate otherwise comes back
750
+ // reporting success while carrying less than was asked for, which is the case an operator finds
751
+ // when a name it never checked turns out to be missing.
752
+ var requested = (from.requested || []).map(function (r) { return r.name || r.oid; });
753
+ var unanswered = requested.filter(function (n) {
754
+ if (args["ignore-requested-extensions"]) return false;
755
+ return !(n === "subjectAltName" && args["copy-requested-san"]);
756
+ });
757
+ if (unanswered.length) {
758
+ return fail("issue: the request asks for " + unanswered.join(", ") + ", and what a CA issues " +
759
+ "is its own decision (RFC 2985 sec. 5.4.2). Pass --copy-requested-san to write the requested " +
760
+ "subjectAltName, or --ignore-requested-extensions to issue without what the request asked for");
761
+ }
762
+ if (args["copy-requested-san"]) {
763
+ if (san) return fail("issue: --san and --copy-requested-san both name the subjectAltName; pass one");
764
+ san = requestedSanNames(from.requested || [], from.subject);
765
+ if (san === undefined) return fail("issue: --copy-requested-san was passed and the request asks for no subjectAltName");
766
+ }
767
+ if (san) exts.subjectAltName = san;
768
+ if (args.ca) { exts.basicConstraints = { cA: true }; exts.keyUsage = ["keyCertSign", "cRLSign"]; }
769
+ if (Object.keys(exts).length) spec.extensions = exts;
770
+ var issuer = args["issuer-cert"]
771
+ ? { key: _keyArg(args["issuer-key"]), cert: _keyArg(args["issuer-cert"]) }
772
+ : { key: from.signerKey };
773
+ if (issuer.key == null) return fail("issue: --csr needs --issuer-cert and --issuer-key, since a request carries no signing key");
774
+ return pki.x509.sign(spec, issuer, { pem: !!args.pem }).then(function (out) { writeOrPrint(args, out); });
775
+ }).then(undefined, function (e) { return fail((e.code || "x509/sign-error") + ": " + e.message); });
776
+ }
777
+
778
+ var FETCH_USAGE = "usage: pki fetch <https-url> [--anchor <cert>] [--system] [--out <file>] [--der]\n" +
779
+ " Prints the certificate chain the TLS session resolved, leaf first, as PEM. The chain is\n" +
780
+ " completed from the anchors in scope, so an entry above the leaf may come from a configured\n" +
781
+ " anchor rather than from the endpoint; the leaf is the one the endpoint certainly sent.\n" +
782
+ " This is NOT a verification. The TLS handshake checked that the endpoint's chain builds to\n" +
783
+ " a configured anchor and that its name matches the URL; it checked no revocation status, no\n" +
784
+ " policy, and nothing about what the certificate is authorized to do. pki verify is the verb\n" +
785
+ " that validates a path.\n" +
786
+ " An anchor is required: --anchor names one, --system uses the platform's store.\n" +
787
+ " --der writes one certificate. A chain of several is refused under it, several concatenated DER\n" +
788
+ " certificates not being a DER document that pki parse can read back.";
789
+
790
+ // pki fetch <url> -- the chain a live endpoint presents, read from a TLS handshake that sends no
791
+ // request, so nothing reaches the application behind it.
792
+ function cmdFetch(args) {
793
+ var url = args._[0];
794
+ if (!url) return fail(FETCH_USAGE);
795
+ var anchors = args.anchor ? [readFileBytes(args.anchor)] : undefined;
796
+ var tlsOpts = {};
797
+ if (anchors) tlsOpts.anchors = anchors;
798
+ if (args.system) tlsOpts.useSystemStore = true;
799
+ return pki.transport.peerChain({ url: url }, { tls: tlsOpts }).then(function (channel) {
800
+ var chain = channel.peerChain || [];
801
+ if (!chain.length) return fail("fetch: the endpoint presented no certificate");
802
+ if (args.der) {
803
+ // Concatenating several certificates produces several top-level DER values with nothing framing
804
+ // them, which is not a DER document: `pki parse` and `pki convert` reject it with `x509/bad-der`,
805
+ // so the file written was one this tool could not read back. One certificate is a document and is
806
+ // written as it was; more than one is refused, naming the two forms that do hold a chain.
807
+ if (chain.length > 1) {
808
+ return fail("fetch: --der writes one certificate, and this chain has " + chain.length +
809
+ ". Several DER certificates concatenated are not a DER document, so `pki parse` and " +
810
+ "`pki convert` reject the result. Omit --der for the chain as PEM, whose first block is the " +
811
+ "leaf, and convert that one block if you need its DER");
812
+ }
813
+ writeOrPrint(args, chain[0]);
814
+ } else {
815
+ var pem = chain.map(function (d) { return pki.schema.x509.pemEncode(d, "CERTIFICATE"); }).join("");
816
+ writeOrPrint(args, pem);
817
+ }
818
+ // What the handshake established, on stderr so it does not land in a redirected chain file.
819
+ process.stderr.write("pki: " + channel.protocol + " " +
820
+ ((channel.cipher && channel.cipher.name) || "") + "; " + chain.length +
821
+ " certificate(s) in the resolved chain, which may include one completed from a configured " +
822
+ "anchor rather than sent by the endpoint; the handshake checked the chain against the configured anchor " +
823
+ "and the name in the URL, and checked no revocation status or policy -- pki verify validates a path\n");
824
+ }, function (e) { return fail((e.code || "transport/error") + ": " + e.message); });
825
+ }
826
+
177
827
  // pki convert <file> --to der|pem -- transcode between DER and PEM. The input encoding is
178
828
  // auto-detected; the bytes must be well-formed DER (we never wrap/emit garbage).
829
+ var CONVERT_USAGE = "usage: pki convert <file> --to der|pem [--label LABEL]";
179
830
  function cmdConvert(args) {
180
831
  var file = args._[0], to = args.to;
181
- if (!file) fail("usage: pki convert <file> --to der|pem [--label LABEL]");
832
+ if (!file) fail(CONVERT_USAGE);
182
833
  if (to !== "der" && to !== "pem") fail("convert: --to must be 'der' or 'pem'");
183
834
  var input = readDer(file);
184
835
  try { pki.asn1.decode(input.der); } catch (e) { return fail("input is not well-formed DER: " + (e.code || e.message)); }
@@ -193,9 +844,10 @@ function cmdConvert(args) {
193
844
 
194
845
  // pki verify <cert>... --anchor <cert> -- validate an ordered certification path
195
846
  // (anchor->target) against a trust anchor via pki.path.validate (RFC 5280 sec. 6.1).
847
+ var VERIFY_USAGE = "usage: pki verify <cert>... --anchor <anchor-cert> [--time ISO]";
196
848
  function cmdVerify(args) {
197
849
  var certFiles = args._, anchorFile = args.anchor;
198
- if (!certFiles.length || !anchorFile) fail("usage: pki verify <cert>... --anchor <anchor-cert> [--time ISO]");
850
+ if (!certFiles.length || !anchorFile) fail(VERIFY_USAGE);
199
851
  var certs, anchor;
200
852
  try { certs = certFiles.map(function (f) { return pki.schema.x509.parse(readForLib(f)); }); }
201
853
  catch (e) { return fail("cannot parse a path certificate: " + (e.code || e.message)); }
@@ -221,17 +873,17 @@ function cmdVerify(args) {
221
873
  // pki sign <content-file> --cert <cert> --key <key> -- produce a CMS SignedData over the file
222
874
  // via pki.cms.sign. The signer key is a PKCS#8 DER or PEM private key; the certificate is DER or
223
875
  // PEM. Output is a DER Buffer (or a PEM block with --pem) to --out or stdout.
876
+ var SIGN_USAGE = "usage: pki sign <content-file> --cert <cert> --key <key.pkcs8> [--detached] [--pss] " +
877
+ "[--digest sha256|sha384|sha512] [--pem] [--out <file>]";
224
878
  function cmdSign(args) {
225
879
  var contentFile = args._[0];
226
- if (!contentFile || !args.cert || !args.key) {
227
- return fail("usage: pki sign <content-file> --cert <cert> --key <key.pkcs8> [--detached] [--pss] [--digest sha256|sha384|sha512] [--pem] [--out <file>]");
228
- }
880
+ if (!contentFile || !args.cert || !args.key) return fail(SIGN_USAGE);
229
881
  var content = readFileBytes(contentFile);
230
882
  var signer = { cert: _asPemOrDer(readFileBytes(args.cert)), key: _asPemOrDer(readFileBytes(args.key)) };
231
883
  if (args.pss) signer.pss = true;
232
884
  if (args.digest) signer.digestAlgorithm = args.digest;
233
885
  return pki.cms.sign(content, signer, { detached: !!args.detached, pem: !!args.pem }).then(function (out) {
234
- if (args.out) { try { fs.writeFileSync(args.out, out); } catch (e) { return fail("cannot write " + args.out + ": " + e.message); } }
886
+ if (args.out) { writeOutputFile(args.out, out); }
235
887
  else { process.stdout.write(out); } // process.exitCode (0) lets Node flush a piped stdout
236
888
  }, function (e) { return fail((e.code || "cms/sign-error") + ": " + e.message); });
237
889
  }
@@ -239,15 +891,49 @@ function cmdSign(args) {
239
891
  // Buffer. cms.sign accepts either for the certificate and needs a string for a PEM private key.
240
892
  function _asPemOrDer(buf) { return buf[0] === 0x2d ? buf.toString("latin1") : buf; }
241
893
 
242
- var USAGE = "usage: pki <version|oid|parse|inspect|lint|convert|verify|sign> [args]\n";
894
+ var USAGE = "usage: pki <version|oid|parse|inspect|keygen|csr|issue|fetch|lint|convert|verify|sign> [args]\n";
895
+
896
+ // The usage line each verb prints, by verb. `main` answers `pki <verb> --help` from this table, so
897
+ // every verb answers help the way `pki --help` does: on stdout, with a zero exit code. A verb reached
898
+ // help through its own missing-argument check before, which calls `fail`, and a script could not tell
899
+ // `pki csr --help` from a csr that failed to run.
900
+ var VERB_USAGE = {
901
+ version: VERSION_USAGE, "--version": VERSION_USAGE, "-v": VERSION_USAGE,
902
+ oid: OID_USAGE, parse: PARSE_USAGE, inspect: INSPECT_USAGE, keygen: KEYGEN_USAGE,
903
+ csr: CSR_USAGE, issue: ISSUE_USAGE, fetch: FETCH_USAGE, lint: LINT_USAGE,
904
+ convert: CONVERT_USAGE, verify: VERIFY_USAGE, sign: SIGN_USAGE,
905
+ };
906
+ // Own properties only, so a verb name that happens to be a name on Object.prototype does not read as
907
+ // a verb the table carries.
908
+ function _usageFor(cmd) {
909
+ return Object.prototype.hasOwnProperty.call(VERB_USAGE, cmd) ? VERB_USAGE[cmd] : null;
910
+ }
911
+ // Asked of the operator's own arguments, not of the parsed flags: `parseArgs` fails on a value flag
912
+ // whose value is missing, so `pki csr --key --help` would exit non-zero before anything looked for
913
+ // help, and `pki oid` never parses its argument at all.
914
+ function _asksHelp(rest) {
915
+ for (var i = 0; i < rest.length; i++) if (rest[i] === "--help" || rest[i] === "-h") return true;
916
+ return false;
917
+ }
243
918
 
244
919
  function main(argv) {
245
920
  var cmd = argv[0];
921
+ var verbUsage = _usageFor(cmd);
922
+ if (verbUsage !== null && _asksHelp(argv.slice(1))) {
923
+ // Written, not exited through: `process.exit` can truncate a buffered stdout write to a pipe, and
924
+ // the exit code is already 0.
925
+ process.stdout.write(verbUsage.charAt(verbUsage.length - 1) === "\n" ? verbUsage : verbUsage + "\n");
926
+ return;
927
+ }
246
928
  switch (cmd) {
247
929
  case "version": case "--version": case "-v": return cmdVersion();
248
930
  case "oid": return cmdOid(argv[1]);
249
931
  case "parse": return cmdParse(argv[1]);
250
- case "inspect": return cmdInspect(argv[1]);
932
+ case "inspect": return cmdInspect(parseArgs(argv.slice(1)));
933
+ case "keygen": return cmdKeygen(parseArgs(argv.slice(1)));
934
+ case "csr": return cmdCsr(parseArgs(argv.slice(1)));
935
+ case "issue": return cmdIssue(parseArgs(argv.slice(1)));
936
+ case "fetch": return cmdFetch(parseArgs(argv.slice(1)));
251
937
  case "lint": return cmdLint(parseArgs(argv.slice(1)));
252
938
  case "convert": return cmdConvert(parseArgs(argv.slice(1)));
253
939
  case "verify": return cmdVerify(parseArgs(argv.slice(1)));