@blamejs/pki 0.8.31 → 0.8.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +393 -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 +939 -48
  32. package/lib/est.js +29 -16
  33. package/lib/framework-error.js +12 -0
  34. package/lib/guard-async.js +1 -1
  35. package/lib/guard-bytes.js +103 -20
  36. package/lib/guard-crypto.js +205 -3
  37. package/lib/guard-encoding.js +6 -1
  38. package/lib/guard-identifier.js +55 -8
  39. package/lib/guard-intrinsic.js +43 -0
  40. package/lib/guard-json.js +122 -9
  41. package/lib/guard-limits.js +12 -1
  42. package/lib/guard-list.js +63 -8
  43. package/lib/guard-name.js +167 -20
  44. package/lib/guard-range.js +43 -5
  45. package/lib/guard-text.js +41 -0
  46. package/lib/guard-verdict.js +39 -8
  47. package/lib/hpke.js +688 -42
  48. package/lib/http-transport.js +156 -4
  49. package/lib/identity-match.js +4 -4
  50. package/lib/inspect.js +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
@@ -72,6 +72,15 @@ var nodeDns = require("node:dns");
72
72
  var nodeUtil = require("node:util");
73
73
  var constants = require("./constants");
74
74
  var guard = require("./guard-all");
75
+ var intrinsic = require("./guard-intrinsic");
76
+ /** @internal `resolve` and `reject` build their promise from the RECEIVER, so reading the global
77
+ * binding at the call hands the construction to whatever that binding holds. A refusal returned as a
78
+ * rejected promise is a verdict: a replacement whose `reject` resolves lets a request the preparation
79
+ * step rejected proceed, and `peerChain`'s refusal to tunnel through a proxy is one of those. Taken at
80
+ * load. */
81
+ var _Promise = intrinsic.Promise;
82
+ var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
83
+ var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
75
84
  var frameworkError = require("./framework-error");
76
85
 
77
86
  var TransportError = frameworkError.TransportError;
@@ -124,6 +133,30 @@ function _tlsInfo(socket) {
124
133
  return { protocol: proto, cipher: cipher, peerCertificate: peer && peer.raw ? peer.raw : null, tlsUnique: tlsUnique, tlsExporter: tlsExporter };
125
134
  }
126
135
 
