@blamejs/pki 0.8.7 → 0.8.10

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/key.js CHANGED
@@ -25,6 +25,8 @@ var constants = require("./constants");
25
25
  var oid = require("./oid");
26
26
  var pbes2 = require("./pbes2");
27
27
  var pkcs8 = require("./schema-pkcs8");
28
+ var schemaPkcs1 = require("./schema-pkcs1");
29
+ var schemaSec1 = require("./schema-sec1");
28
30
  var pkix = require("./schema-pkix");
29
31
  var schema = require("./schema-engine");
30
32
  var webcrypto = require("./webcrypto");
@@ -64,6 +66,60 @@ var INFER_ALG = _create(null);
64
66
  function _isCryptoKey(x) { return webcrypto.isCryptoKeyLike(x); }
65
67
  function _algName(a) { return typeof a === "string" ? a : (a && a.name); }
66
68
 
69
+ /** @internal The curve each SEC1 registry name is known by in WebCrypto, which names its curves
70
+ * the way RFC 5480 sec. 2.1.1.1 writes them and not the way the ASN.1 registry does. A curve with
71
+ * no row is one the engine cannot take, and saying so names the curve. */
72
+ var WEBCRYPTO_CURVE = _assign(_create(null), {
73
+ "prime256v1": "P-256", "secp384r1": "P-384", "secp521r1": "P-521",
74
+ });
75
+
76
+ /** @internal The byte doors the two named formats come in through. Each keeps its own PEM label,
77
+ * so an `EC PRIVATE KEY` block handed in as `pkcs1` is refused by label before it is read. */
78
+ var _PKCS1_ENTRY = _assign(_create(null), { pemLabel: "RSA PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
79
+ var _SEC1_ENTRY = _assign(_create(null), { pemLabel: "EC PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
80
+ var _PKCS1_PUBLIC_ENTRY = _assign(_create(null), { pemLabel: "RSA PUBLIC KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
81
+
82
+ /** @internal RFC 8017 App. A.1 gives the PKCS#1 encoding two structures, `RSAPrivateKey` and
83
+ * `RSAPublicKey`, with an armor label each. `opts.format` names the encoding, so both labels are
84
+ * that encoding: the one on the armor says which structure to expect, and the structure the bytes
85
+ * carry has to agree, so a key armored under the label of the half it is not is refused instead of
86
+ * being read as the other half. Bare DER carries no label and is classified by its structure. */
87
+ function _pkcs1Der(input) {
88
+ var armored = typeof input === "string" ||
89
+ (guard.bytes.isByteSource(input) && pkix.isPemArmor(guard.bytes.source(input, KeyError, "key/bad-input", "import")));
90
+ if (!armored) return pkix.coerceToDer(input, _PKCS1_ENTRY);
91
+ var der, saidPublic;
92
+ try { der = pkix.coerceToDer(input, _PKCS1_ENTRY); saidPublic = false; }
93
+ catch (e) {
94
+ if (!e || e.code !== "pem/label-mismatch") throw e;
95
+ der = pkix.coerceToDer(input, _PKCS1_PUBLIC_ENTRY);
96
+ saidPublic = true;
97
+ }
98
+ if (_looksLikeRsaPublic(der) !== saidPublic) {
99
+ throw _err("key/bad-input", "the PEM label says this is an RSA " + (saidPublic ? "PUBLIC" : "PRIVATE") +
100
+ " KEY and the bytes carry the other half (RFC 8017 App. A.1)");
101
+ }
102
+ return der;
103
+ }
104
+
105
+ /** @internal PKCS#1 and SEC1 both carry a key and no algorithm identifier, so importing one means
106
+ * wrapping it in the structure that does. RFC 5958 sec. 2 for the private half, RFC 5280
107
+ * sec. 4.1.2.7 for the public one. The inner bytes are carried through unchanged: what the caller
108
+ * handed in is what the engine reads. */
109
+ function _pkcs1ToPkcs8(der) {
110
+ return b.sequence([b.integer(0n),
111
+ b.sequence([b.oid(O("rsaEncryption")), b.nullValue()]),
112
+ b.octetString(der)]);
113
+ }
114
+ function _pkcs1ToSpki(der) {
115
+ return b.sequence([b.sequence([b.oid(O("rsaEncryption")), b.nullValue()]), b.bitString(der, 0)]);
116
+ }
117
+ function _sec1ToPkcs8(der, curveOid) {
118
+ return b.sequence([b.integer(0n),
119
+ b.sequence([b.oid(O("ecPublicKey")), b.oid(curveOid)]),
120
+ b.octetString(der)]);
121
+ }
122
+
67
123
  async function _toPrivateKeyDer(input) {
68
124
  if (_isCryptoKey(input)) {
69
125
  if (input.type !== "private") throw _err("key/bad-input", "a private CryptoKey is required (got a " + input.type + " key)");
@@ -210,8 +266,15 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
210
266
  * A key created non-extractable is refused with that as the reason,
211
267
  * as is one whose implementation keeps its material out of this process's reach entirely.
212
268
  *
269
+ * `format` also writes the encodings an operator's other tools read: `pkcs1` for the RFC 8017 App. A.1
270
+ * `RSAPrivateKey` or `RSAPublicKey`, `sec1` for the RFC 5915 `ECPrivateKey`, and `jwk` for an RFC 7517
271
+ * JSON Web Key. The two DER forms are pulled out of the wrapper the engine wrote, so the inner bytes are
272
+ * the engine's own; a SEC1 key is given the `[0] parameters` field the standalone form needs, which
273
+ * RFC 5915 sec. 3 omits while the key sits inside a PKCS#8. A key whose algorithm the encoding does not
274
+ * describe, an Ed25519 key asked for as PKCS#1 for instance, is refused with `key/bad-input`.
275
+ *
213
276
  * @opts
214
- * - `format` (string) -- `der` (default) or `pem`.
277
+ * - `format` (string) -- `der` (default), `pem`, `pkcs1`, `sec1` or `jwk`.
215
278
  * - `label` (string) -- the PEM label (defaults `PRIVATE KEY` / `PUBLIC KEY` by key type).
216
279
  * @example
217
280
  * var pair = await pki.key.generate("Ed25519");
@@ -229,11 +292,21 @@ async function export_(key, opts) {
229
292
  if (key.type === "private") defaultLabel = "PRIVATE KEY";
230
293
  else if (key.type === "public") defaultLabel = "PUBLIC KEY";
231
294
  else throw _err("key/bad-input", "export supports asymmetric (private / public) CryptoKeys only");
232
- var der = await webcrypto.exportAnyKey(key, _err, "key/bad-input");
233
295
  var fmt = opts.format || "der";
296
+ if (fmt === "jwk") {
297
+ try {
298
+ return await webcrypto.exportAnyKey(key, _err, "key/bad-input", "jwk");
299
+ } catch (e) {
300
+ if (e && e.isPkiError) throw e;
301
+ throw _err("key/bad-input", "exporting the key as a JWK failed", e);
302
+ }
303
+ }
304
+ var der = await webcrypto.exportAnyKey(key, _err, "key/bad-input");
234
305
  if (fmt === "der") return der;
235
306
  if (fmt === "pem") return pkix.pemEncode(der, opts.label || defaultLabel, PemError);
236
- throw _err("key/bad-input", "unsupported format " + guard.text.showValue(opts.format) + " (der / pem)");
307
+ if (fmt === "pkcs1") return _toPkcs1(der, key);
308
+ if (fmt === "sec1") return _toSec1(der, key);
309
+ throw _err("key/bad-input", "unsupported format " + guard.text.showValue(opts.format) + " (der / pem / pkcs1 / sec1 / jwk)");
237
310
  }
238
311
 
239
312
  /**
@@ -250,8 +323,16 @@ async function export_(key, opts) {
250
323
  * SLH-DSA); RSA and EC are ambiguous between signing and key agreement, so `opts.algorithm` must be supplied
251
324
  * for them (without it the import fails closed). Default key usages follow the algorithm and key type.
252
325
  *
326
+ * `opts.format` reads an encoding that is not PKCS#8 or SubjectPublicKeyInfo: `pkcs1` for the RFC 8017
327
+ * App. A.1 `RSA PRIVATE KEY` and `RSA PUBLIC KEY`, `sec1` for the RFC 5915 `EC PRIVATE KEY`, and `jwk`
328
+ * for an RFC 7517 JSON Web Key. Neither DER encoding names an algorithm, so `opts.algorithm` is required
329
+ * with both and nothing is inferred; the key is wrapped in the structure that does name one and handed to
330
+ * the engine with its bytes unchanged. A multi-prime RSA key reads as a structure through
331
+ * `pki.schema.pkcs1.parse` and is refused here with `key/unsupported-key`.
332
+ *
253
333
  * @opts
254
- * - `algorithm` (string | object) -- the WebCrypto algorithm (required for RSA / EC; overrides inference).
334
+ * - `algorithm` (string | object) -- the WebCrypto algorithm (required for RSA / EC / pkcs1 / sec1 / jwk; overrides inference).
335
+ * - `format` (string) -- `pkcs1`, `sec1` or `jwk`; omit to read PKCS#8 or SubjectPublicKeyInfo.
255
336
  * - `usages` (string[]) -- key usages (default derived from the algorithm and public/private type).
256
337
  * - `extractable` (boolean) -- default false.
257
338
  * - `password` (string | Buffer) -- decrypt an `ENCRYPTED PRIVATE KEY` first.
@@ -260,12 +341,26 @@ async function export_(key, opts) {
260
341
  * var spki = await pki.key.export(pair.publicKey);
261
342
  * var pub = await pki.key.import(spki); // Ed25519 -- algorithm inferred
262
343
  */
263
- var _IMPORT_OPTS = _assign(_create(null), { algorithm: 1, extractable: 1, password: 1, usages: 1 });
344
+ var _IMPORT_OPTS = _assign(_create(null), { algorithm: 1, extractable: 1, password: 1, usages: 1, format: 1 });
345
+ var _IMPORT_FORMATS = _assign(_create(null), { pkcs1: 1, sec1: 1, jwk: 1 });
346
+
347
+ /** @internal Which named encoding the caller asked for, or null when they named none and the door
348
+ * detects PKCS#8 or SubjectPublicKeyInfo as it always has. A value outside the set is refused
349
+ * here, before any byte of the input is read. */
350
+ function _namedImportFormat(value) {
351
+ if (value == null) return null;
352
+ var name = typeof value === "string" ? guard.text.keyOf(value) : null;
353
+ if (name === null || _IMPORT_FORMATS[name] !== 1) {
354
+ throw _err("key/bad-input", "opts.format is pkcs1, sec1 or jwk; leave it out to read PKCS#8 or SubjectPublicKeyInfo. Got " + guard.text.showValue(value));
355
+ }
356
+ return name;
357
+ }
264
358
 
265
359
  async function import_(input, opts) {
266
360
  opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.import options");
267
361
  guard.identifier.assertKnownKeys(opts, _IMPORT_OPTS, _err, "key/bad-input",
268
362
  "pki.key.import has an unknown option: ");
363
+ if (_namedImportFormat(opts.format) !== null) return await _importNamedFormat(input, opts);
269
364
  var detected = _detectKeyInput(input);
270
365
  if (detected.format === "encrypted") {
271
366
  if (opts.password == null) throw _err("key/bad-input", "an ENCRYPTED PRIVATE KEY requires opts.password to import");
@@ -1014,6 +1109,156 @@ function _inferAlgorithm(detected) {
1014
1109
  " (RSA and EC are ambiguous between signing and key agreement -- pass opts.algorithm)");
1015
1110
  }
1016
1111
 
1112
+ /** @internal Import from an encoding the caller named. PKCS#1 and SEC1 carry no algorithm, so
1113
+ * `opts.algorithm` is required for both and there is no inference to fall back on. JWK carries its
1114
+ * own key type and goes to the engine as it arrived. */
1115
+ async function _importNamedFormat(input, opts) {
1116
+ var fmt = guard.text.keyOf(opts.format);
1117
+ var extractable = opts.extractable != null ? opts.extractable : false;
1118
+ if (fmt === "jwk") {
1119
+ if (input === null || typeof input !== "object" || _isArray(input)) {
1120
+ throw _err("key/bad-input", "a JWK is a JSON object (RFC 7517 sec. 4)");
1121
+ }
1122
+ if (typeof input.kty !== "string" || input.kty === "") {
1123
+ throw _err("key/bad-input", "a JWK carries a kty naming its key type (RFC 7517 sec. 4.1)");
1124
+ }
1125
+ if (opts.algorithm == null) throw _err("key/bad-input", "importing a JWK needs opts.algorithm");
1126
+ var jwkPublic = input.d === undefined && input.priv === undefined;
1127
+ var jwkUsages = opts.usages || _importUsages(_algName(opts.algorithm), jwkPublic);
1128
+ try {
1129
+ return await subtle.importKey("jwk", input, opts.algorithm, extractable, jwkUsages);
1130
+ } catch (e) {
1131
+ if (e && e.isPkiError) throw e;
1132
+ throw _err("key/bad-input", "importing the JWK failed", e);
1133
+ }
1134
+ }
1135
+
1136
+ if (opts.algorithm == null) {
1137
+ throw _err("key/bad-input", "a " + (fmt === "pkcs1" ? "PKCS#1" : "SEC1") +
1138
+ " key names no algorithm (RFC 8017 App. A.1, RFC 5915 sec. 3), so opts.algorithm says what it is");
1139
+ }
1140
+ /** @internal `opts.format` names the ENCODING, and RFC 8017 App. A.1 gives that encoding two
1141
+ * structures with an armor label each. So both labels are read, and the structure the bytes
1142
+ * carry decides which half arrived; a label naming the half the bytes are not is refused
1143
+ * rather than read as the other. SEC1 has one structure and keeps its single label. */
1144
+ var der;
1145
+ if (fmt === "pkcs1") {
1146
+ der = _pkcs1Der(input);
1147
+ } else {
1148
+ der = pkix.coerceToDer(input, _SEC1_ENTRY);
1149
+ }
1150
+ var wrapped, isPublic = false;
1151
+ if (fmt === "pkcs1") {
1152
+ var isPub = _looksLikeRsaPublic(der);
1153
+ isPublic = isPub;
1154
+ if (isPub) {
1155
+ schemaPkcs1.parsePublic(der);
1156
+ wrapped = _pkcs1ToSpki(der);
1157
+ } else {
1158
+ var rsa = schemaPkcs1.parse(der);
1159
+ if (rsa.otherPrimeInfos.length > 0) {
1160
+ throw _err("key/unsupported-key", "a multi-prime RSA key is readable as a structure and has no arm in this toolkit's crypto engine (RFC 8017 App. A.1.2)");
1161
+ }
1162
+ wrapped = _pkcs1ToPkcs8(der);
1163
+ }
1164
+ } else {
1165
+ var ec = schemaSec1.parse(der);
1166
+ var named = WEBCRYPTO_CURVE[ec.curve];
1167
+ if (named === undefined) {
1168
+ throw _err("key/unsupported-key", "the curve " + ec.curve + " has no WebCrypto name in this toolkit");
1169
+ }
1170
+ var wanted = opts.algorithm && opts.algorithm.namedCurve;
1171
+ if (wanted != null && wanted !== named) {
1172
+ throw _err("key/bad-input", "the key names the curve " + named + " and opts.algorithm names " + guard.text.showValue(wanted));
1173
+ }
1174
+ wrapped = _sec1ToPkcs8(der, ec.curveOid);
1175
+ }
1176
+ var usages = opts.usages || _importUsages(_algName(opts.algorithm), isPublic);
1177
+ try {
1178
+ return await subtle.importKey(isPublic ? "spki" : "pkcs8", wrapped, opts.algorithm, extractable, usages);
1179
+ } catch (e) {
1180
+ if (e && e.isPkiError) throw e;
1181
+ throw _err("key/bad-input", "importKey failed", e);
1182
+ }
1183
+ }
1184
+
1185
+ /** @internal The inverse of the wrap: pull the inner key out of the PKCS#8 or SPKI the engine
1186
+ * wrote. The bytes come back as they went in, since the engine carries the inner structure
1187
+ * unchanged and this reads it rather than re-encoding it. */
1188
+ function _toPkcs1(der, key) {
1189
+ var algOid = _wrappedAlgorithmOid(der, key);
1190
+ if (algOid !== O("rsaEncryption")) {
1191
+ throw _err("key/bad-input", "PKCS#1 is an RSA encoding (RFC 8017 App. A.1) and this key is " + _algName(key.algorithm));
1192
+ }
1193
+ var node = asn1.decode(der);
1194
+ if (key.type === "public") {
1195
+ var bits = asn1.read.bitString(node.children[1]);
1196
+ if (bits.unusedBits !== 0) throw _err("key/bad-input", "the exported public key does not hold whole octets");
1197
+ schemaPkcs1.parsePublic(bits.bytes);
1198
+ return bits.bytes;
1199
+ }
1200
+ var inner = asn1.read.octetString(node.children[2]);
1201
+ schemaPkcs1.parse(inner);
1202
+ return inner;
1203
+ }
1204
+ function _toSec1(der, key) {
1205
+ var algOid = _wrappedAlgorithmOid(der, key);
1206
+ if (algOid !== O("ecPublicKey")) {
1207
+ throw _err("key/bad-input", "SEC1 is an elliptic-curve private-key encoding (RFC 5915 sec. 3) and this key is " + _algName(key.algorithm));
1208
+ }
1209
+ if (key.type !== "private") {
1210
+ throw _err("key/bad-input", "SEC1 encodes a private key; export the public half as SubjectPublicKeyInfo");
1211
+ }
1212
+ var node = asn1.decode(der);
1213
+ var inner = asn1.read.octetString(node.children[2]);
1214
+ /** @internal RFC 5915 sec. 3 says the [0] parameters field SHOULD be omitted when the key sits
1215
+ * inside a PKCS#8, whose AlgorithmIdentifier already names the curve, and SHOULD be present when
1216
+ * the key stands alone. So the curve is carried across from the wrapper here: without it the
1217
+ * standalone key would not say which curve its scalar belongs to. */
1218
+ var curveOid = asn1.read.oid(node.children[1].children[1]);
1219
+ var innerNode = asn1.decode(inner);
1220
+ var hasParams = false;
1221
+ for (var i = 0; i < innerNode.children.length; i++) {
1222
+ if (innerNode.children[i].tagClass === "context" && innerNode.children[i].tagNumber === 0) hasParams = true;
1223
+ }
1224
+ if (!hasParams) {
1225
+ var stored;
1226
+ for (var j = 0; j < innerNode.children.length; j++) {
1227
+ var c = innerNode.children[j];
1228
+ if (c.tagClass === "context" && c.tagNumber === 1) stored = asn1.read.bitString(c.children[0]);
1229
+ }
1230
+ inner = schemaSec1.encode({
1231
+ version: 1,
1232
+ privateKey: asn1.read.octetString(innerNode.children[1]),
1233
+ curveOid: curveOid,
1234
+ publicKey: stored === undefined ? null : stored.bytes,
1235
+ });
1236
+ }
1237
+ schemaSec1.parse(inner);
1238
+ return inner;
1239
+ }
1240
+
1241
+ /** @internal The algorithm the wrapper names, read from whichever wrapper the key type produced:
1242
+ * a PrivateKeyInfo names it second, a SubjectPublicKeyInfo first. */
1243
+ function _wrappedAlgorithmOid(der, key) {
1244
+ var node;
1245
+ try { node = asn1.decode(der); } catch (e) { throw _err("key/bad-input", "the exported key is not DER", e); }
1246
+ var algSeq = key.type === "public" ? node.children[0] : node.children[1];
1247
+ if (!algSeq || !algSeq.children || algSeq.children.length === 0) {
1248
+ throw _err("key/bad-input", "the exported key names no algorithm");
1249
+ }
1250
+ return asn1.read.oid(algSeq.children[0]);
1251
+ }
1252
+
1253
+ /** @internal An RSAPublicKey is two INTEGERs and an RSAPrivateKey is nine or ten, so the two
1254
+ * encodings are told apart by counting the children of the top-level SEQUENCE. Neither door reads
1255
+ * the other's structure, and the count decides which one this is before either runs. */
1256
+ function _looksLikeRsaPublic(der) {
1257
+ var node;
1258
+ try { node = asn1.decode(der); } catch (e) { throw _err("key/bad-input", "the input is not DER", e); }
1259
+ return !!(node && node.children && node.children.length === 2);
1260
+ }
1261
+
1017
1262
  function _importUsages(name, isPublic) {
1018
1263
  var n = _toUpperCase(_String(name == null ? "" : name));
1019
1264
  if (n === "X25519" || n === "X448" || n === "ECDH") return isPublic ? [] : ["deriveBits", "deriveKey"];
package/lib/ocsp.js CHANGED
@@ -39,6 +39,7 @@ var subtle = webcrypto.webcrypto.subtle;
39
39
  var guard = require("./guard-all");
40
40
  var constants = require("./constants");
41
41
  var frameworkError = require("./framework-error");
42
+ var E = _err;
42
43
 
43
44
  var OcspError = frameworkError.OcspError;
44
45
  var b = asn1.build;
@@ -651,30 +652,21 @@ function verify(response, opts) {
651
652
  issuerCert = _certOf(opts.issuer, "the issuer certificate");
652
653
  time = opts.time == null ? new Date() : _asDate(opts.time);
653
654
  } catch (e) { return Promise.reject(e); }
654
- return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time, { historicalMode: opts.historicalMode === true }).then(function (verdict) {
655
- var out;
656
- if (opts.requestNonce == null) {
657
- out = guard.verdict.of(verdict, { nonceMatched: null });
658
- } else {
659
- var respNonce = _responseNonce(parsed);
660
- var reqNonce = guard.bytes.isByteSource(opts.requestNonce)
661
- ? guard.bytes.snapshotSource(opts.requestNonce, OcspError, "ocsp/bad-input", "opts.requestNonce") : null;
662
- var matched = respNonce != null && reqNonce != null && guard.crypto.constantTimeEqual(respNonce, reqNonce);
663
- out = guard.verdict.of(verdict, { nonceMatched: matched });
664
- if (!matched && out.status === "good") {
665
- out = guard.verdict.of(out, { status: "unknown", reason: "the OCSP response nonce does not echo the request nonce (RFC 9654)" });
666
- }
667
- }
668
- return guard.verdict.of(out, { valid: out.status === "good" });
655
+ /** @internal The nonce rule is applied by the acceptance gate rather than restated here, so this
656
+ * door and every other consumer of that gate answer alike. The bytes are snapshotted here so a
657
+ * fault in them carries this door's own code; a value that is not bytes is passed through for the
658
+ * gate to fail closed on, which is what a caller asking for a nonce with something that cannot be
659
+ * one has earned. */
660
+ var reqNonce = opts.requestNonce;
661
+ if (guard.bytes.isByteSource(reqNonce)) {
662
+ try { reqNonce = guard.bytes.snapshotSource(reqNonce, OcspError, "ocsp/bad-input", "opts.requestNonce"); }
663
+ catch (e) { return Promise.reject(e); }
664
+ }
665
+ return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time,
666
+ { historicalMode: opts.historicalMode === true, requestNonce: reqNonce }).then(function (verdict) {
667
+ return guard.verdict.of(verdict, { valid: verdict.status === "good" });
669
668
  });
670
669
  }
671
- function _responseNonce(parsed) {
672
- var br = parsed && parsed.basicResponse;
673
- var exts = (br && br.responseExtensions) || [];
674
- for (var i = 0; i < exts.length; i++) if (exts[i].oid === OID_OCSP_NONCE) return exts[i].nonce || null;
675
- return null;
676
- }
677
-
678
670
  var _VERIFY_REQUEST_OPTS = Object.assign(Object.create(null), { certs: 1 });
679
671
  function _requestFromBytes(request) {
680
672
  if (guard.bytes.isByteSource(request)) request = guard.bytes.snapshotSource(request, OcspError, "ocsp/bad-input", "the OCSP request");
@@ -828,8 +820,119 @@ async function verifyRequest(request, opts) {
828
820
  "the OCSP request signature does not verify over tbsRequest under any opts.certs entry");
829
821
  }
830
822
 
823
+ /** @internal RFC 5019 sec. 5 measures the GET decision over "the scheme and delimiters (http://),
824
+ * server name and base64-encoded OCSPRequest structure", which is the whole encoded URL. */
825
+ var HTTP_GET_MAX_URL_BYTES = 255;
826
+
827
+ /**
828
+ * @primitive pki.ocsp.httpRequest
829
+ * @signature pki.ocsp.httpRequest(der, responderUrl, opts?) -> { method, url, headers, body }
830
+ * @since 0.8.8
831
+ * @status stable
832
+ * @spec RFC 6960, RFC 5019
833
+ * @related pki.ocsp.buildRequest, pki.path.fetchingChecker
834
+ *
835
+ * Shape a DER `OCSPRequest` into the HTTP request RFC 6960 Appendix A.1 defines, without
836
+ * opening a socket: the result is the method, the URL, the headers and the body a caller
837
+ * hands to its own transport.
838
+ *
839
+ * A GET carries the request in the URL, as the base64 of the DER percent-encoded into one
840
+ * path segment appended to the responder URI. RFC 5019 sec. 5 requires GET when the whole
841
+ * encoded URL is 255 bytes or fewer, counting the scheme, the delimiters, the server name and
842
+ * the encoded request, and that is what is measured; anything longer is a POST carrying
843
+ * `Content-Type: application/ocsp-request` and the DER as its body. The base64 carries no CR
844
+ * and no LF, which that section also requires.
845
+ *
846
+ * The encoding escapes every byte outside RFC 3986 sec. 2.3's unreserved set, so the `+`, `/`
847
+ * and `=` of base64 become triplets and a request cannot reach past the segment it was placed
848
+ * in. A responder URL carrying a query or a fragment is refused, since a path segment appended
849
+ * after either would not be part of the path.
850
+ *
851
+ * @opts method `"GET"` or `"POST"` to override the sec. 5 measurement. A GET asked for on a
852
+ * request too long to carry that way is refused rather than truncated.
853
+ * @example
854
+ * var ca = await pki.key.generate("Ed25519");
855
+ * var caDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(ca.publicKey),
856
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
857
+ * extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } },
858
+ * { key: await pki.key.export(ca.privateKey) });
859
+ * var req = await pki.ocsp.buildRequest({ cert: caDer, issuer: caDer }, { profile: "lightweight" });
860
+ * var http = pki.ocsp.httpRequest(req, "http://ocsp.example/");
861
+ * console.log(http.method, http.url.length <= 255);
862
+ */
863
+ function httpRequest(der, responderUrl, opts) {
864
+ opts = guard.identifier.optionsObject(opts, E, "ocsp/bad-input", "httpRequest: opts");
865
+ guard.identifier.assertKnownKeys(opts, HTTP_REQUEST_OPTS, E, "ocsp/bad-input",
866
+ "pki.ocsp.httpRequest has an unknown option (the only option is `method`). The unknown option was: ");
867
+ var body = guard.bytes.snapshotSource(der, OcspError, "ocsp/bad-input", "httpRequest: the OCSPRequest DER");
868
+ if (body.length === 0) throw E("ocsp/bad-input", "httpRequest: the OCSPRequest DER is empty");
869
+ if (typeof responderUrl !== "string" || responderUrl === "") {
870
+ throw E("ocsp/bad-input", "httpRequest: the responder URL must be a non-empty string");
871
+ }
872
+ var parts = guard.name.uriParts(responderUrl);
873
+ if (parts === null || !parts.hasAuthority || parts.host === null) {
874
+ throw E("ocsp/bad-input", "httpRequest: the responder URL must carry a scheme and a host (RFC 5280 sec. 4.2.2.1)");
875
+ }
876
+ if (responderUrl.indexOf("?") !== -1 || responderUrl.indexOf("#") !== -1) {
877
+ throw E("ocsp/bad-input", "httpRequest: the responder URL carries a query or a fragment, and a path segment appended after either would not be part of the path (RFC 3986 sec. 3.3)");
878
+ }
879
+ /** @internal The bound below is a byte count, and a character is not a byte above US-ASCII. A
880
+ * URI is US-ASCII by RFC 3986 sec. 2; a responder URL carrying more than that is an IRI, whose
881
+ * conversion to a URI is RFC 3987 work this toolkit does not do, so it is refused rather than
882
+ * measured as though the two were the same length. */
883
+ if (!guard.encoding.isAsciiText(responderUrl)) {
884
+ throw E("ocsp/bad-input", "httpRequest: the responder URL must be US-ASCII (RFC 3986 sec. 2); percent-encode the rest before passing it, since the RFC 5019 sec. 5 size bound counts the bytes that go on the wire");
885
+ }
886
+ var asked = opts.method === undefined ? null : opts.method;
887
+ if (asked !== null && asked !== "GET" && asked !== "POST") {
888
+ throw E("ocsp/bad-input", "httpRequest: opts.method must be \"GET\" or \"POST\"");
889
+ }
890
+ /** @internal The base64 goes through the buffer encoder, which emits one line and therefore no
891
+ * CR and no LF, which RFC 5019 sec. 5 states as a MUST NOT. */
892
+ var b64 = body.toString("base64");
893
+ var segment = guard.encoding.uriPathSegment(b64, E, "ocsp/bad-input", "the encoded OCSPRequest");
894
+ /** @internal The URL is handed to a transport, which parses it before opening a socket, and RFC
895
+ * 3986 sec. 5.2.4 removes a `.` or `..` segment during that parse. Appending to an unnormalized
896
+ * path would therefore build one URL and send another, at a path the responder was never named
897
+ * at. Normalizing the base first makes what this returns what goes on the wire, which is also
898
+ * the string the sec. 5 size bound has to be measured on. The appended segment is base64 under
899
+ * the sec. 2.3 unreserved set, which holds no `.`, so it cannot become a dot segment itself. */
900
+ var normalized;
901
+ try { normalized = new URL(responderUrl).href; }
902
+ catch (e) { throw E("ocsp/bad-input", "httpRequest: the responder URL does not parse as a URL: " + guard.text.describeThrown(e), e); }
903
+ var base = normalized.charAt(normalized.length - 1) === "/"
904
+ ? normalized : normalized + "/";
905
+ var getUrl = base + segment;
906
+ /** @internal Both parts are US-ASCII by the checks above, so a character is a byte and the
907
+ * count is the one RFC 5019 sec. 5 states. */
908
+ var getUrlBytes = Buffer.byteLength(getUrl, "ascii");
909
+ var fitsGet = getUrlBytes <= HTTP_GET_MAX_URL_BYTES;
910
+ if (asked === "GET" && !fitsGet) {
911
+ throw E("ocsp/bad-input", "httpRequest: the encoded GET URL is " + getUrlBytes +
912
+ " bytes, over the " + HTTP_GET_MAX_URL_BYTES + " RFC 5019 sec. 5 allows; send this request as a POST");
913
+ }
914
+ var useGet = asked === "GET" || (asked === null && fitsGet);
915
+ if (useGet) {
916
+ return guard.verdict.of({ method: "GET", url: getUrl, headers: _frozenHeaders({ accept: "application/ocsp-response" }), body: null });
917
+ }
918
+ return guard.verdict.of({
919
+ method: "POST", url: normalized,
920
+ headers: _frozenHeaders({ "content-type": "application/ocsp-request", accept: "application/ocsp-response" }),
921
+ body: body,
922
+ });
923
+ }
924
+
925
+ var HTTP_REQUEST_OPTS = Object.assign(Object.create(null), { method: 1 });
926
+
927
+ function _frozenHeaders(h) {
928
+ var out = Object.create(null), k;
929
+ for (k in h) { if (Object.prototype.hasOwnProperty.call(h, k)) out[k] = h[k]; }
930
+ return Object.freeze(out);
931
+ }
932
+
831
933
  module.exports = {
832
934
  buildRequest: buildRequest,
935
+ httpRequest: httpRequest,
833
936
  sign: sign,
834
937
  buildErrorResponse: buildErrorResponse,
835
938
  verify: verify,
package/lib/oid.js CHANGED
@@ -116,7 +116,7 @@ var FAMILIES = Object.assign(Object.create(null), {
116
116
  "id-cct-PKIData": 2, "id-cct-PKIResponse": 3,
117
117
  } },
118
118
 
119
- pkixOn: { base: [1, 3, 6, 1, 5, 5, 7, 8], of: { hardwareModuleName: 4, smtpUtf8Mailbox: 9, macAddress: 12 } },
119
+ pkixOn: { base: [1, 3, 6, 1, 5, 5, 7, 8], of: { hardwareModuleName: 4, dnsSRV: 7, smtpUtf8Mailbox: 9, macAddress: 12 } },
120
120
 
121
121
  cabfPolicy: { base: [2, 23, 140, 1], of: {
122
122
  "ev-guidelines": 1, "domain-validated": [2, 1], "organization-validated": [2, 2], "individual-validated": [2, 3] } },