@blamejs/pki 0.9.4 → 0.9.6
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/CHANGELOG.md +60 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +19 -3
- package/lib/cmp-verify.js +25 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +10 -5
- package/lib/cms-encrypt.js +6 -1
- package/lib/cms-sign.js +16 -0
- package/lib/cms-verify.js +32 -14
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/csr-sign.js +6 -2
- package/lib/ct.js +16 -3
- package/lib/est.js +67 -31
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +224 -0
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +1 -1
- package/lib/http-transport.js +299 -36
- package/lib/jose.js +1 -1
- package/lib/lint.js +67 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +16 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +119 -18
- package/lib/pkcs12-build.js +6 -1
- package/lib/pki-build.js +38 -6
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +46 -11
- package/lib/scep.js +39 -14
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +32 -16
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +121 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +15 -2
- package/lib/webauthn-mds.js +29 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +26 -10
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/est.js
CHANGED
|
@@ -173,6 +173,17 @@ function _knownOpts(opts, known, verb) {
|
|
|
173
173
|
Object.keys(known).sort().join(", ");
|
|
174
174
|
});
|
|
175
175
|
_assertTransport(settled);
|
|
176
|
+
/** @internal The two switches these verbs document, typed here because this is the one door every
|
|
177
|
+
* one of them passes through and it runs before any request goes out. Both are read further down
|
|
178
|
+
* by truthiness: `allowCrossOriginRedirect: "false"` is truthy, so it ALLOWED the cross-origin
|
|
179
|
+
* redirect on an unsafe method that the check exists to refuse (sec. 3.2.1), and
|
|
180
|
+
* `requestedEncryption: "false"` is read as "encryption was requested", which skips the
|
|
181
|
+
* sec. 4.4.2 check that an encrypted key part was advertised. A verb that does not accept one of
|
|
182
|
+
* them never sees a value for it. */
|
|
183
|
+
guard.identifier.booleanOption(settled.allowCrossOriginRedirect, E, "est/bad-input",
|
|
184
|
+
"pki.est." + verb + " opts.allowCrossOriginRedirect");
|
|
185
|
+
guard.identifier.booleanOption(settled.requestedEncryption, E, "est/bad-input",
|
|
186
|
+
"pki.est." + verb + " opts.requestedEncryption");
|
|
176
187
|
return settled;
|
|
177
188
|
}
|
|
178
189
|
|
|
@@ -907,10 +918,23 @@ function _parseUrl(urlStr) {
|
|
|
907
918
|
return url;
|
|
908
919
|
}
|
|
909
920
|
|
|
910
|
-
/** @internal The settings this client forwards to its transport.
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
921
|
+
/** @internal The settings this client forwards to its transport. The distrust list is typed here, once
|
|
922
|
+
* per verb, and the frozen copy is the one every hop carries. A setting not on the list is refused. */
|
|
923
|
+
var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity", "distrustedKeys"];
|
|
924
|
+
var TLS_KNOWN = intrinsic.freeze(guard.identifier.pickSettings(null, TLS_SETTINGS));
|
|
925
|
+
function _tlsForRequest(tls) {
|
|
926
|
+
guard.identifier.assertKnownKeys(tls, TLS_KNOWN, E, "est/bad-input", "unknown opts.tls setting ");
|
|
927
|
+
var t = guard.identifier.pickSettings(tls, TLS_SETTINGS);
|
|
928
|
+
t.distrustedKeys = guard.distrust.keys(t.distrustedKeys, E, "est/bad-input", "opts.tls.distrustedKeys");
|
|
929
|
+
return t;
|
|
930
|
+
}
|
|
931
|
+
/** @internal The proxy and the tls settings, taken at the synchronous verb door. Every request and
|
|
932
|
+
* redirect hop of the verb uses this record. */
|
|
933
|
+
function _doorSnapshot(opts) {
|
|
934
|
+
var proxy, tls;
|
|
935
|
+
try { proxy = _snapProxy(opts); tls = opts.tls; }
|
|
936
|
+
catch (_pe) { throw E("est/bad-input", "the options object could not be read", _pe); }
|
|
937
|
+
return { proxy: proxy, tls: _tlsForRequest(tls) };
|
|
914
938
|
}
|
|
915
939
|
|
|
916
940
|
function _redirectTarget(current, location, method, opts) {
|
|
@@ -969,7 +993,7 @@ function _authScheme(opts) {
|
|
|
969
993
|
if (sc !== "basic" && sc !== "digest") throw E("est/bad-input", "opts.auth.scheme must be \"basic\" or \"digest\"");
|
|
970
994
|
return sc;
|
|
971
995
|
}
|
|
972
|
-
if (opts.username
|
|
996
|
+
if (opts.username != null || opts.password != null) return "basic";
|
|
973
997
|
return null;
|
|
974
998
|
}
|
|
975
999
|
function _authUser(opts) { return opts.auth && opts.auth.username !== undefined ? opts.auth.username : opts.username; }
|
|
@@ -1084,7 +1108,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
1084
1108
|
/** @internal `checked` is the transport the verb already took at its door. Taking it again here
|
|
1085
1109
|
* would ask the caller's options a second time, and anything read in between could change the
|
|
1086
1110
|
* answer, so a verb that holds one passes it and this reads the option only when none was held. */
|
|
1087
|
-
function _client(op, method, baseUrl, body, headers, opts,
|
|
1111
|
+
function _client(op, method, baseUrl, body, headers, opts, snap, onAttempt, checked) {
|
|
1088
1112
|
var base;
|
|
1089
1113
|
try { base = new _URL(_stringOf(baseUrl)); }
|
|
1090
1114
|
catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + guard.text.showValue(baseUrl), e); }
|
|
@@ -1092,14 +1116,14 @@ function _client(op, method, baseUrl, body, headers, opts, proxySnap, onAttempt,
|
|
|
1092
1116
|
var url = _parseUrl(paths(baseUrl, { label: opts.label })[op]);
|
|
1093
1117
|
var transport = checked !== undefined ? checked : _assertTransport(opts);
|
|
1094
1118
|
if (!transport) {
|
|
1095
|
-
var t =
|
|
1119
|
+
var t = snap.tls;
|
|
1096
1120
|
var hasAnchors = t.anchors !== undefined && t.anchors !== null && !(Array.isArray(t.anchors) && t.anchors.length === 0);
|
|
1097
1121
|
if (!hasAnchors && t.useSystemStore !== true) throw E("est/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true -- refusing an unpinned server (RFC 7030 sec. 3.6)");
|
|
1098
1122
|
transport = httpTransport.https({ E: E, errPrefix: "est" });
|
|
1099
1123
|
}
|
|
1100
1124
|
var budgets = {
|
|
1101
|
-
tls:
|
|
1102
|
-
proxy:
|
|
1125
|
+
tls: snap.tls,
|
|
1126
|
+
proxy: snap.proxy,
|
|
1103
1127
|
timeout: guard.limits.cap(opts.timeout, "timeout", DEFAULT_TIMEOUT, { E: E, code: "est/bad-input", min: 1, max: MAX_TIMEOUT }),
|
|
1104
1128
|
maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "maxResponseBytes", constants.LIMITS.HTTP_MAX_RESPONSE_BYTES, { E: E, code: "est/bad-input", min: 1, max: constants.LIMITS.HTTP_MAX_RESPONSE_BYTES }),
|
|
1105
1129
|
maxRedirects: guard.limits.cap(opts.maxRedirects, "maxRedirects", 5, { E: E, code: "est/bad-input", min: 0, max: 32 }),
|
|
@@ -1230,18 +1254,19 @@ function fullcmc(baseUrl, request, opts) {
|
|
|
1230
1254
|
* caller's nested objects, and one of those reads must not be able to change which transport
|
|
1231
1255
|
* the request goes out on, so the value that was checked is the value carried below. */
|
|
1232
1256
|
var carriedTransport = opts.transport;
|
|
1233
|
-
|
|
1234
|
-
|
|
1257
|
+
/** @internal Taken before the own-property copy below, so a tls a defaults bag carries on its
|
|
1258
|
+
* prototype is the one the request uses. */
|
|
1259
|
+
var snap = _doorSnapshot(opts);
|
|
1235
1260
|
der = _cmcRequestDer(request);
|
|
1236
1261
|
wanted = _requestedPublicKeys(der);
|
|
1237
1262
|
sent = _cmcSent(opts, der);
|
|
1238
1263
|
/** @internal The copy below keeps own properties only, and a defaults bag holds its options on
|
|
1239
1264
|
* a prototype, so the transport is put back from the value held at the door. */
|
|
1240
1265
|
opts = _shallowCopy(opts);
|
|
1241
|
-
if (carriedTransport
|
|
1266
|
+
if (carriedTransport != null) opts.transport = carriedTransport;
|
|
1242
1267
|
var body = transferEncode(der);
|
|
1243
1268
|
return _client("fullcmc", "POST", baseUrl, body,
|
|
1244
|
-
{ accept: "application/pkcs7-mime", "content-type": "application/pkcs7-mime; smime-type=CMC-request" }, opts,
|
|
1269
|
+
{ accept: "application/pkcs7-mime", "content-type": "application/pkcs7-mime; smime-type=CMC-request" }, opts, snap, undefined, carriedTransport)
|
|
1245
1270
|
.then(function (res) { return _fullcmcResult(res, opts, wanted, sent); });
|
|
1246
1271
|
} catch (e) {
|
|
1247
1272
|
return _rej(e);
|
|
@@ -1560,7 +1585,7 @@ function _causedBy(e, target) {
|
|
|
1560
1585
|
|
|
1561
1586
|
var ENROLL_HEADERS = intrinsic.assign(intrinsic.create(null), { accept: "application/pkcs7-mime", "content-type": "application/pkcs10" });
|
|
1562
1587
|
|
|
1563
|
-
function _enrollBound(op, baseUrl, builder, opts,
|
|
1588
|
+
function _enrollBound(op, baseUrl, builder, opts, snap) {
|
|
1564
1589
|
/** @internal Held before anything else the caller supplied is read, and carried to the client:
|
|
1565
1590
|
* the builder runs during the handshake, so a later read could answer with a different value. */
|
|
1566
1591
|
var checkedTransport = opts.transport;
|
|
@@ -1590,7 +1615,7 @@ function _enrollBound(op, baseUrl, builder, opts, proxySnap) {
|
|
|
1590
1615
|
.then(null, function (e) { attempt.error = e; attempt.done = true; throw e; });
|
|
1591
1616
|
};
|
|
1592
1617
|
}
|
|
1593
|
-
return _client(op, "POST", baseUrl, hopBody, ENROLL_HEADERS, opts,
|
|
1618
|
+
return _client(op, "POST", baseUrl, hopBody, ENROLL_HEADERS, opts, snap, hopBody, checkedTransport)
|
|
1594
1619
|
.then(function (res) {
|
|
1595
1620
|
if (current && current.error) throw current.error;
|
|
1596
1621
|
if (!current || !current.done) {
|
|
@@ -1606,13 +1631,13 @@ function _enrollBound(op, baseUrl, builder, opts, proxySnap) {
|
|
|
1606
1631
|
});
|
|
1607
1632
|
}
|
|
1608
1633
|
|
|
1609
|
-
function _enroll(op, baseUrl, csrInput, opts,
|
|
1610
|
-
if (typeof csrInput === "function") return _enrollBound(op, baseUrl, csrInput, opts,
|
|
1634
|
+
function _enroll(op, baseUrl, csrInput, opts, snap) {
|
|
1635
|
+
if (typeof csrInput === "function") return _enrollBound(op, baseUrl, csrInput, opts, snap);
|
|
1611
1636
|
var checkedTransport = opts.transport;
|
|
1612
1637
|
var csrDer = _csrDer(csrInput);
|
|
1613
1638
|
var spki = csr.parse(csrDer).subjectPublicKeyInfo;
|
|
1614
1639
|
var body = transferEncode(csrDer);
|
|
1615
|
-
return _client(op, "POST", baseUrl, body, ENROLL_HEADERS, opts,
|
|
1640
|
+
return _client(op, "POST", baseUrl, body, ENROLL_HEADERS, opts, snap, undefined, checkedTransport)
|
|
1616
1641
|
.then(function (res) { return _certsResult(op, res, opts, spki); });
|
|
1617
1642
|
}
|
|
1618
1643
|
|
|
@@ -1635,7 +1660,7 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
|
|
|
1635
1660
|
*
|
|
1636
1661
|
* @opts
|
|
1637
1662
|
* - `transport`: an injected `transport(request) -> Promise<{status, headers, body, tls}>`; default pki.transport.https. It must return a promise of the response; anything else is `est/bad-input`.
|
|
1638
|
-
* - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity }.
|
|
1663
|
+
* - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity, distrustedKeys }. `distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `est/server-key-distrusted`, on every hop including a cross-origin redirect, and a malformed list is `est/bad-input`. A setting not named in that list is refused `est/bad-input`. The verb reads `tls` when it is called, so a change the caller makes to it afterward does not reach the request.
|
|
1639
1664
|
* - `label` -- an OPTIONAL CA label path segment; `timeout` / `maxResponseBytes` / `maxRedirects` -- budgets.
|
|
1640
1665
|
* - `proxy` -- reach the CA through a forward HTTP proxy (`{ url, auth?, tls? }`; see pki.transport).
|
|
1641
1666
|
* - `now` -- receipt time (epoch ms) to render a 202 Retry-After HTTP-date as seconds.
|
|
@@ -1643,6 +1668,8 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
|
|
|
1643
1668
|
* There is no `"auto"`: the scheme is chosen here, not by whatever a server offers. `username` / `password`
|
|
1644
1669
|
* at the top level are the older form and mean Basic. Answered only after the transport authenticated the server.
|
|
1645
1670
|
* - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on an unsafe method.
|
|
1671
|
+
* A value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
|
|
1672
|
+
* before the request goes out.
|
|
1646
1673
|
* @example
|
|
1647
1674
|
* // a live CA uses the default pki.transport.https; here an injected transport returns a canned bag
|
|
1648
1675
|
* var r = await pki.est.cacerts("https://ca.example",
|
|
@@ -1651,10 +1678,10 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
|
|
|
1651
1678
|
*/
|
|
1652
1679
|
function cacerts(baseUrl, opts) {
|
|
1653
1680
|
opts = opts || intrinsic.create(null);
|
|
1654
|
-
var
|
|
1681
|
+
var snap; try { snap = _doorSnapshot(opts); } catch (e) { return _rej(e); }
|
|
1655
1682
|
return _promiseResolve(_Promise, undefined).then(function () {
|
|
1656
1683
|
opts = _knownOpts(opts, CACERTS_OPTS, "cacerts");
|
|
1657
|
-
return _client("cacerts", "GET", baseUrl, null, { accept: "application/pkcs7-mime" }, opts,
|
|
1684
|
+
return _client("cacerts", "GET", baseUrl, null, { accept: "application/pkcs7-mime" }, opts, snap, undefined, opts.transport);
|
|
1658
1685
|
}).then(function (res) { return _certsResult("cacerts", res, opts, null); });
|
|
1659
1686
|
}
|
|
1660
1687
|
|
|
@@ -1695,7 +1722,9 @@ function cacerts(baseUrl, opts) {
|
|
|
1695
1722
|
* - `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `proxy` / `now`, as in pki.est.cacerts.
|
|
1696
1723
|
* - `strict`: reject an enroll response that carries more than the single issued certificate.
|
|
1697
1724
|
* - `username` / `password` / `auth` -- HTTP credentials: top-level `username` / `password` mean Basic; `auth` (`{ scheme: "basic" | "digest", username, password, ... }`, as in pki.est.cacerts) selects Basic or Digest. Answered only after server authorization (empty username allowed).
|
|
1698
|
-
* - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST.
|
|
1725
|
+
* - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST. A
|
|
1726
|
+
* value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
|
|
1727
|
+
* before the request goes out.
|
|
1699
1728
|
* @example
|
|
1700
1729
|
* var req = await pki.csr.sign({ subject: "device.example", subjectPublicKey: signerSpki }, { key: signerKeyPkcs8 });
|
|
1701
1730
|
* // a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
|
|
@@ -1717,10 +1746,10 @@ function cacerts(baseUrl, opts) {
|
|
|
1717
1746
|
*/
|
|
1718
1747
|
function simpleenroll(baseUrl, csrInput, opts) {
|
|
1719
1748
|
opts = opts || intrinsic.create(null);
|
|
1720
|
-
var
|
|
1749
|
+
var snap; try { snap = _doorSnapshot(opts); } catch (e) { return _rej(e); }
|
|
1721
1750
|
return _promiseResolve(_Promise, undefined).then(function () {
|
|
1722
1751
|
opts = _knownOpts(opts, SIMPLEENROLL_OPTS, "simpleenroll");
|
|
1723
|
-
return _enroll("simpleenroll", baseUrl, csrInput, opts,
|
|
1752
|
+
return _enroll("simpleenroll", baseUrl, csrInput, opts, snap);
|
|
1724
1753
|
});
|
|
1725
1754
|
}
|
|
1726
1755
|
|
|
@@ -1757,12 +1786,12 @@ function simpleenroll(baseUrl, csrInput, opts) {
|
|
|
1757
1786
|
*/
|
|
1758
1787
|
function simplereenroll(baseUrl, csrInput, opts) {
|
|
1759
1788
|
opts = opts || intrinsic.create(null);
|
|
1760
|
-
var
|
|
1789
|
+
var snap; try { snap = _doorSnapshot(opts); } catch (e) { return _rej(e); }
|
|
1761
1790
|
return _promiseResolve(_Promise, undefined).then(function () {
|
|
1762
1791
|
opts = _knownOpts(opts, SIMPLEREENROLL_OPTS, "simplereenroll");
|
|
1763
1792
|
if (!opts.oldCert) throw E("est/bad-input", "simplereenroll requires opts.oldCert (the certificate being renewed, RFC 7030 sec. 4.2.2)");
|
|
1764
1793
|
if (typeof csrInput !== "function") reenrollGuard(opts.oldCert, _csrDer(csrInput));
|
|
1765
|
-
return _enroll("simplereenroll", baseUrl, csrInput, opts,
|
|
1794
|
+
return _enroll("simplereenroll", baseUrl, csrInput, opts, snap);
|
|
1766
1795
|
});
|
|
1767
1796
|
}
|
|
1768
1797
|
|
|
@@ -1919,6 +1948,8 @@ async function _serverkeygenResult(res, opts, derived) {
|
|
|
1919
1948
|
* @opts
|
|
1920
1949
|
* - `requestedEncryption` / `expectedRecipientKeyId` / `expectedRecipientIssuerSerial` -- OPTIONAL
|
|
1921
1950
|
* overrides of the CSR-derived recipient coherence; a value that contradicts the CSR is `est/bad-input`.
|
|
1951
|
+
* `requestedEncryption` is a boolean: a value other than `true`, `false`, `null` or an absent
|
|
1952
|
+
* option is refused `est/bad-input`.
|
|
1922
1953
|
* - every option of pki.est.simpleenroll (transport, tls, label, budgets, credentials incl. `auth`).
|
|
1923
1954
|
* @example
|
|
1924
1955
|
* var r = await pki.est.serverkeygen("https://ca.example", csrDer,
|
|
@@ -1927,13 +1958,18 @@ async function _serverkeygenResult(res, opts, derived) {
|
|
|
1927
1958
|
*/
|
|
1928
1959
|
function serverkeygen(baseUrl, csrInput, opts) {
|
|
1929
1960
|
opts = opts || intrinsic.create(null);
|
|
1930
|
-
var
|
|
1961
|
+
var snap; try { snap = _doorSnapshot(opts); } catch (e) { return _rej(e); }
|
|
1931
1962
|
return _promiseResolve(_Promise, undefined).then(function () {
|
|
1932
1963
|
opts = _knownOpts(opts, SERVERKEYGEN_OPTS, "serverkeygen");
|
|
1933
1964
|
var checkedTransport = opts.transport;
|
|
1934
1965
|
var csrDer = _csrDer(csrInput);
|
|
1935
1966
|
var derived = _serverkeygenEncryptionFromCsr(csrDer);
|
|
1936
|
-
|
|
1967
|
+
/** @internal An override is a value the caller stated, and `null` states nothing: it is the
|
|
1968
|
+
* absent form the switch door accepts, so the CSR's own answer stands. Read as present it
|
|
1969
|
+
* coerced to `false` and contradicted a CSR the caller never disagreed with. The value is a
|
|
1970
|
+
* boolean by the time it reaches here, so it is compared rather than coerced. */
|
|
1971
|
+
if (opts.requestedEncryption != null && opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
|
|
1972
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means the recipient is not checked against the CSR, so a null must be refused
|
|
1937
1973
|
if (opts.expectedRecipientKeyId !== undefined) {
|
|
1938
1974
|
if (!Buffer.isBuffer(opts.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId must be a Buffer");
|
|
1939
1975
|
if (derived.expectedRecipientKeyId && !opts.expectedRecipientKeyId.equals(derived.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId contradicts the key identifier the CSR advertised (RFC 7030 sec. 4.4.1)");
|
|
@@ -1947,7 +1983,7 @@ function serverkeygen(baseUrl, csrInput, opts) {
|
|
|
1947
1983
|
if ((opts.expectedRecipientKeyId !== undefined || opts.expectedRecipientIssuerSerial != null) && !derived.requestedEncryption) {
|
|
1948
1984
|
throw E("est/bad-input", "a recipient expectation (expectedRecipientKeyId / expectedRecipientIssuerSerial) implies an encrypted key, but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute (RFC 7030 sec. 4.4.1)");
|
|
1949
1985
|
}
|
|
1950
|
-
return _client("serverkeygen", "POST", baseUrl, transferEncode(csrDer), { accept: "multipart/mixed", "content-type": "application/pkcs10" }, opts,
|
|
1986
|
+
return _client("serverkeygen", "POST", baseUrl, transferEncode(csrDer), { accept: "multipart/mixed", "content-type": "application/pkcs10" }, opts, snap, undefined, checkedTransport)
|
|
1951
1987
|
.then(function (res) { return _serverkeygenResult(res, opts, derived); });
|
|
1952
1988
|
});
|
|
1953
1989
|
}
|
|
@@ -1989,10 +2025,10 @@ function _csrattrsResult(res, opts) {
|
|
|
1989
2025
|
*/
|
|
1990
2026
|
function csrattrs(baseUrl, opts) {
|
|
1991
2027
|
opts = opts || intrinsic.create(null);
|
|
1992
|
-
var
|
|
2028
|
+
var snap; try { snap = _doorSnapshot(opts); } catch (e) { return _rej(e); }
|
|
1993
2029
|
return _promiseResolve(_Promise, undefined).then(function () {
|
|
1994
2030
|
opts = _knownOpts(opts, CSRATTRS_OPTS, "csrattrs");
|
|
1995
|
-
return _client("csrattrs", "GET", baseUrl, null, { accept: "application/csrattrs" }, opts,
|
|
2031
|
+
return _client("csrattrs", "GET", baseUrl, null, { accept: "application/csrattrs" }, opts, snap, undefined, opts.transport);
|
|
1996
2032
|
}).then(function (res) { return _csrattrsResult(res, opts); });
|
|
1997
2033
|
}
|
|
1998
2034
|
|
package/lib/guard-all.js
CHANGED
|
@@ -22,6 +22,7 @@ var secret = require("./guard-secret");
|
|
|
22
22
|
var parsed = require("./guard-parsed");
|
|
23
23
|
var verdict = require("./guard-verdict");
|
|
24
24
|
var async_ = require("./guard-async");
|
|
25
|
+
var distrust = require("./guard-distrust");
|
|
25
26
|
|
|
26
27
|
module.exports = _freeze({
|
|
27
28
|
bytes: bytes,
|
|
@@ -42,4 +43,5 @@ module.exports = _freeze({
|
|
|
42
43
|
parsed: parsed,
|
|
43
44
|
verdict: verdict,
|
|
44
45
|
async: async_,
|
|
46
|
+
distrust: distrust,
|
|
45
47
|
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
// @internal
|
|
5
|
+
|
|
6
|
+
var util = require("util");
|
|
7
|
+
var intrinsic = require("./guard-intrinsic");
|
|
8
|
+
var constants = require("./constants");
|
|
9
|
+
var gBytes = require("./guard-bytes");
|
|
10
|
+
var gList = require("./guard-list");
|
|
11
|
+
var gCrypto = require("./guard-crypto");
|
|
12
|
+
|
|
13
|
+
var _freeze = intrinsic.freeze;
|
|
14
|
+
var _isArray = intrinsic.isArray;
|
|
15
|
+
var _create = intrinsic.create;
|
|
16
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
17
|
+
var _getOwnPropertyDescriptor = intrinsic.getOwnPropertyDescriptor;
|
|
18
|
+
var _bufToString = intrinsic.bufToString;
|
|
19
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
20
|
+
var _fromCharCode = intrinsic.fromCharCode;
|
|
21
|
+
var _isProxy = util.types.isProxy;
|
|
22
|
+
|
|
23
|
+
var DIGEST_BYTES = 32;
|
|
24
|
+
var DIGEST_HEX = 64;
|
|
25
|
+
|
|
26
|
+
/** @internal One hexadecimal character, folded to lower case, or -1. The fold is arithmetic on the
|
|
27
|
+
* character code, so the normalized digest is not the answer of a `toLowerCase` read off the live
|
|
28
|
+
* prototype. */
|
|
29
|
+
function _lowerHexCode(c) {
|
|
30
|
+
if (c >= 48 && c <= 57) return c;
|
|
31
|
+
if (c >= 97 && c <= 102) return c;
|
|
32
|
+
if (c >= 65 && c <= 70) return c + 32;
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _hexEntry(s, E, code, where) {
|
|
37
|
+
if (s.length !== DIGEST_HEX) {
|
|
38
|
+
throw E(code, where + " must be a SHA-256 digest of a SubjectPublicKeyInfo: 64 hexadecimal characters or 32 bytes, got " + s.length + " characters");
|
|
39
|
+
}
|
|
40
|
+
var out = "";
|
|
41
|
+
for (var i = 0; i < DIGEST_HEX; i++) {
|
|
42
|
+
var c = _lowerHexCode(_charCodeAt(s, i));
|
|
43
|
+
if (c === -1) throw E(code, where + " must be 64 hexadecimal characters with no separators; character " + i + " is not a hexadecimal digit");
|
|
44
|
+
out += _fromCharCode(c);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _bytesEntry(v, E, code, where) {
|
|
50
|
+
var copy = gBytes.snapshotSource(v, E, code, where);
|
|
51
|
+
if (copy.length !== DIGEST_BYTES) {
|
|
52
|
+
throw E(code, where + " must be a SHA-256 digest of a SubjectPublicKeyInfo: 32 bytes or 64 hexadecimal characters, got " + copy.length + " bytes");
|
|
53
|
+
}
|
|
54
|
+
return _bufToString(copy, "hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The caller's list of distrusted keys, read once and returned as a frozen list of lower-case hex
|
|
58
|
+
* digests with duplicates removed, or `null` when the option is absent or `null`. Each entry is the
|
|
59
|
+
* SHA-256 digest of a certificate's DER SubjectPublicKeyInfo, given as 64 hexadecimal characters or
|
|
60
|
+
* as 32 bytes.
|
|
61
|
+
*
|
|
62
|
+
* Every slot is read through its descriptor, so a hole, an accessor and a Proxy are refused rather
|
|
63
|
+
* than read: a hole is answered by the array prototype and an accessor answers each read separately,
|
|
64
|
+
* and either one makes the list the check uses differ from the list the caller wrote. The list is a
|
|
65
|
+
* refusal list, so reading it as shorter than it is lets through a key the caller distrusted.
|
|
66
|
+
*
|
|
67
|
+
* @enforced-by behavioral -- the defect is a door that does not call this, which has no rename-proof
|
|
68
|
+
* code shape; the guard is the RED vector each verb carries, driving a listed key through the
|
|
69
|
+
* shipped verb and asserting the refusal.
|
|
70
|
+
*/
|
|
71
|
+
function keys(value, E, code, label) {
|
|
72
|
+
if (value === undefined || value === null) return null;
|
|
73
|
+
if (typeof value !== "object" || _isProxy(value) || !_isArray(value)) {
|
|
74
|
+
throw E(code, label + " must be an array of SHA-256 SubjectPublicKeyInfo digests");
|
|
75
|
+
}
|
|
76
|
+
var n = gList.count(value, "keys");
|
|
77
|
+
if (n > constants.LIMITS.DISTRUSTED_KEYS_MAX) {
|
|
78
|
+
throw E(code, label + " has " + n + " entries, more than the " + constants.LIMITS.DISTRUSTED_KEYS_MAX + " this toolkit reads");
|
|
79
|
+
}
|
|
80
|
+
var seen = _create(null);
|
|
81
|
+
var out = [];
|
|
82
|
+
for (var i = 0; i < n; i++) {
|
|
83
|
+
var where = label + "[" + i + "]";
|
|
84
|
+
var d = _getOwnPropertyDescriptor(value, i);
|
|
85
|
+
if (!d) throw E(code, where + " is a hole; every entry must be present");
|
|
86
|
+
if (d.get || d.set) throw E(code, where + " is an accessor; every entry must be a data property");
|
|
87
|
+
var v = d.value;
|
|
88
|
+
var hex;
|
|
89
|
+
if (typeof v === "string") hex = _hexEntry(v, E, code, where);
|
|
90
|
+
else if (gBytes.isByteSource(v)) hex = _bytesEntry(v, E, code, where);
|
|
91
|
+
else throw E(code, where + " must be 64 hexadecimal characters or 32 bytes");
|
|
92
|
+
if (_hasOwn(seen, hex)) continue;
|
|
93
|
+
seen[hex] = true;
|
|
94
|
+
gList.append(out, hex);
|
|
95
|
+
}
|
|
96
|
+
return _freeze(out);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The lower-case hex SHA-256 of `spkiBytes`, the DER SubjectPublicKeyInfo exactly as a certificate
|
|
100
|
+
* or an anchor carries it.
|
|
101
|
+
*
|
|
102
|
+
* @enforced-by behavioral -- a digest has no rename-proof shape that separates this use from the
|
|
103
|
+
* others `guard-crypto` serves; the guard is the membership vector, which fails if the bytes hashed
|
|
104
|
+
* are not the ones listed.
|
|
105
|
+
*/
|
|
106
|
+
function digest(spkiBytes) {
|
|
107
|
+
return gCrypto.digest("sha256", spkiBytes, { output: "hex" });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @internal The codec and the registry, reached through inline requires: both load this module
|
|
111
|
+
* through guard-all, so both are loaded by the time a digest is asked for. */
|
|
112
|
+
function _codec() {
|
|
113
|
+
return {
|
|
114
|
+
asn1: require("./asn1-der"), // allow:inline-require -- circular load: asn1-der -> guard-all -> this module
|
|
115
|
+
oid: require("./oid"), // allow:inline-require -- circular load: oid -> guard-all -> this module
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @internal The SubjectPublicKeyInfo with an identifier that restricts the key's use replaced by the
|
|
120
|
+
* key's own: id-ecDH and id-ecMQV (RFC 5480 sec. 2.1.2) become ecPublicKey with the curve kept, and
|
|
121
|
+
* RSASSA-PSS and RSAES-OAEP (RFC 4055) become rsaEncryption with NULL. The subjectPublicKey bits are
|
|
122
|
+
* the same key in each pair. Any other identifier is returned unchanged. */
|
|
123
|
+
function _unrestricted(spkiBytes, c) {
|
|
124
|
+
var b = c.asn1.build;
|
|
125
|
+
var root = c.asn1.decode(spkiBytes);
|
|
126
|
+
var alg = root.children[0];
|
|
127
|
+
var name = c.oid.name(c.asn1.read.oid(alg.children[0]));
|
|
128
|
+
if (name === "ecDH" || name === "ecMQV") {
|
|
129
|
+
var params = alg.children.length > 1 ? [b.raw(alg.children[1].bytes)] : [];
|
|
130
|
+
return b.sequence([b.sequence(intrinsic.concatList([b.oid(c.oid.byName("ecPublicKey"))], params)), b.raw(root.children[1].bytes)]);
|
|
131
|
+
}
|
|
132
|
+
if (name === "rsassaPss" || name === "rsaesOaep") {
|
|
133
|
+
return b.sequence([b.sequence([b.oid(c.oid.byName("rsaEncryption")), b.nullValue()]), b.raw(root.children[1].bytes)]);
|
|
134
|
+
}
|
|
135
|
+
return spkiBytes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @internal The key in canonical form, or null when node does not read the bytes as a public key.
|
|
139
|
+
* An elliptic curve key is written with ecPublicKey, its named curve and an uncompressed point,
|
|
140
|
+
* whatever point form or parameter form it was read in. Any other key node writes as a JWK is
|
|
141
|
+
* written the way node writes a key it builds from one, which puts an RSA key under rsaEncryption
|
|
142
|
+
* with NULL. A key with neither form is re-exported as node read it. */
|
|
143
|
+
function _canonicalSpki(spkiBytes) {
|
|
144
|
+
try {
|
|
145
|
+
var c = _codec();
|
|
146
|
+
var spki = _unrestricted(spkiBytes, c);
|
|
147
|
+
var key = gCrypto.publicKey({ key: spki, format: "der", type: "spki" });
|
|
148
|
+
if (gCrypto.keyTypeOf(key) === "ec") {
|
|
149
|
+
var details = gCrypto.keyDetailsOf(key);
|
|
150
|
+
var curveOid = details && typeof details.namedCurve === "string" ? c.oid.byName(details.namedCurve) : undefined;
|
|
151
|
+
if (curveOid !== undefined) {
|
|
152
|
+
var b = c.asn1.build;
|
|
153
|
+
var point = c.asn1.read.bitString(c.asn1.decode(spki).children[1]).bytes;
|
|
154
|
+
var uncompressed = gCrypto.ecConvertPoint(point, details.namedCurve, "uncompressed");
|
|
155
|
+
return b.sequence([b.sequence([b.oid(c.oid.byName("ecPublicKey")), b.oid(curveOid)]), b.bitString(uncompressed, 0)]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
var jwk = null;
|
|
159
|
+
try { jwk = gCrypto.exportKey(key, { format: "jwk" }); }
|
|
160
|
+
catch (_je) {
|
|
161
|
+
// allow:swallow-unverified a key type with no JWK form is re-exported as node read it
|
|
162
|
+
jwk = null;
|
|
163
|
+
}
|
|
164
|
+
if (jwk !== null) key = gCrypto.publicKey({ key: jwk, format: "jwk" });
|
|
165
|
+
return gCrypto.exportKey(key, { type: "spki", format: "der" });
|
|
166
|
+
} catch (_e) {
|
|
167
|
+
// allow:swallow-unverified a key node does not read has no second encoding, and the raw digest still decides
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The digest a distrust entry names a key by: the lower-case hex SHA-256 of the key's
|
|
173
|
+
* SubjectPublicKeyInfo in canonical form, an uncompressed EC point with its named curve and an RSA
|
|
174
|
+
* key with NULL parameters. That is how nearly every certificate encodes a key, so for those it is
|
|
175
|
+
* the digest of the bytes the certificate carries. A key node does not read is digested as given.
|
|
176
|
+
*
|
|
177
|
+
* @enforced-by behavioral -- a digest has no rename-proof shape; the guard is the two-direction
|
|
178
|
+
* re-encoding vectors in guard-distrust.test.js.
|
|
179
|
+
*/
|
|
180
|
+
function keyDigest(spkiBytes) {
|
|
181
|
+
var canonical = _canonicalSpki(spkiBytes);
|
|
182
|
+
return digest(canonical !== null ? canonical : spkiBytes);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Whether the key `spkiBytes` names is on `list`, a value `keys` returned. A `null` or empty list
|
|
186
|
+
* answers `false`. The digest of the bytes as given is compared, and so is `keyDigest`, so an entry
|
|
187
|
+
* `keyDigest` computed matches the key however a certificate encodes it (a compressed EC point, RSA
|
|
188
|
+
* parameters without NULL), and an entry computed over raw bytes matches a certificate with the same
|
|
189
|
+
* bytes.
|
|
190
|
+
*
|
|
191
|
+
* @enforced-by behavioral -- membership has no rename-proof code shape; the RED vectors are the
|
|
192
|
+
* listed-key refusals at each verb, and the re-encodings in guard-distrust.test.js in both directions.
|
|
193
|
+
*/
|
|
194
|
+
function has(list, spkiBytes) {
|
|
195
|
+
if (list === null || list === undefined || list.length === 0 || spkiBytes === null || spkiBytes === undefined) return false;
|
|
196
|
+
if (gList.contains(list, digest(spkiBytes))) return true;
|
|
197
|
+
return gList.contains(list, keyDigest(spkiBytes));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Every entry of two lists that `keys` returned, once, as a frozen list; `null` when both are
|
|
201
|
+
* `null`. A transport default and a per-request list are combined this way, so a request cannot
|
|
202
|
+
* remove a key the default distrusts by naming a shorter list.
|
|
203
|
+
*
|
|
204
|
+
* @enforced-by behavioral -- a union has no rename-proof code shape; the RED vector is a request
|
|
205
|
+
* list that omits a default entry and the default entry still being refused.
|
|
206
|
+
*/
|
|
207
|
+
function union(a, b) {
|
|
208
|
+
if ((a === null || a === undefined) && (b === null || b === undefined)) return null;
|
|
209
|
+
var seen = _create(null);
|
|
210
|
+
var out = [];
|
|
211
|
+
var parts = [a || [], b || []];
|
|
212
|
+
for (var p = 0; p < 2; p++) {
|
|
213
|
+
var part = parts[p];
|
|
214
|
+
for (var i = 0, n = gList.count(part, "union"); i < n; i++) {
|
|
215
|
+
var hex = part[i];
|
|
216
|
+
if (_hasOwn(seen, hex)) continue;
|
|
217
|
+
seen[hex] = true;
|
|
218
|
+
gList.append(out, hex);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return _freeze(out);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = _freeze({ keys: keys, digest: digest, keyDigest: keyDigest, has: has, union: union });
|
package/lib/guard-identifier.js
CHANGED
|
@@ -743,7 +743,32 @@ function assertCallableOption(opts, key, E, code, label, shape) {
|
|
|
743
743
|
// @enforced-by behavioral -- guard: each drives the shipped verb with a non-boolean and asserts the
|
|
744
744
|
// @enforced-by behavioral -- typed refusal, and with false and absence to pin the two off forms.
|
|
745
745
|
function booleanOption(value, E, code, label) {
|
|
746
|
-
|
|
746
|
+
return _switchValue(value, false, E, code, label);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** @internal The same read for a switch whose absent form is ON: an option the caller turns OFF,
|
|
750
|
+
* where the documented default is the state an omitted option selects. The two differ only in what
|
|
751
|
+
* absence means, so the type rule is written once and each boundary names the default it documents.
|
|
752
|
+
* A boundary comparing such an option to `false` answers for every other value as the default
|
|
753
|
+
* state, which is how `certificates: "false"` embedded the certificate the caller asked to
|
|
754
|
+
* withhold; once the value has been through here, that comparison reads a boolean.
|
|
755
|
+
*
|
|
756
|
+
* @enforced-by behavioral -- the per-verb vectors in coercion-safe-options.test.js drive each
|
|
757
|
+
* @enforced-by behavioral -- shipped verb with a non-boolean and assert the typed refusal names the
|
|
758
|
+
* @enforced-by behavioral -- option, and with true, false and absence to pin the three accepted
|
|
759
|
+
* @enforced-by behavioral -- forms. The comparison this replaces has its own codebase-wide
|
|
760
|
+
* @enforced-by behavioral -- detector, optout-switch-read-by-coercion.
|
|
761
|
+
*/
|
|
762
|
+
function booleanOptionDefaultOn(value, E, code, label) {
|
|
763
|
+
return _switchValue(value, true, E, code, label);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** @internal A switch is the boolean it documents, an absent one is the stated default, and every
|
|
767
|
+
* other value is refused. The value is rendered from its own type rather than coerced, so a caller
|
|
768
|
+
* value with a throwing `toString` reports the typed refusal. `null` is read as absence, the same
|
|
769
|
+
* reading the rest of this module gives it. */
|
|
770
|
+
function _switchValue(value, whenAbsent, E, code, label) {
|
|
771
|
+
if (value === undefined || value === null) return whenAbsent;
|
|
747
772
|
if (value === true || value === false) return value;
|
|
748
773
|
throw E(code, label + " must be true or false, got " + _showSwitch(value));
|
|
749
774
|
}
|
|
@@ -776,6 +801,7 @@ function declaresOwn(obj, name) {
|
|
|
776
801
|
module.exports = intrinsic.freeze({
|
|
777
802
|
assertCallable: assertCallable,
|
|
778
803
|
booleanOption: booleanOption,
|
|
804
|
+
booleanOptionDefaultOn: booleanOptionDefaultOn,
|
|
779
805
|
declaresOwn: declaresOwn,
|
|
780
806
|
assertCallableOption: assertCallableOption,
|
|
781
807
|
assertCanonicalOid: assertCanonicalOid,
|
package/lib/guard-json.js
CHANGED
|
@@ -24,7 +24,7 @@ var _hexVal = require("./guard-encoding").hexNibble;
|
|
|
24
24
|
// @enforced-by json-parse-not-via-guard
|
|
25
25
|
function parse(input, ErrorClass, spec) {
|
|
26
26
|
function E(code, message, cause) { return new ErrorClass(code, message, cause); }
|
|
27
|
-
if (spec.maxBytes
|
|
27
|
+
if (spec.maxBytes == null || spec.maxDepth == null) {
|
|
28
28
|
throw new TypeError("guard.json.parse: spec.maxBytes and spec.maxDepth are required");
|
|
29
29
|
}
|
|
30
30
|
var maxBytes = limits.cap(spec.maxBytes, "guard.json.parse spec.maxBytes", undefined);
|