136
+ /** @internal The chain the TLS session resolved, leaf first, walked through the detailed peer
137
+ * certificate's `issuerCertificate` links. The walk is bounded twice over: the top certificate
138
+ * issues itself, so the identity comparison ends it, and a cap ends any cycle a runtime could
139
+ * report instead.
140
+ * IT IS NOT THE SAME AS WHAT THE ENDPOINT TRANSMITTED, which this comment used to claim. Node
141
+ * completes `issuerCertificate` from the trust store in scope, so an endpoint that sends its leaf
142
+ * alone still yields a chain whose upper entries came from the configured anchors rather than off
143
+ * the wire. Node exposes no API for the transmitted list, so the distinction cannot be recovered
144
+ * here and is stated instead of guessed at: filtering the entries that match an anchor would drop
145
+ * a root an endpoint really did send. */
146
+ function _peerChain(socket) {
147
+ if (!socket || typeof socket.getPeerCertificate !== "function") return [];
148
+ var out = [];
149
+ var seen = [];
150
+ var cur = socket.getPeerCertificate(true);
151
+ while (cur && Buffer.isBuffer(cur.raw) && out.length < constants.LIMITS.TLS_MAX_PEER_CHAIN) {
152
+ if (seen.indexOf(cur) !== -1) break;
153
+ seen.push(cur);
154
+ out.push(cur.raw);
155
+ cur = cur.issuerCertificate;
156
+ }
157
+ return out;
158
+ }
159
+
127
160
  function _pemifyAnchor(a) {
128
161
  if (typeof a === "string") return a;
129
162
  if (Buffer.isBuffer(a)) {
@@ -557,8 +590,8 @@ function httpsTransport(defaults) {
557
590
  request = request || {};
558
591
  var prep;
559
592
  try { prep = _prepare(request); }
560
- catch (e) { return Promise.reject(e); }
561
- return new Promise(function (resolve, reject) {
593
+ catch (e) { return _promiseReject(_Promise, e); }
594
+ return new _Promise(function (resolve, reject) {
562
595
  var settled = false;
563
596
  var req = null;
564
597
  var creq = null;
@@ -625,7 +658,7 @@ function httpsTransport(defaults) {
625
658
  function _writeBoundBody() {
626
659
  if (settled) return;
627
660
  var info = _tlsInfo(socket);
628
- Promise.resolve().then(function () { return prep.body(info); }).then(function (b) {
661
+ _promiseResolve(_Promise, undefined).then(function () { return prep.body(info); }).then(function (b) {
629
662
  if (settled) return;
630
663
  try {
631
664
  if (guard.bytes.isByteSource(b)) b = guard.bytes.source(b, E, C("bad-input"), "the channel-binding body");
@@ -745,7 +778,126 @@ function httpsTransport(defaults) {
745
778
  });
746
779
  };
747
780
  _transportFn.blocksPrivateAddresses = true;
781
+
782
+ /** @internal peerChain negotiates TLS and hands back the channel without sending a request. It
783
+ * prepares the connection through the SAME `_prepare` the request path uses, so the anchors, the
784
+ * SNI, the version floor, the identity hook, the private-address block and the URL scheme gate are
785
+ * one policy with two entry points rather than two policies. `tls.connect` takes `host` where
786
+ * `https.request` takes `hostname`, and ignores the method, path and headers `_prepare` also
787
+ * builds. A proxy is refused rather than bypassed: tunnelling belongs to the request path, and
788
+ * connecting directly while a proxy is configured would reach an origin the caller asked not to
789
+ * reach directly. */
790
+ _transportFn.peerChain = function peerChain(request) {
791
+ request = request || {};
792
+ var prep;
793
+ try { prep = _prepare({
794
+ url: request.url,
795
+ method: "GET",
796
+ tls: request.tls,
797
+ timeout: request.timeout,
798
+ blockPrivateAddresses: request.blockPrivateAddresses,
799
+ allowPlaintextHttp: false,
800
+ proxy: request.proxy,
801
+ }); }
802
+ catch (e) { return _promiseReject(_Promise, e); }
803
+ if (prep.proxy) {
804
+ return _promiseReject(_Promise, E(C("bad-proxy"),
805
+ "peerChain opens a direct connection and does not tunnel; fetch through a proxy with the request verb"));
806
+ }
807
+ var o = prep.options;
808
+ var connectOpts = {
809
+ host: o.hostname, port: o.port, minVersion: o.minVersion,
810
+ rejectUnauthorized: o.rejectUnauthorized,
811
+ };
812
+ if (o.servername !== undefined) connectOpts.servername = o.servername;
813
+ if (o.ca !== undefined) connectOpts.ca = o.ca;
814
+ if (o.cert !== undefined) connectOpts.cert = o.cert;
815
+ if (o.key !== undefined) connectOpts.key = o.key;
816
+ if (o.checkServerIdentity !== undefined) connectOpts.checkServerIdentity = o.checkServerIdentity;
817
+ if (o.lookup !== undefined) connectOpts.lookup = o.lookup;
818
+
819
+ return new _Promise(function (resolve, reject) {
820
+ var settled = false;
821
+ var socket;
822
+ var timer = null;
823
+ function done(fn, arg) {
824
+ if (settled) return;
825
+ settled = true;
826
+ if (timer !== null) { clearTimeout(timer); timer = null; }
827
+ try { if (socket) socket.destroy(); } catch (_de) { /* allow:swallow-unverified the socket is being abandoned either way */ }
828
+ fn(arg);
829
+ }
830
+ try { socket = nodeTls.connect(connectOpts); }
831
+ catch (e) {
832
+ reject(E(_classifyError(e, C), "the TLS connection failed: " + e.message, e));
833
+ return;
834
+ }
835
+ /** @internal The budget is an ABSOLUTE deadline, the same timer the request path above uses, and not
836
+ * `socket.setTimeout`: that one bounds INACTIVITY, so what it measures is the gap between bytes
837
+ * rather than the elapsed time a caller asked to be bounded by. A peer that keeps the socket busy
838
+ * without completing the handshake is the case the two differ on. The socket inactivity timeout is
839
+ * kept as well, since it ends a connection that goes silent sooner than the deadline would. */
840
+ timer = setTimeout(function () {
841
+ done(reject, E(C("timeout"), "the TLS handshake did not complete within " + prep.timeout + "ms"));
842
+ }, prep.timeout);
843
+ socket.setTimeout(prep.timeout, function () {
844
+ done(reject, E(C("timeout"), "the TLS handshake did not complete within " + prep.timeout + "ms"));
845
+ });
846
+ socket.once("secureConnect", function () {
847
+ var info = _tlsInfo(socket);
848
+ info.peerChain = _peerChain(socket);
849
+ done(resolve, info);
850
+ });
851
+ socket.once("error", function (e) {
852
+ var code = _classifyError(e, C);
853
+ done(reject, E(code || C("tls-failed"), "the TLS connection failed: " + e.message, e));
854
+ });
855
+ });
856
+ };
857
+
748
858
  return _transportFn;
749
859
  }
750
860
 
751
- module.exports = { https: httpsTransport, isBlockedIp: _isBlockedIp, _makeGuardedLookup: _makeGuardedLookup, MAX_TIMEOUT: MAX_TIMEOUT, snapshotProxy: snapshotProxy };
861
+ /**
862
+ * @primitive pki.transport.peerChain
863
+ * @signature pki.transport.peerChain(request, defaults?) -> Promise<channel>
864
+ * @since 0.8.32
865
+ * @status stable
866
+ * @spec RFC 8446, RFC 6125
867
+ * @defends server-impersonation (CWE-297), tls-downgrade (CWE-757), ssrf (CWE-918)
868
+ * @related pki.transport.https, pki.path.validate
869
+ *
870
+ * Negotiate TLS with an endpoint and return the channel it established, without sending a
871
+ * request: `{ protocol, cipher, peerCertificate, peerChain, tlsUnique, tlsExporter }`.
872
+ * `peerChain` is the chain the session resolved as DER, leaf first, walked up from the peer
873
+ * certificate and stopping at the one that issues itself. It is not a record of what crossed the
874
+ * wire: the runtime completes the chain from the trust anchors in scope, so an endpoint that sends
875
+ * only its leaf still yields the entries above it, and those came from the configured anchors.
876
+ * Treat the list as the chain this session built, and read `peerCertificate` for the one
877
+ * certificate the endpoint certainly sent. Nothing reaches the application behind the endpoint,
878
+ * which is the difference from making a request for the same purpose.
879
+ *
880
+ * The connection is prepared by the same code `pki.transport.https` prepares a request with,
881
+ * so the trust anchors, the SNI, the version floor, the `checkServerIdentity` hook, the
882
+ * private-address block and the https-only rule are one policy rather than a second copy of
883
+ * one. A request with neither an explicit anchor nor `useSystemStore` is refused
884
+ * (`transport/no-trust-anchors`) before any socket is opened, and an endpoint whose chain does
885
+ * not build to the configured anchors, or whose name does not match the URL, is refused
886
+ * (`transport/server-auth-failed`). A configured proxy is refused (`transport/bad-proxy`)
887
+ * rather than bypassed, since connecting directly would reach an origin the caller asked to
888
+ * reach through the proxy.
889
+ *
890
+ * **A chain this returns is not a validated one.** The handshake checked that it builds to a
891
+ * configured anchor and matches the name in the URL, and checked no revocation status and no
892
+ * policy. `pki.path.validate` is what validates a path.
893
+ *
894
+ * @example
895
+ * var channel = await pki.transport.peerChain(
896
+ * { url: "https://example.com/" }, { tls: { useSystemStore: true } });
897
+ * channel.peerChain.length; // the chain the session resolved
898
+ * pki.schema.x509.parse(channel.peerChain[0]).subject.dn; // the leaf's subject
899
+ * channel.peerCertificate; // the one certificate the endpoint sent
900
+ */
901
+ function peerChain(request, defaults) { return httpsTransport(defaults || {}).peerChain(request); }
902
+
903
+ module.exports = { https: httpsTransport, peerChain: peerChain, isBlockedIp: _isBlockedIp, _makeGuardedLookup: _makeGuardedLookup, MAX_TIMEOUT: MAX_TIMEOUT, snapshotProxy: snapshotProxy };
@@ -446,8 +446,8 @@ function _search(refs, entries, wildcardsAllowed) {
446
446
  pairs.tick();
447
447
  var got = _compare(refs[r], entries[e], wildcardsAllowed);
448
448
  if (got.disposition === "match") {
449
- return { matched: true, matchedReference: refs[r].input, matchedEntry: _entryLabel(entries[e]),
450
- ignored: ignored, reason: null };
449
+ return guard.verdict.of({ matched: true, matchedReference: refs[r].input, matchedEntry: _entryLabel(entries[e]),
450
+ ignored: ignored, reason: null });
451
451
  }
452
452
  if (got.disposition === "ignored") {
453
453
  var key = e + "|" + got.reason;
@@ -458,10 +458,10 @@ function _search(refs, entries, wildcardsAllowed) {
458
458
  }
459
459
  }
460
460
  }
461
- return { matched: false, matchedReference: null, matchedEntry: null, ignored: ignored,
461
+ return guard.verdict.of({ matched: false, matchedReference: null, matchedEntry: null, ignored: ignored,
462
462
  reason: entries.length === 0
463
463
  ? "the certificate presents no subjectAltName entry, and RFC 9525 sec. 2 forbids reading an identity from the subject"
464
- : "no presented identifier matched a reference identity" };
464
+ : "no presented identifier matched a reference identity" });
465
465
  }
466
466
 
467
467
  /**
package/lib/inspect.js CHANGED
@@ -409,6 +409,29 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
409
409
  ocspNoCheck: function (decoded, inner) {
410
410
  return inner + "OCSP No Check (this is an OCSP responder certificate; do not check its revocation)";
411
411
  },
412
+ /** @internal ITU-T X.509 (2019) clause 9.8. The alternative key and signature run to thousands of
413
+ * octets for a post-quantum algorithm, which is the case the extensions exist for, so each is
414
+ * reported by its algorithm and its length rather than dumped whole. The algorithm is what a reader
415
+ * needs: it says which set of algorithms this certificate is migrating to. */
416
+ subjectAltPublicKeyInfo: function (decoded, inner) {
417
+ return inner + "Algorithm: " + (decoded.algorithm.name || decoded.algorithm.oid) + "\n" +
418
+ inner + "Alternative Public Key: " + decoded.subjectAltPublicKey.length + " bytes";
419
+ },
420
+ altSignatureAlgorithm: function (decoded, inner) {
421
+ return inner + (decoded.name || decoded.oid);
422
+ },
423
+ altSignatureValue: function (decoded, inner) {
424
+ return inner + "Alternative Signature: " + decoded.bytes.length + " bytes";
425
+ },
426
+ /** @internal RFC 9763 sec. 4.1 names a second certificate of the same subject by the digest of the
427
+ * whole certificate, so both fields are shown: the digest is only meaningful beside the algorithm
428
+ * that produced it. */
429
+ relatedCertificate: function (decoded, inner) {
430
+ var alg = decoded.hashAlgorithm || decoded.hashAlgorithmOid || "(unnamed algorithm)";
431
+ var value = Buffer.isBuffer(decoded.hashValue) ? decoded.hashValue : Buffer.alloc(0);
432
+ return inner + "Hash Algorithm: " + alg + "\n" +
433
+ inner + "Related Certificate: " + _hexColon(value, { upper: true });
434
+ },
412
435
  subjectKeyIdentifier: function (decoded, inner) {
413
436
  return inner + _hexColon(Buffer.isBuffer(decoded) ? decoded : (decoded.bytes || Buffer.alloc(0)), { upper: true });
414
437
  },
@@ -419,8 +442,8 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
419
442
  decoded.authorityCertIssuer.names.forEach(function (g) { akiLines.push(inner + _gn(g)); });
420
443
  }
421
444
  if (decoded.authorityCertSerialNumber != null) {
422
- var sn = (typeof decoded.authorityCertSerialNumber === "bigint"
423
- ? decoded.authorityCertSerialNumber : BigInt(decoded.authorityCertSerialNumber)).toString(16);
445
+ var sn = guard.range.authoredInteger(decoded.authorityCertSerialNumber, _err,
446
+ "inspect/bad-input", "authorityCertSerialNumber").toString(16);
424
447
  if (sn.length % 2) sn = "0" + sn;
425
448
  akiLines.push(inner + "serial:0x" + sn.toUpperCase());
426
449
  }
@@ -2350,12 +2373,16 @@ function asn1Dump(input, opts) {
2350
2373
  var der;
2351
2374
  try { der = pkix.coerceToDer(input, _INSPECT_ENTRY); }
2352
2375
  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);
2376
+ /** @internal Read once, and safe to read off `der` because `pkix.coerceToDer` above has already
2377
+ * normalized the caller's input: an own `length` property shadowing the real byte count cannot reach
2378
+ * here. The cap below is therefore a cap on the bytes the walk decodes. */
2379
+ var derLen = der.length;
2380
+ if (derLen === 0) throw _err("inspect/bad-input", "input is empty");
2381
+ if (derLen > maxBytes) {
2382
+ throw _err("inspect/bad-input", "input " + derLen + " bytes exceeds the cap " + maxBytes);
2356
2383
  }
2357
2384
 
2358
- var head = ["ASN.1 structure: " + der.length + " bytes, decoded as DER"];
2385
+ var head = ["ASN.1 structure: " + derLen + " bytes, decoded as DER"];
2359
2386
  var node = null;
2360
2387
  var refusal = null;
2361
2388
  /** @internal The decoder builds the whole node tree before the renderer counts a line, so the
package/lib/jose.js CHANGED
@@ -39,6 +39,10 @@
39
39
  var constants = require("./constants");
40
40
  var wcEngine = require("./webcrypto");
41
41
  var webcrypto = wcEngine.webcrypto;
42
+ /** @internal The verify operation, captured at load. The object was held and its METHOD fetched per
43
+ * call, which leaves it replaceable: one answering true accepts a JWS whose signer never signed it,
44
+ * including on the embedded-key route this module pins against its thumbprint. */
45
+ var _subtleVerify = require("./guard-intrinsic").uncurry(webcrypto.subtle.verify);
42
46
  var frameworkError = require("./framework-error");
43
47
  var guard = require("./guard-all");
44
48
  var edwardsPoint = require("./edwards-point");
@@ -121,11 +125,11 @@ function b64uDecode(text) {
121
125
  * pki.jose.parseJson('{"a":1}'); // -> { a: 1 }
122
126
  */
123
127
  function parseJson(input) {
124
- return guard.json.parse(input, E, {
128
+ return guard.verdict.shield(guard.json.parse(input, E, {
125
129
  maxBytes: LIMITS.JSON_MAX_BYTES, maxDepth: LIMITS.JSON_MAX_DEPTH,
126
130
  badJson: "jose/bad-json", tooDeep: "jose/too-deep", duplicateMember: "jose/duplicate-member",
127
131
  tooLarge: "jose/too-large", badInput: "jose/bad-input", label: "the JSON document",
128
- });
132
+ }));
129
133
  }
130
134
 
131
135
 
@@ -329,9 +333,18 @@ async function verify(jws, opts) {
329
333
  if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
330
334
  if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
331
335
  if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
332
- if (typeof jws.protected !== "string" || typeof jws.signature !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
333
- if (typeof jws.payload !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
334
- var header = parseJson(b64uDecode(jws.protected));
336
+ /** @internal The three members are taken ONCE, for the reason `opts` is above: the JWS is the caller's
337
+ * object, and a member reached through an accessor answers every read separately. `protected` decides
338
+ * both the header the profile rules are applied to and the signing input the signature is checked over,
339
+ * and the thumbprint comparison awaits between those two uses, so reading it twice would let the rules
340
+ * be applied to one message and the signature checked over another. Everything below reads these
341
+ * locals. */
342
+ var protectedB64 = jws.protected;
343
+ var signatureB64 = jws.signature;
344
+ var payloadB64 = jws.payload;
345
+ if (typeof protectedB64 !== "string" || typeof signatureB64 !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
346
+ if (typeof payloadB64 !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
347
+ var header = parseJson(b64uDecode(protectedB64));
335
348
  var profileName = opts.profile || "acme-outer";
336
349
  var checked = _checkHeader(header, profileName);
337
350
  /** @internal The embedded jwk is the signer's PUBLIC key (RFC 7515 sec. 4.1.3). One carrying
@@ -342,14 +355,32 @@ async function verify(jws, opts) {
342
355
  if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
343
356
  throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
344
357
  }
345
- var jwk = header.jwk || opts.key;
358
+ /** @internal The caller's key is COPIED here, before ANY member of it is read, and everything below
359
+ * reads the copy: the thumbprint comparison against the embedded jwk, the key-type check, the Edwards
360
+ * low-order point gate, the signature-width calculation, the import parameters and the algorithm
361
+ * selector. Its members were read many times over, `crv` eight times and `kty` nine on one successful
362
+ * verify, and an accessor answers each read separately.
363
+ *
364
+ * The FIRST read is the one that decides which curve the point gate validates against
365
+ * (`jwk.crv === "Ed25519" ? 6 : 7`), so a `crv` answering `Ed448` once and `Ed25519` afterwards had the
366
+ * point gated under the wrong curve's parameters while an Ed25519 key was imported and verified. That
367
+ * gate is the whole defense against a low-order key verifying a FORGED signature. Copying before the
368
+ * thumbprint comparison rather than after it matters for the same reason: otherwise a key could
369
+ * thumbprint as the embedded one and be copied as a different one.
370
+ *
371
+ * `snapshotDeep` refuses an accessor-backed member rather than reading it once, which is the stronger
372
+ * answer for a value that selects an algorithm. The embedded jwk comes from the parsed header and is
373
+ * this module's own object, so it needs no copy. */
374
+ var suppliedKey = opts.key === undefined ? undefined
375
+ : guard.bytes.snapshotDeep(opts.key, JoseError, "jose/bad-key", "opts.key");
376
+ var jwk = header.jwk || suppliedKey;
346
377
  if (!jwk) throw E("jose/bad-key", "a verification key is required (opts.key) when the profile does not embed a jwk");
347
378
  var keySource = "embedded-jwk";
348
- if (opts.key) {
379
+ if (suppliedKey) {
349
380
  keySource = "opts.key";
350
381
  if (header.jwk) {
351
382
  var embeddedTp, suppliedTp;
352
- try { embeddedTp = await thumbprint(header.jwk); suppliedTp = await thumbprint(opts.key); }
383
+ try { embeddedTp = await thumbprint(header.jwk); suppliedTp = await thumbprint(suppliedKey); }
353
384
  catch (e) { throw E("jose/bad-key", "the embedded jwk and opts.key could not be compared as RFC 7638 thumbprints", e); }
354
385
  if (embeddedTp !== suppliedTp) {
355
386
  throw E("jose/key-mismatch",
@@ -358,19 +389,19 @@ async function verify(jws, opts) {
358
389
  ", supplied " + suppliedTp + ")");
359
390
  }
360
391
  }
361
- jwk = opts.key;
392
+ jwk = suppliedKey;
362
393
  }
363
394
  _assertKeyType(checked.algRow, jwk);
364
- var sig = b64uDecode(jws.signature);
395
+ var sig = b64uDecode(signatureB64);
365
396
  var want = _expectedSigBytes(checked.algRow, jwk);
366
397
  if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
367
- var signingInput = _bufferFrom(jws.protected + "." + jws.payload, "ascii");
398
+ var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
368
399
  var key;
369
400
  try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
370
401
  catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
371
- var ok = await webcrypto.subtle.verify(_cryptoAlg(checked.algRow, jwk), key, sig, signingInput);
402
+ var ok = await _subtleVerify(webcrypto.subtle, _cryptoAlg(checked.algRow, jwk), key, sig, signingInput);
372
403
  if (!ok) throw E("jose/verify-failed", "the JWS signature did not verify");
373
- return { header: header, payload: b64uDecode(jws.payload), keySource: keySource };
404
+ return { header: header, payload: b64uDecode(payloadB64), keySource: keySource };
374
405
  }
375
406
 
376
407
 
@@ -409,9 +440,23 @@ async function verify(jws, opts) {
409
440
  */
410
441
  var _SIGN_KEYS = intrinsic.assign(intrinsic.create(null), { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 });
411
442
  var _SIGN_KEYS_EMBEDDED_JWK = intrinsic.assign(intrinsic.create(null), { protected: 1, payload: 1, key: 1, profile: 1 });
443
+
444
+ /** @internal The payload, copied, taken before anything else on the caller's object is read. Every later
445
+ * read of that object is caller code when the field is an accessor, and the protected header is read
446
+ * FIRST and SERIALIZED, which reaches the header object, each of its fields and any `toJSON` on it. Over
447
+ * a view of the payload any of those would replace what gets signed, after the verb had accepted the
448
+ * payload it was handed, so the copy comes before all of them rather than before whichever one is in
449
+ * view. A payload that is not a Buffer is carried through untouched and refused where it always was, so
450
+ * no diagnostic moves ahead of the header's own. */
451
+ function _fixedPayload(supplied) {
452
+ if (!_isBuffer(supplied)) return supplied;
453
+ return guard.bytes.snapshot(supplied, JoseError, "jose/bad-input", "payload");
454
+ }
455
+
412
456
  async function sign(opts) {
413
457
  opts = opts || {};
414
458
  guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
459
+ var fixedPayload = _fixedPayload(opts.payload);
415
460
  var headerJson;
416
461
  try { headerJson = _stringify(opts.protected); }
417
462
  catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
@@ -424,13 +469,25 @@ async function sign(opts) {
424
469
  (k === "jwk" ? "; the protected header already embeds the public JWK" : "");
425
470
  });
426
471
  }
427
- var payload = opts.payload;
472
+ var payload = fixedPayload;
428
473
  if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
429
- payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
430
- if (!wcEngine.isCryptoKeyLike(opts.key)) throw E("jose/bad-input", "a private or secret CryptoKey (opts.key) is required");
474
+ /** @internal ONE read of the caller's signing key, taken before anything inspects it, and every use
475
+ * below reads this binding. It was read four times: the key-like test, twice by the hash-to-algorithm
476
+ * check, and once more to sign with. An accessor answers each read separately, so the check could
477
+ * approve a SHA-256 key while the signature was produced by a SHA-512 one, and the JWS emitted then
478
+ * DECLARES an algorithm its signature was not made under, which is the algorithm confusion the header
479
+ * registry exists to close before any crypto call.
480
+ *
481
+ * The RSA arms are where it showed: the registry's fixed signature widths catch the HMAC and EC arms,
482
+ * and two 2048-bit RSA keys produce the same 256-byte signature, so the width pin below cannot see a
483
+ * substitution between them. */
484
+ var suppliedKey = opts.key;
485
+ if (!wcEngine.isCryptoKeyLike(suppliedKey)) throw E("jose/bad-input", "a private or secret CryptoKey (opts.key) is required");
431
486
  var checked = _checkHeader(header, opts.profile || "acme-outer");
487
+ var suppliedAlg = suppliedKey.algorithm;
488
+ var suppliedHash = suppliedAlg && suppliedAlg.hash;
432
489
  if ((checked.algRow.kty === "RSA" || checked.algRow.kty === "oct") &&
433
- (!opts.key.algorithm.hash || opts.key.algorithm.hash.name !== checked.algRow.hash)) {
490
+ (!suppliedHash || suppliedHash.name !== checked.algRow.hash)) {
434
491
  throw E("jose/bad-key", "the signing key's hash does not match the algorithm " + checked.alg);
435
492
  }
436
493
  if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
@@ -438,15 +495,18 @@ async function sign(opts) {
438
495
  var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
439
496
  var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
440
497
  var jwk = header.jwk || opts.jwk || {};
441
- var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
498
+ /** @internal `suppliedKey` is the reading taken at the top, so the key adopted for signing, the key the
499
+ * hash-to-algorithm check approved and the key the expected signature width is computed from are all
500
+ * one key. */
501
+ var signKey = await wcEngine.adoptKey(suppliedKey, null, ["sign"], E, "jose/bad-input");
442
502
  var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
443
- var want = _expectedSigBytes(checked.algRow, jwk, opts.key);
503
+ var want = _expectedSigBytes(checked.algRow, jwk, suppliedKey);
444
504
  if (want != null && sigBuf.length !== want) throw E("jose/bad-key", "the signing key produced a " + sigBuf.length + "-byte signature but " + checked.alg + " requires " + want + " (the key does not match the algorithm)");
445
505
  if (header.jwk) {
446
506
  var pubKey;
447
507
  try { pubKey = await webcrypto.subtle.importKey("jwk", header.jwk, _importParams(checked.algRow, header.jwk), false, ["verify"]); }
448
508
  catch (e) { throw E("jose/bad-key", "the embedded jwk could not be imported to confirm it matches the signing key", e); }
449
- if (!(await webcrypto.subtle.verify(_cryptoAlg(checked.algRow, header.jwk), pubKey, sigBuf, signingInput))) {
509
+ if (!(await _subtleVerify(webcrypto.subtle, _cryptoAlg(checked.algRow, header.jwk), pubKey, sigBuf, signingInput))) {
450
510
  throw E("jose/bad-key", "the embedded jwk does not match the signing key (it cannot verify the produced signature)");
451
511
  }
452
512
  }
@@ -485,14 +545,22 @@ var THUMBPRINT_MEMBERS = intrinsic.assign(intrinsic.create(null), {
485
545
  * await pki.jose.thumbprint(accountJwk); // -> "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
486
546
  */
487
547
  async function thumbprint(jwk) {
488
- if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
489
- var members = THUMBPRINT_MEMBERS[jwk.kty];
490
- if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(jwk.kty));
548
+ if (!jwk || typeof jwk !== "object") throw E("jose/bad-key", "a JWK object with a kty is required");
549
+ /** @internal COPIED before any member is read, because what this returns is an IDENTITY. Each member
550
+ * was read twice, the type check and then the value put into the hash, and `kty` four times; an
551
+ * accessor answers each read separately, so a member could be checked as one value and HASHED as
552
+ * another. The thumbprint is what RFC 7638 identifies a key by: `pki.jose.verify` compares an embedded
553
+ * jwk against `opts.key` with it, and ACME uses it as the account identity in a key authorization. A
554
+ * thumbprint over a value the checks never approved names a key that was never the one validated. */
555
+ var copied = guard.bytes.snapshotDeep(jwk, JoseError, "jose/bad-key", "the JWK");
556
+ if (typeof copied.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
557
+ var members = THUMBPRINT_MEMBERS[copied.kty];
558
+ if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(copied.kty));
491
559
  var parts = [];
492
560
  for (var i = 0; i < members.length; i++) {
493
561
  var m = members[i];
494
- if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
495
- _push(parts, _stringify(m) + ":" + _stringify(jwk[m]));
562
+ if (typeof copied[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
563
+ _push(parts, _stringify(m) + ":" + _stringify(copied[m]));
496
564
  }
497
565
  var canonical = "{" + _join(parts, ",") + "}";
498
566
  var digest = _bufferFrom(await webcrypto.subtle.digest("SHA-256", _bufferFrom(canonical, "utf8")));
package/lib/key.js CHANGED
@@ -30,6 +30,11 @@ var schemaSec1 = require("./schema-sec1");
30
30
  var pkix = require("./schema-pkix");
31
31
  var schema = require("./schema-engine");
32
32
  var webcrypto = require("./webcrypto");
33
+ /** @internal The key-material exporter, captured at load. The signature probe that decides whether two
34
+ * halves are a pair imports and signs with what this returns, so a replacement exporting a different
35
+ * private key makes a mismatched pair report as corresponding: the probe then signs and verifies with
36
+ * a pair that does correspond, and neither half is the one the caller supplied. */
37
+ var _exportAnyKey = webcrypto.exportAnyKey;
33
38
  var compositeKem = require("./composite-kem");
34
39
  var frameworkError = require("./framework-error");
35
40
  var guard = require("./guard-all");
@@ -123,7 +128,7 @@ function _sec1ToPkcs8(der, curveOid) {
123
128
  async function _toPrivateKeyDer(input) {
124
129
  if (_isCryptoKey(input)) {
125
130
  if (input.type !== "private") throw _err("key/bad-input", "a private CryptoKey is required (got a " + input.type + " key)");
126
- return webcrypto.exportAnyKey(input, _err, "key/bad-input");
131
+ return _exportAnyKey(input, _err, "key/bad-input");
127
132
  }
128
133
  return pkix.coerceToDer(input, { pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
129
134
  }
@@ -295,13 +300,13 @@ async function export_(key, opts) {
295
300
  var fmt = opts.format || "der";
296
301
  if (fmt === "jwk") {
297
302
  try {
298
- return await webcrypto.exportAnyKey(key, _err, "key/bad-input", "jwk");
303
+ return await _exportAnyKey(key, _err, "key/bad-input", "jwk");
299
304
  } catch (e) {
300
305
  if (e && e.isPkiError) throw e;
301
306
  throw _err("key/bad-input", "exporting the key as a JWK failed", e);
302
307
  }
303
308
  }
304
- var der = await webcrypto.exportAnyKey(key, _err, "key/bad-input");
309
+ var der = await _exportAnyKey(key, _err, "key/bad-input");
305
310
  if (fmt === "der") return der;
306
311
  if (fmt === "pem") return pkix.pemEncode(der, opts.label || defaultLabel, PemError);
307
312
  if (fmt === "pkcs1") return _toPkcs1(der, key);
@@ -870,8 +875,8 @@ async function publicFromPrivate(privateKey, opts) {
870
875
  return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
871
876
  }
872
877
  try {
873
- var priv = nodeCrypto.createPrivateKey({ key: der, format: "der", type: "pkcs8" });
874
- spki = nodeCrypto.createPublicKey(priv).export({ format: "der", type: "spki" });
878
+ var priv = guard.crypto.privateKey({ key: der, format: "der", type: "pkcs8" });
879
+ spki = guard.crypto.exportKey(guard.crypto.publicKey(priv), { format: "der", type: "spki" });
875
880
  } catch (e) { throw _err("key/bad-input", "could not derive the public key from the private key", e); }
876
881
  return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
877
882
  }
@@ -946,7 +951,7 @@ function _pairSignOptions(priv, pub) {
946
951
  /** @internal Null-prototype, because the engine READS this object by name. A plain object would
947
952
  * let an inherited `saltLength` reach the signature as a restriction neither half stated. */
948
953
  var options = _create(null);
949
- options.padding = nodeCrypto.constants.RSA_PKCS1_PSS_PADDING;
954
+ options.padding = guard.crypto.padding.RSA_PKCS1_PSS;
950
955
  if (salt != null) options.saltLength = salt;
951
956
  return { digest: digest, options: options };
952
957
  }
@@ -1006,7 +1011,7 @@ async function correspondsTo(privateKey, spkiDer) {
1006
1011
  var privAsPkcs3 = _dhAsPkcs3(privDer, 1, "the private key's");
1007
1012
  try {
1008
1013
  _assertDhWithin(privAsPkcs3, 1, "the private key's");
1009
- try { priv = nodeCrypto.createPrivateKey({ key: privAsPkcs3, format: "der", type: "pkcs8" }); }
1014
+ try { priv = guard.crypto.privateKey({ key: privAsPkcs3, format: "der", type: "pkcs8" }); }
1010
1015
  catch (e) { throw _err("key/bad-input", "the private key could not be read", e); }
1011
1016
  } finally { if (privAsPkcs3 !== privDer) guard.secret.zeroize(privAsPkcs3, KeyError, "key/bad-input", "the rewritten private key"); }
1012
1017
  var publicOid = _algorithmOidAt(spki, 0);
@@ -1014,7 +1019,7 @@ async function correspondsTo(privateKey, spkiDer) {
1014
1019
  if (_isDhOid(_algorithmOidAt(privDer, 1)) !== _isDhOid(publicOid)) return false;
1015
1020
  var pubAsPkcs3 = _dhAsPkcs3(spki, 0, "the public key's");
1016
1021
  _assertDhWithin(pubAsPkcs3, 0, "the public key's");
1017
- try { pub = nodeCrypto.createPublicKey({ key: pubAsPkcs3, format: "der", type: "spki" }); }
1022
+ try { pub = guard.crypto.publicKey({ key: pubAsPkcs3, format: "der", type: "spki" }); }
1018
1023
  catch (e) { throw _err("key/bad-input", "the public key could not be read", e); }
1019
1024
  if (!_sameKeyFamily(priv, pub)) return false;
1020
1025
  var kind = guard.text.keyOf(priv.asymmetricKeyType);
@@ -1029,7 +1034,7 @@ async function correspondsTo(privateKey, spkiDer) {
1029
1034
  /** @internal Compared as KEYS rather than as encodings: a PKCS #3 DHParameter may carry the
1030
1035
  * optional privateValueLength beside the same p and g, and two encodings of one public value
1031
1036
  * are one key. The runtime's equality compares the group and the value. */
1032
- return _keyEquals(nodeCrypto.createPublicKey(priv), pub) === true;
1037
+ return _keyEquals(guard.crypto.publicKey(priv), pub) === true;
1033
1038
  }
1034
1039
  if (_PAIR_KEM[kind]) {
1035
1040
  var sealed = nodeCrypto.encapsulate(pub);
@@ -1053,8 +1058,8 @@ async function correspondsTo(privateKey, spkiDer) {
1053
1058
  * structure for an attacker to plant, so what the scalar generates IS the canonical answer and
1054
1059
  * comparing against it adds a bound the agreement alone does not give. */
1055
1060
  return guard.crypto.constantTimeEqual(
1056
- nodeCrypto.createPublicKey(priv).export({ format: "der", type: "spki" }),
1057
- pub.export({ format: "der", type: "spki" }));
1061
+ guard.crypto.exportKey(guard.crypto.publicKey(priv), { format: "der", type: "spki" }),
1062
+ guard.crypto.exportKey(pub, { format: "der", type: "spki" }));
1058
1063
  }
1059
1064
  var how = _pairSignOptions(priv, pub);
1060
1065
  if (how === null) {
@@ -1062,8 +1067,8 @@ async function correspondsTo(privateKey, spkiDer) {
1062
1067
  }
1063
1068
  var signKey = how.options ? _assign(_create(null), { key: priv }, how.options) : priv;
1064
1069
  var verifyKey = how.options ? _assign(_create(null), { key: pub }, how.options) : pub;
1065
- return nodeCrypto.verify(how.digest, _PAIR_PROBE, verifyKey,
1066
- nodeCrypto.sign(how.digest, _PAIR_PROBE, signKey)) === true;
1070
+ return guard.crypto.verify(how.digest, _PAIR_PROBE, verifyKey,
1071
+ guard.crypto.sign(how.digest, _PAIR_PROBE, signKey)) === true;
1067
1072
  } catch (e) {
1068
1073
  /** @internal The incompatible-parameters verdict is decided rather than caught, so it is passed
1069
1074
  * through with the reason it was raised for. Rewrapping it would tell an operator the pair could