@blamejs/pki 0.6.14 → 0.6.16
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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/ip-utils.js
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- no operator-facing namespace. Textual IP-literal validation, shared so a
|
|
6
|
-
// consumer (pki.lint's commonName-in-SAN check today; the SAN iPAddress GeneralName shorthand
|
|
7
|
-
// and name-constraint surfaces) does not hand-roll a partial regex that misses a valid IPv6 form.
|
|
8
|
-
//
|
|
9
|
-
// The IPv4 grammar is strict per-octet 0-255 (RFC 791); the IPv6 parser follows RFC 4291
|
|
10
|
-
// sec. 2.2 (8 groups of 1-4 hex, one "::" run compressing a contiguous zero run) plus the
|
|
11
|
-
// RFC 4291 sec. 2.5.5.2 / RFC 5952 sec. 5 IPv4-mapped + dual-stack "::ffff:1.2.3.4" tail.
|
|
12
|
-
// This mirrors the vetted validation in the sibling blamejs framework (lib/ip-utils.js);
|
|
13
|
-
// this toolkit keeps its own copy, since Hard rule #1 rules out a runtime dependency.
|
|
4
|
+
// @internal
|
|
14
5
|
|
|
15
|
-
// This parser uses no regular expression. packIpLiteral gates a GeneralName form decision (iPAddress
|
|
16
|
-
// vs dNSName), and a regex is matched through a live, replaceable protocol -- RegExp.prototype.exec /
|
|
17
|
-
// test and String.prototype.match / split consult the regex's own `exec` and the `[Symbol.match]` /
|
|
18
|
-
// `[Symbol.split]` hooks, and RegExp.prototype.compile can mutate a pattern in place -- so co-resident
|
|
19
|
-
// code could steer the decision after load. Instead every byte is read directly through captured string
|
|
20
|
-
// primitives, and separation on a literal is done by scanning with captured indexOf/slice, so nothing the
|
|
21
|
-
// decision rests on is re-dispatchable.
|
|
22
6
|
var intrinsic = require("./guard-intrinsic");
|
|
23
7
|
var _slice = intrinsic.uncurry(String.prototype.slice);
|
|
24
8
|
var _padStart = intrinsic.uncurry(String.prototype.padStart);
|
|
@@ -31,15 +15,7 @@ var _bufferFrom = intrinsic.bufferFrom;
|
|
|
31
15
|
var _bufferAlloc = intrinsic.bufferAlloc;
|
|
32
16
|
var _defineProperty = intrinsic.defineProperty;
|
|
33
17
|
var _Number = intrinsic.Number;
|
|
34
|
-
// Every array here is built and read as own elements only. Appending through a captured Object.defineProperty
|
|
35
|
-
// (not `arr[i] = v`) creates an own element, so a co-resident Array.prototype numeric setter is never invoked
|
|
36
|
-
// and the own element shadows any inherited getter on read; and no Array.prototype.map / concat runs, so
|
|
37
|
-
// Array[Symbol.species] is never consulted. Both are routes a hostile prototype could otherwise use to
|
|
38
|
-
// rewrite a packed octet and make a certificate carry an attacker-chosen address. The emitted octets are
|
|
39
|
-
// written straight into a Buffer, whose integer-indexed store bypasses the prototype entirely.
|
|
40
18
|
function _push(arr, v) { _defineProperty(arr, arr.length, { value: v, writable: true, enumerable: true, configurable: true }); }
|
|
41
|
-
// Split on a literal string separator without String.prototype.split, which consults the separator's
|
|
42
|
-
// `[Symbol.split]`; this scans with captured indexOf/slice only.
|
|
43
19
|
function _splitOn(str, sep) {
|
|
44
20
|
var out = [], from = 0, at;
|
|
45
21
|
while ((at = _indexOf(str, sep, from)) !== -1) { _push(out, _slice(str, from, at)); from = at + sep.length; }
|
|
@@ -47,19 +23,17 @@ function _splitOn(str, sep) {
|
|
|
47
23
|
return out;
|
|
48
24
|
}
|
|
49
25
|
|
|
50
|
-
function _isDigitCode(c) { return c >= 48 && c <= 57; }
|
|
26
|
+
function _isDigitCode(c) { return c >= 48 && c <= 57; }
|
|
51
27
|
function _isHexCode(c) { return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70); }
|
|
52
28
|
|
|
53
|
-
// A strict RFC 791 dotted-quad octet: 1-3 digits, value 0-255, no leading zero beyond "0" itself.
|
|
54
29
|
function _isOctet(p) {
|
|
55
30
|
var n = p.length, i, c;
|
|
56
31
|
if (n < 1 || n > 3) return false;
|
|
57
32
|
for (i = 0; i < n; i += 1) { c = _charCodeAt(p, i); if (!_isDigitCode(c)) return false; }
|
|
58
|
-
if (n > 1 && _charCodeAt(p, 0) === 48) return false;
|
|
33
|
+
if (n > 1 && _charCodeAt(p, 0) === 48) return false;
|
|
59
34
|
return _Number(p) <= 255;
|
|
60
35
|
}
|
|
61
36
|
|
|
62
|
-
// Strict RFC 791 dotted quad: four 0-255 octets, nothing else.
|
|
63
37
|
function isIPv4(s) {
|
|
64
38
|
if (typeof s !== "string") return false;
|
|
65
39
|
var parts = _splitOn(s, ".");
|
|
@@ -68,7 +42,6 @@ function isIPv4(s) {
|
|
|
68
42
|
return true;
|
|
69
43
|
}
|
|
70
44
|
|
|
71
|
-
// A single 1-4 hex-digit IPv6 group.
|
|
72
45
|
function _isHexGroup(g) {
|
|
73
46
|
var n = g.length, i;
|
|
74
47
|
if (n < 1 || n > 4) return false;
|
|
@@ -76,8 +49,6 @@ function _isHexGroup(g) {
|
|
|
76
49
|
return true;
|
|
77
50
|
}
|
|
78
51
|
|
|
79
|
-
// The RFC 4291 sec. 2.5.5.2 dual-stack tail SHAPE: exactly four 1-3 digit groups joined by dots. Validity
|
|
80
|
-
// (0-255 per octet) is a separate isIPv4 check, matching the historical two-step (shape then value).
|
|
81
52
|
function _isDottedQuadShape(t) {
|
|
82
53
|
var q = _splitOn(t, "."), i, p, j;
|
|
83
54
|
if (q.length !== 4) return false;
|
|
@@ -89,14 +60,9 @@ function _isDottedQuadShape(t) {
|
|
|
89
60
|
return true;
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
|
|
93
|
-
// (bad hex group, group count != 8, more than one "::", an over-length group). Accepts the
|
|
94
|
-
// IPv4-mapped / dual-stack ".d.d.d.d" tail.
|
|
95
|
-
var IPV6_TEXT_MAX_LEN = 45; // 8 groups x4 hex + 7 colons + a dotted-quad tail headroom
|
|
63
|
+
var IPV6_TEXT_MAX_LEN = 45;
|
|
96
64
|
function expandIpv6Hex(ip) {
|
|
97
65
|
if (typeof ip !== "string" || ip.length > IPV6_TEXT_MAX_LEN || _indexOf(ip, ":") === -1) return null;
|
|
98
|
-
// RFC 4291 sec. 2.5.5.2 IPv4-mapped / dual-stack: fold a trailing dotted-quad (the suffix after the
|
|
99
|
-
// last colon, when it carries a dot) into two 16-bit hex groups before the pure-hex parse.
|
|
100
66
|
var lastColon = _lastIndexOf(ip, ":");
|
|
101
67
|
var tail = _slice(ip, lastColon + 1);
|
|
102
68
|
if (_indexOf(tail, ".") !== -1) {
|
|
@@ -110,10 +76,8 @@ function expandIpv6Hex(ip) {
|
|
|
110
76
|
var right = dbl.length === 2 ? (dbl[1] === "" ? [] : _splitOn(dbl[1], ":")) : [];
|
|
111
77
|
if (dbl.length === 1 && left.length !== 8) return null;
|
|
112
78
|
var fill = 8 - left.length - right.length;
|
|
113
|
-
// A "::" run MUST compress at least one zero group (RFC 4291 sec. 2.2 / RFC 5952 sec.
|
|
114
|
-
// 4.2.2): a "::" with fill 0 is a full 8-group address that must not use compression.
|
|
115
79
|
if (dbl.length === 2 ? fill < 1 : fill !== 0) return null;
|
|
116
|
-
var groups = left, i;
|
|
80
|
+
var groups = left, i;
|
|
117
81
|
for (i = 0; i < fill; i += 1) _push(groups, "0");
|
|
118
82
|
for (i = 0; i < right.length; i += 1) _push(groups, right[i]);
|
|
119
83
|
if (groups.length !== 8) return null;
|
|
@@ -126,17 +90,12 @@ function expandIpv6Hex(ip) {
|
|
|
126
90
|
return hex;
|
|
127
91
|
}
|
|
128
92
|
|
|
129
|
-
// Is `s` a syntactically valid IPv4 or IPv6 textual literal?
|
|
130
93
|
function isIpLiteral(s) { return isIPv4(s) || expandIpv6Hex(s) !== null; }
|
|
131
94
|
|
|
132
|
-
// Pack an IPv4/IPv6 textual literal to its network-order octets (4 for IPv4, 16 for IPv6),
|
|
133
|
-
// or null when `s` is not a valid literal. The binary inverse of the textual forms isIPv4 /
|
|
134
|
-
// expandIpv6Hex validate, so a SAN/GeneralName iPAddress (RFC 5280 sec. 4.2.1.6, always a bare
|
|
135
|
-
// host address of 4 or 16 octets) can be given as a string instead of a pre-packed Buffer.
|
|
136
95
|
function packIpLiteral(s) {
|
|
137
96
|
if (isIPv4(s)) {
|
|
138
97
|
var parts = _splitOn(s, "."), buf = _bufferAlloc(4), i;
|
|
139
|
-
for (i = 0; i < 4; i += 1) buf[i] = _Number(parts[i]);
|
|
98
|
+
for (i = 0; i < 4; i += 1) buf[i] = _Number(parts[i]);
|
|
140
99
|
return buf;
|
|
141
100
|
}
|
|
142
101
|
var hex = expandIpv6Hex(s);
|
package/lib/jose.js
CHANGED
|
@@ -42,12 +42,6 @@ var webcrypto = wcEngine.webcrypto;
|
|
|
42
42
|
var frameworkError = require("./framework-error");
|
|
43
43
|
var guard = require("./guard-all");
|
|
44
44
|
var edwardsPoint = require("./edwards-point");
|
|
45
|
-
// What this module reads from the runtime while it is deciding something, taken at load. A JWS is
|
|
46
|
-
// signed and verified against a header the caller owns, and the reads below are the ones that decide
|
|
47
|
-
// what that header IS: whether a value is a list, whether a name is the caller's own, which
|
|
48
|
-
// conversion turns a claim into the string compared against a profile. A caller who receives control
|
|
49
|
-
// afterwards -- through an accessor on the header, or a `toJSON` while it is being serialized -- can
|
|
50
|
-
// replace any of them, and every check around them still runs and still passes. See guard-intrinsic.
|
|
51
45
|
var intrinsic = require("./guard-intrinsic");
|
|
52
46
|
var _isArray = intrinsic.isArray;
|
|
53
47
|
var _hasOwn = intrinsic.hasOwn;
|
|
@@ -66,7 +60,6 @@ function E(code, message, cause) { return new JoseError(code, message, cause); }
|
|
|
66
60
|
|
|
67
61
|
var LIMITS = constants.LIMITS;
|
|
68
62
|
|
|
69
|
-
// ---- strict base64url codec (RFC 7515 sec. 2 / RFC 4648 sec. 5) ----------
|
|
70
63
|
|
|
71
64
|
/**
|
|
72
65
|
* @primitive pki.jose.base64url.encode
|
|
@@ -84,8 +77,6 @@ var LIMITS = constants.LIMITS;
|
|
|
84
77
|
*/
|
|
85
78
|
function b64uEncode(bytes) {
|
|
86
79
|
if (!_isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
|
|
87
|
-
// Re-view through the byte guard: a detached-backed Buffer reads as zero-length,
|
|
88
|
-
// so a bare .toString would silently encode "" -- fail closed instead.
|
|
89
80
|
bytes = guard.bytes.view(bytes, JoseError, "jose/bad-input", "base64url.encode input");
|
|
90
81
|
return _bufToString(bytes, "base64url");
|
|
91
82
|
}
|
|
@@ -107,13 +98,9 @@ function b64uEncode(bytes) {
|
|
|
107
98
|
* pki.jose.base64url.decode("AQID"); // -> <Buffer 01 02 03>
|
|
108
99
|
*/
|
|
109
100
|
function b64uDecode(text) {
|
|
110
|
-
// The strict alphabet + impossible-length + canonical-round-trip discipline
|
|
111
|
-
// lives once in the shared encoding guard (RFC 4648 sec. 5 / RFC 8555 sec. 6.1);
|
|
112
|
-
// the frozen jose/bad-base64url code is threaded through.
|
|
113
101
|
return guard.encoding.base64url(text, null, E, "jose/bad-base64url", "base64url value");
|
|
114
102
|
}
|
|
115
103
|
|
|
116
|
-
// ---- strict bounded JSON reader ------------------------------------------
|
|
117
104
|
|
|
118
105
|
/**
|
|
119
106
|
* @primitive pki.jose.parseJson
|
|
@@ -134,9 +121,6 @@ function b64uDecode(text) {
|
|
|
134
121
|
* pki.jose.parseJson('{"a":1}'); // -> { a: 1 }
|
|
135
122
|
*/
|
|
136
123
|
function parseJson(input) {
|
|
137
|
-
// The strict bounded reader (byte + depth caps, duplicate-member reject at every
|
|
138
|
-
// depth, __proto__-safe own-property assignment, fatal UTF-8, RFC 8259 grammar)
|
|
139
|
-
// lives once in the shared JSON guard; the frozen jose/* codes are threaded through.
|
|
140
124
|
return guard.json.parse(input, E, {
|
|
141
125
|
maxBytes: LIMITS.JSON_MAX_BYTES, maxDepth: LIMITS.JSON_MAX_DEPTH,
|
|
142
126
|
badJson: "jose/bad-json", tooDeep: "jose/too-deep", duplicateMember: "jose/duplicate-member",
|
|
@@ -144,15 +128,7 @@ function parseJson(input) {
|
|
|
144
128
|
});
|
|
145
129
|
}
|
|
146
130
|
|
|
147
|
-
// ---- JOSE algorithm registry (RFC 7518 / 8037 / 9964) --------------------
|
|
148
131
|
|
|
149
|
-
// One data row per JWS `alg`, keyed for O(1) resolution -- never a switch. Each
|
|
150
|
-
// row binds the alg to its JWK key type (and, for EC/OKP, the exact curve), the
|
|
151
|
-
// hash, and the WebCrypto import/verify parameters, plus the exact signature
|
|
152
|
-
// byte length (fixed for EC / EdDSA / ML-DSA; the RSA modulus length for RS*/PS*,
|
|
153
|
-
// resolved from the key). Binding alg->kty in DATA is what kills the RS256->HS256
|
|
154
|
-
// key-confusion class: an ES256 header with an RSA key is rejected here, before
|
|
155
|
-
// any crypto call. `HS*`/`none` are absent from this table by construction.
|
|
156
132
|
var SIG_ALGS = {
|
|
157
133
|
ES256: { kty: "EC", crv: "P-256", hash: "SHA-256", subtle: "ECDSA", sigBytes: 64 },
|
|
158
134
|
ES384: { kty: "EC", crv: "P-384", hash: "SHA-384", subtle: "ECDSA", sigBytes: 96 },
|
|
@@ -169,66 +145,41 @@ var SIG_ALGS = {
|
|
|
169
145
|
"ML-DSA-87": { kty: "AKP", subtle: "ML-DSA-87", sigBytes: 4627 },
|
|
170
146
|
};
|
|
171
147
|
|
|
172
|
-
// The MAC table exists ONLY for the RFC 8555 sec. 7.3.4 externalAccountBinding
|
|
173
|
-
// inner JWS -- HS* appears nowhere in the outer/signature profiles, so an
|
|
174
|
-
// `alg:"HS256"` on an ACME request rejects (the RS256->HS256 downgrade class).
|
|
175
148
|
var MAC_ALGS = {
|
|
176
149
|
HS256: { kty: "oct", hash: "SHA-256", sigBytes: 32 },
|
|
177
150
|
HS384: { kty: "oct", hash: "SHA-384", sigBytes: 48 },
|
|
178
151
|
HS512: { kty: "oct", hash: "SHA-512", sigBytes: 64 },
|
|
179
152
|
};
|
|
180
153
|
|
|
181
|
-
// The header parameter names this implementation understands. A `crit` naming
|
|
182
|
-
// anything outside this set is unprocessable -> the JWS is invalid (RFC 7515
|
|
183
|
-
// sec. 4.1.11); ACME defines no extension params, so any `crit` entry rejects.
|
|
184
154
|
var UNDERSTOOD_HEADER = { alg: 1, nonce: 1, url: 1, jwk: 1, kid: 1, crit: 1 };
|
|
185
155
|
|
|
186
|
-
// ---- JWS profiles (declarative -- one table drives sign AND verify) ------
|
|
187
156
|
|
|
188
|
-
// Each profile is DATA: the algorithm table it permits, the required params,
|
|
189
|
-
// whether `nonce` is required/forbidden, and the key-identification rule
|
|
190
|
-
// (exactly-one-of jwk/kid, or one specifically). The profile carries the rule,
|
|
191
|
-
// not the code path -- so the EAB-inner table ACCEPTS HS256 where the ACME-outer
|
|
192
|
-
// table REJECTS it, from the same walker.
|
|
193
157
|
var PROFILES = {
|
|
194
158
|
"acme-outer": { algs: SIG_ALGS, nonce: "required", keyId: "one-of", requireUrl: true },
|
|
195
159
|
"eab-inner": { algs: MAC_ALGS, nonce: "forbidden", keyId: "kid", requireUrl: true },
|
|
196
160
|
"keychange-inner": { algs: SIG_ALGS, nonce: "forbidden", keyId: "jwk", requireUrl: true },
|
|
197
161
|
};
|
|
198
162
|
|
|
199
|
-
// Validate a decoded protected header against a profile. Returns the resolved
|
|
200
|
-
// { alg, algRow, header }; throws a typed jose/* fault on any violation.
|
|
201
163
|
function _checkHeader(header, profileName) {
|
|
202
164
|
var profile = PROFILES[profileName];
|
|
203
165
|
if (!profile) throw E("jose/bad-input", "unknown JWS profile " + _stringify(profileName));
|
|
204
166
|
if (!header || typeof header !== "object" || _isArray(header)) throw E("jose/bad-header", "the protected header must be a JSON object");
|
|
205
|
-
// Every field is read as an OWN property. JSON.stringify (used by sign) omits
|
|
206
|
-
// inherited members, so an alg/nonce/url living on the prototype (a polluted or
|
|
207
|
-
// Object.create(...) header) would pass here yet serialize to a header MISSING
|
|
208
|
-
// them -- so require own-ness, matching what the serialized JWS will actually carry.
|
|
209
167
|
var own = function (k) { return _hasOwn(header, k); };
|
|
210
|
-
// b64 (RFC 7797) MUST NOT be present at all -- the unencoded-payload downgrade.
|
|
211
168
|
if (own("b64")) throw E("jose/bad-jws", "the RFC 7797 b64 header parameter is forbidden");
|
|
212
169
|
var alg = header.alg;
|
|
213
170
|
if (!own("alg") || typeof alg !== "string") throw E("jose/bad-header", "the protected header must carry a string alg");
|
|
214
171
|
var algRow = profile.algs[alg];
|
|
215
172
|
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " + _stringify(alg) + " for this JWS profile");
|
|
216
173
|
if (profile.requireUrl && (!own("url") || typeof header.url !== "string" || header.url.length === 0)) throw E("jose/bad-header", "the protected header must carry a non-empty string url");
|
|
217
|
-
// nonce discipline.
|
|
218
174
|
var hasNonce = own("nonce");
|
|
219
175
|
if (profile.nonce === "required") {
|
|
220
|
-
// A Replay-Nonce is 1*base64url -- NON-empty (RFC 8555 sec. 6.5.2); "" decodes as
|
|
221
|
-
// canonical base64url but no server issues it, so reject it locally.
|
|
222
176
|
if (!hasNonce || typeof header.nonce !== "string" || header.nonce.length === 0) throw E("jose/bad-header", "the protected header must carry a non-empty nonce");
|
|
223
|
-
// a nonce that is not valid base64url is malformed (RFC 8555 sec. 6.5.2).
|
|
224
177
|
try { b64uDecode(header.nonce); } catch (e) { throw E("jose/bad-header", "the nonce is not valid base64url", e); }
|
|
225
178
|
} else if (profile.nonce === "forbidden" && hasNonce) {
|
|
226
179
|
throw E("jose/bad-header", "a nonce is forbidden in this inner JWS");
|
|
227
180
|
}
|
|
228
|
-
// key identification.
|
|
229
181
|
var hasJwk = own("jwk");
|
|
230
182
|
var hasKid = own("kid");
|
|
231
|
-
// A kid is a non-empty account URL string.
|
|
232
183
|
if (hasKid && (typeof header.kid !== "string" || header.kid.length === 0)) throw E("jose/bad-header", "kid must be a non-empty string");
|
|
233
184
|
if (profile.keyId === "one-of") {
|
|
234
185
|
if (hasJwk === hasKid) throw E("jose/bad-header", "the protected header must carry EXACTLY ONE of jwk / kid");
|
|
@@ -238,7 +189,6 @@ function _checkHeader(header, profileName) {
|
|
|
238
189
|
if (!hasJwk || hasKid) throw E("jose/bad-header", "this JWS must carry an embedded jwk, not kid");
|
|
239
190
|
}
|
|
240
191
|
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" || _isArray(header.jwk))) throw E("jose/bad-header", "jwk must be a JSON object");
|
|
241
|
-
// crit: protected-only, non-empty, no standard names, no duplicates, all understood.
|
|
242
192
|
if (_hasOwn(header, "crit")) {
|
|
243
193
|
var crit = header.crit;
|
|
244
194
|
if (!_isArray(crit) || crit.length === 0) throw E("jose/bad-crit", "crit must be a non-empty array");
|
|
@@ -249,72 +199,53 @@ function _checkHeader(header, profileName) {
|
|
|
249
199
|
if (_hasOwn(seen, name)) throw E("jose/bad-crit", "duplicate crit entry " + _stringify(name));
|
|
250
200
|
seen[name] = 1;
|
|
251
201
|
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " + _stringify(name));
|
|
252
|
-
// Anything else is an extension parameter this implementation does not process.
|
|
253
202
|
throw E("jose/bad-crit", "unprocessed critical header parameter " + _stringify(name));
|
|
254
203
|
}
|
|
255
204
|
}
|
|
256
205
|
return { alg: alg, algRow: algRow, header: header };
|
|
257
206
|
}
|
|
258
207
|
|
|
259
|
-
// ---- flattened-JWS verify (RFC 7515 sec. 5.2) ----------------------------
|
|
260
208
|
|
|
261
|
-
// Resolve the WebCrypto import parameters for a JWK under an alg row.
|
|
262
209
|
function _importParams(algRow, jwk) {
|
|
263
210
|
if (algRow.kty === "EC") return { name: "ECDSA", namedCurve: algRow.crv };
|
|
264
211
|
if (algRow.kty === "RSA") return { name: algRow.subtle, hash: algRow.hash };
|
|
265
|
-
if (algRow.kty === "OKP") return { name: jwk.crv };
|
|
266
|
-
if (algRow.kty === "AKP") return { name: algRow.subtle };
|
|
267
|
-
if (algRow.kty === "oct") return { name: "HMAC", hash: algRow.hash };
|
|
212
|
+
if (algRow.kty === "OKP") return { name: jwk.crv };
|
|
213
|
+
if (algRow.kty === "AKP") return { name: algRow.subtle };
|
|
214
|
+
if (algRow.kty === "oct") return { name: "HMAC", hash: algRow.hash };
|
|
268
215
|
throw E("jose/bad-alg", "unsupported key type");
|
|
269
216
|
}
|
|
270
217
|
|
|
271
|
-
// The WebCrypto verify/sign algorithm for an alg row + JWK.
|
|
272
218
|
function _cryptoAlg(algRow, jwk, key) {
|
|
273
219
|
if (algRow.kty === "EC") return { name: "ECDSA", hash: algRow.hash };
|
|
274
220
|
if (algRow.kty === "RSA") return algRow.subtle === "RSA-PSS" ? { name: "RSA-PSS", saltLength: algRow.saltLength } : { name: "RSASSA-PKCS1-V1_5" };
|
|
275
|
-
// EdDSA carries the curve (Ed25519 / Ed448) as the WebCrypto algorithm name. In
|
|
276
|
-
// a kid-signed request there is no header/opts jwk to read it from, so fall back
|
|
277
|
-
// to the signing key's own algorithm -- an Ed25519 account key must stay usable
|
|
278
|
-
// after account creation, not only in the embedded-jwk (newAccount) case.
|
|
279
221
|
if (algRow.kty === "OKP") return { name: (jwk && jwk.crv) || (key && key.algorithm && key.algorithm.name) };
|
|
280
222
|
if (algRow.kty === "AKP") return { name: algRow.subtle };
|
|
281
223
|
if (algRow.kty === "oct") return { name: "HMAC" };
|
|
282
224
|
throw E("jose/bad-alg", "unsupported key type");
|
|
283
225
|
}
|
|
284
226
|
|
|
285
|
-
// The key type the JWK MUST have for this alg (fail closed on confusion).
|
|
286
227
|
function _assertKeyType(algRow, jwk) {
|
|
287
228
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
288
229
|
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + _stringify(jwk.kty) + " does not match the algorithm");
|
|
289
230
|
if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
|
|
290
231
|
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + _stringify(jwk.crv));
|
|
291
|
-
// node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
|
|
292
|
-
// forged signature; reject a non-full-order Edwards point through the shared gate, the same check
|
|
293
|
-
// every EdDSA verify path in the toolkit applies.
|
|
294
232
|
if (algRow.kty === "OKP" && typeof jwk.x === "string" && !edwardsPoint.validate(b64uDecode(jwk.x), jwk.crv === "Ed25519" ? 6 : 7)) {
|
|
295
233
|
throw E("jose/bad-key", "the OKP public key is not a valid, full-order Edwards point");
|
|
296
234
|
}
|
|
297
|
-
// An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
|
|
298
|
-
// the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
|
|
299
235
|
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + _stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
|
|
300
236
|
}
|
|
301
237
|
|
|
302
|
-
// The exact signature byte length this alg + key must produce.
|
|
303
238
|
function _expectedSigBytes(algRow, jwk, key) {
|
|
304
239
|
if (algRow.sigBytes != null) return algRow.sigBytes;
|
|
305
|
-
// EdDSA: the curve fixes the length. On the verify side it is read from the JWK;
|
|
306
|
-
// on the sign side (kid mode, no JWK) it comes from the signing key's algorithm.
|
|
307
240
|
if (algRow.sigBytesByCrv) return algRow.sigBytesByCrv[(jwk && jwk.crv) || (key && key.algorithm && key.algorithm.name)] || null;
|
|
308
241
|
if (algRow.kty === "RSA") {
|
|
309
|
-
if (jwk && typeof jwk.n === "string") return b64uDecode(jwk.n).length;
|
|
310
|
-
if (key && key.algorithm && key.algorithm.modulusLength) return key.algorithm.modulusLength / 8;
|
|
242
|
+
if (jwk && typeof jwk.n === "string") return b64uDecode(jwk.n).length;
|
|
243
|
+
if (key && key.algorithm && key.algorithm.modulusLength) return key.algorithm.modulusLength / 8;
|
|
311
244
|
return null;
|
|
312
245
|
}
|
|
313
246
|
return null;
|
|
314
247
|
}
|
|
315
248
|
|
|
316
|
-
// The JWK members that carry PRIVATE key material across every key type -- EC/OKP
|
|
317
|
-
// `d`, the RSA CRT set, the symmetric `k`, and the AKP `priv`.
|
|
318
249
|
var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
|
|
319
250
|
|
|
320
251
|
/**
|
|
@@ -384,10 +315,6 @@ function assertPublicJwk(jwk) {
|
|
|
384
315
|
* var v = await pki.jose.verify(jws, { profile: "acme-outer", key: accountJwk });
|
|
385
316
|
* v.header.alg; // -> "ES256"
|
|
386
317
|
*/
|
|
387
|
-
// `key` NAMES the key this message must be signed under and `profile` selects which header rules
|
|
388
|
-
// apply -- both narrow what verifies, so a misspelling of either silently widens it back to the
|
|
389
|
-
// default. That is the shape this check exists for: the caller asked for something stricter and got
|
|
390
|
-
// the looser behavior, with nothing said.
|
|
391
318
|
var _VERIFY_KEYS = { key: 1, profile: 1 };
|
|
392
319
|
async function verify(jws, opts) {
|
|
393
320
|
opts = opts || {};
|
|
@@ -400,17 +327,6 @@ async function verify(jws, opts) {
|
|
|
400
327
|
var header = parseJson(b64uDecode(jws.protected));
|
|
401
328
|
var profileName = opts.profile || "acme-outer";
|
|
402
329
|
var checked = _checkHeader(header, profileName);
|
|
403
|
-
// A caller supplying opts.key is NAMING the key this message must be signed under. A profile
|
|
404
|
-
// that also permits an embedded jwk lets the message carry one, and preferring that would let
|
|
405
|
-
// the sender choose which key verifies it -- exactly the question opts.key was asked to settle.
|
|
406
|
-
// So when both are present they must be the SAME key, compared as RFC 7638 thumbprints: that
|
|
407
|
-
// canonicalizes member order and ignores members outside the key itself, so two spellings of
|
|
408
|
-
// one key agree and two different keys cannot.
|
|
409
|
-
// A SUPPLIED key that cannot be used is refused, never quietly treated as absent. Falling back
|
|
410
|
-
// to the embedded jwk there would drop the caller's intent to pin a signer at the moment it
|
|
411
|
-
// matters most -- a `key` that came back null from a lookup would verify against whatever the
|
|
412
|
-
// message carried, and the verdict would report the embedded key as though none was named.
|
|
413
|
-
// `undefined` alone means "not supplied", so spreading an options object stays safe.
|
|
414
330
|
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
|
|
415
331
|
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");
|
|
416
332
|
}
|
|
@@ -436,20 +352,15 @@ async function verify(jws, opts) {
|
|
|
436
352
|
var sig = b64uDecode(jws.signature);
|
|
437
353
|
var want = _expectedSigBytes(checked.algRow, jwk);
|
|
438
354
|
if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
|
|
439
|
-
// Signing input from the RECEIVED base64url strings verbatim (never a re-serialize).
|
|
440
355
|
var signingInput = _bufferFrom(jws.protected + "." + jws.payload, "ascii");
|
|
441
356
|
var key;
|
|
442
357
|
try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
|
|
443
358
|
catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
|
|
444
359
|
var ok = await webcrypto.subtle.verify(_cryptoAlg(checked.algRow, jwk), key, sig, signingInput);
|
|
445
360
|
if (!ok) throw E("jose/verify-failed", "the JWS signature did not verify");
|
|
446
|
-
// WHICH key answered. A caller auditing the decision needs "checked against the key I named"
|
|
447
|
-
// to be distinguishable from "checked against the one the message brought with it"; the two
|
|
448
|
-
// are different claims and only the first says anything about who the signer is.
|
|
449
361
|
return { header: header, payload: b64uDecode(jws.payload), keySource: keySource };
|
|
450
362
|
}
|
|
451
363
|
|
|
452
|
-
// ---- flattened-JWS sign (RFC 7515 sec. 5.1) ------------------------------
|
|
453
364
|
|
|
454
365
|
/**
|
|
455
366
|
* @primitive pki.jose.sign
|
|
@@ -484,31 +395,16 @@ async function verify(jws, opts) {
|
|
|
484
395
|
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
|
|
485
396
|
* var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
|
|
486
397
|
*/
|
|
487
|
-
// The public JWK has two sources and the embedded one wins, so opts.jwk is read only in kid mode.
|
|
488
|
-
// Supplied alongside a header that embeds its own, it selected nothing: the signature parameters
|
|
489
|
-
// came from the header's key while the caller had named a different one.
|
|
490
398
|
var _SIGN_KEYS = { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 };
|
|
491
399
|
var _SIGN_KEYS_EMBEDDED_JWK = { protected: 1, payload: 1, key: 1, profile: 1 };
|
|
492
400
|
async function sign(opts) {
|
|
493
401
|
opts = opts || {};
|
|
494
|
-
// The options are checked BEFORE the header is touched. Serializing it runs the caller's own code
|
|
495
|
-
// -- a toJSON, an accessor -- and that code can delete an option from the very bag being checked,
|
|
496
|
-
// so a misspelling reached the door already removed and the message went out signed.
|
|
497
402
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
|
|
498
|
-
// The header is then serialized ONCE, and every read below comes from a snapshot of those exact
|
|
499
|
-
// bytes: the header validated, the header embedded in the JWS and the key confirmed against the
|
|
500
|
-
// produced signature are then all the same values. Read live, a caller getter answers each of
|
|
501
|
-
// those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
|
|
502
|
-
// the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
|
|
503
403
|
var headerJson;
|
|
504
404
|
try { headerJson = _stringify(opts.protected); }
|
|
505
405
|
catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
|
|
506
|
-
// Read back through the same bounded reader a received JWS goes through, so a header this side
|
|
507
|
-
// produces is held to the caps and the duplicate-member rule the other side enforces.
|
|
508
406
|
var headerBytes = headerJson === undefined ? null : _bufferFrom(headerJson, "utf8");
|
|
509
407
|
var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
|
|
510
|
-
// Which of the two key sources is in force is known only once the header has been read, so the
|
|
511
|
-
// narrower table runs here. It re-walks the bag, which also catches a name that code added.
|
|
512
408
|
var embedsJwk = !!(header && typeof header === "object" && header.jwk);
|
|
513
409
|
if (embedsJwk) {
|
|
514
410
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
|
|
@@ -518,47 +414,22 @@ async function sign(opts) {
|
|
|
518
414
|
}
|
|
519
415
|
var payload = opts.payload;
|
|
520
416
|
if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
|
|
521
|
-
// Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
|
|
522
|
-
// and would be silently signed as an empty POST-as-GET body -- fail closed instead.
|
|
523
417
|
payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
|
|
524
|
-
// A non-CryptoKey (a raw Buffer, a string, a JWK object) would reach subtle.sign
|
|
525
|
-
// and throw a bare TypeError; require a CryptoKey (it carries an `algorithm`) so
|
|
526
|
-
// every caller -- and every acme builder that composes sign -- fails closed.
|
|
527
418
|
if (!wcEngine.isCryptoKeyLike(opts.key)) throw E("jose/bad-input", "a private or secret CryptoKey (opts.key) is required");
|
|
528
419
|
var checked = _checkHeader(header, opts.profile || "acme-outer");
|
|
529
|
-
// An RSA / HMAC key binds its hash at import; the signature-length pin below cannot
|
|
530
|
-
// see a hash mismatch (the modulus / MAC size is unchanged), so a SHA-512 key under
|
|
531
|
-
// RS256 would sign here yet fail verification. Reject the hash disagreement up front.
|
|
532
420
|
if ((checked.algRow.kty === "RSA" || checked.algRow.kty === "oct") &&
|
|
533
421
|
(!opts.key.algorithm.hash || opts.key.algorithm.hash.name !== checked.algRow.hash)) {
|
|
534
422
|
throw E("jose/bad-key", "the signing key's hash does not match the algorithm " + checked.alg);
|
|
535
423
|
}
|
|
536
|
-
// An embedded jwk (jwk mode / keychange-inner) is the public key a verifier will
|
|
537
|
-
// use, so it MUST match the alg exactly as verify enforces -- otherwise the header
|
|
538
|
-
// advertises a key that cannot verify this signature. (No jwk in kid mode.) It is
|
|
539
|
-
// also PUBLISHED to the server, so it must be public-only: an accidentally exported
|
|
540
|
-
// private JWK must never be serialized into the protected header.
|
|
541
424
|
if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
|
|
542
|
-
var protectedB64 = b64uEncode(headerBytes);
|
|
425
|
+
var protectedB64 = b64uEncode(headerBytes);
|
|
543
426
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
544
427
|
var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
|
|
545
428
|
var jwk = header.jwk || opts.jwk || {};
|
|
546
|
-
// opts.key is documented as a CryptoKey, so one created outside this engine signs here too: it
|
|
547
|
-
// carries none of the material this engine signs with, and is re-imported through it rather than
|
|
548
|
-
// reaching subtle.sign as a key it cannot read. A key whose material this process cannot reach at
|
|
549
|
-
// all -- non-extractable, or held behind another implementation's interface -- is refused there.
|
|
550
429
|
var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
|
|
551
430
|
var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
|
|
552
|
-
// Pin the produced signature length the same way verify does. A key whose curve /
|
|
553
|
-
// hash / modulus does not match the header alg (a P-384 key under ES256, an
|
|
554
|
-
// HS512 key under HS256) still signs, but emits the wrong length -- caught here
|
|
555
|
-
// so the builder fails locally instead of shipping a JWS the server will reject.
|
|
556
431
|
var want = _expectedSigBytes(checked.algRow, jwk, opts.key);
|
|
557
432
|
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)");
|
|
558
|
-
// In jwk mode the EMBEDDED public jwk is what verifiers use. Confirm it actually
|
|
559
|
-
// verifies this signature -- i.e. it is the signing key's public half -- so a
|
|
560
|
-
// key-pair mix-up (a jwk from a different key of the same type) fails locally
|
|
561
|
-
// instead of producing a JWS the CA rejects. Same-type/curve is not enough.
|
|
562
433
|
if (header.jwk) {
|
|
563
434
|
var pubKey;
|
|
564
435
|
try { pubKey = await webcrypto.subtle.importKey("jwk", header.jwk, _importParams(checked.algRow, header.jwk), false, ["verify"]); }
|
|
@@ -570,17 +441,13 @@ async function sign(opts) {
|
|
|
570
441
|
return { protected: protectedB64, payload: payloadB64, signature: b64uEncode(sigBuf) };
|
|
571
442
|
}
|
|
572
443
|
|
|
573
|
-
// ---- JWK thumbprint (RFC 7638 + RFC 8037 + RFC 9964) ---------------------
|
|
574
444
|
|
|
575
|
-
// The required-member templates per key type (RFC 7638 sec. 3.2 / RFC 8037 sec. 2
|
|
576
|
-
// / RFC 9964). ONLY these members, in lexicographic order, no whitespace, feed
|
|
577
|
-
// the SHA-256; optional members (alg/use/kid) never enter the hash.
|
|
578
445
|
var THUMBPRINT_MEMBERS = {
|
|
579
446
|
EC: ["crv", "kty", "x", "y"],
|
|
580
447
|
RSA: ["e", "kty", "n"],
|
|
581
448
|
oct: ["k", "kty"],
|
|
582
449
|
OKP: ["crv", "kty", "x"],
|
|
583
|
-
AKP: ["alg", "kty", "pub"],
|
|
450
|
+
AKP: ["alg", "kty", "pub"],
|
|
584
451
|
};
|
|
585
452
|
|
|
586
453
|
/**
|
|
@@ -613,8 +480,6 @@ async function thumbprint(jwk) {
|
|
|
613
480
|
for (var i = 0; i < members.length; i++) {
|
|
614
481
|
var m = members[i];
|
|
615
482
|
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
|
|
616
|
-
// The canonical form IS the thumbprint preimage (RFC 7638 sec. 3.3), so the serializer that
|
|
617
|
-
// builds it is the captured one: a replacement changes the identity every consumer compares.
|
|
618
483
|
_push(parts, _stringify(m) + ":" + _stringify(jwk[m]));
|
|
619
484
|
}
|
|
620
485
|
var canonical = "{" + _join(parts, ",") + "}";
|