@blamejs/pki 0.9.5 → 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.
@@ -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-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 === undefined || spec.maxDepth === undefined) {
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);