@blamejs/pki 0.4.2 → 0.4.3
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 +22 -1
- package/README.md +2 -0
- package/index.js +5 -0
- package/lib/cms-compress.js +10 -12
- package/lib/cms-sign.js +28 -20
- package/lib/cms-verify.js +21 -12
- package/lib/composite-sig.js +4 -4
- package/lib/constants.js +13 -0
- package/lib/framework-error.js +10 -0
- package/lib/guard-all.js +5 -0
- package/lib/guard-compress.js +164 -0
- package/lib/tls-cert-compress.js +398 -0
- package/lib/validator-all.js +2 -0
- package/lib/validator-tls.js +168 -0
- package/lib/webcrypto.js +41 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.tls
|
|
6
|
+
* @nav Transparency
|
|
7
|
+
* @title TLS
|
|
8
|
+
* @order 215
|
|
9
|
+
* @slug tls
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* TLS handshake structures that carry certificates. `decompressCertificate`
|
|
13
|
+
* and `compressCertificate` are the RFC 8879 `CompressedCertificate` codec --
|
|
14
|
+
* the compressed form of a TLS 1.3 `Certificate` message, which shrinks the
|
|
15
|
+
* largest thing a handshake sends and matters most for post-quantum chains,
|
|
16
|
+
* where certificates grow by kilobytes. `parseCertificateMessage` decodes the
|
|
17
|
+
* RFC 8446 sec. 4.4.2 `Certificate` message itself, so a compressed chain
|
|
18
|
+
* arrives as certificate DER ready for `pki.schema.x509.parse` rather than as
|
|
19
|
+
* an opaque blob.
|
|
20
|
+
*
|
|
21
|
+
* These are encoded in the TLS presentation language -- positional, tag-less,
|
|
22
|
+
* fixed-width big-endian integers and length-prefixed opaque vectors -- NOT
|
|
23
|
+
* ASN.1/DER, so this module composes the toolkit's bounded big-endian cursor
|
|
24
|
+
* rather than the DER schema engine.
|
|
25
|
+
*
|
|
26
|
+
* Decompression is the attack surface and is fail-closed on both sides of the
|
|
27
|
+
* bound RFC 8879 sec. 5 requires: the decompressor is capped at the message's
|
|
28
|
+
* OWN declared uncompressed length (so a bomb is refused mid-stream, never
|
|
29
|
+
* allocated), and the output must then equal that length EXACTLY (which catches
|
|
30
|
+
* the under-length direction a cap cannot see). The caller's policy cap applies
|
|
31
|
+
* independently, so a peer declaring 16 MiB does not get 16 MiB. This module
|
|
32
|
+
* decodes structure only -- it never verifies a certificate, builds a path, or
|
|
33
|
+
* speaks the handshake.
|
|
34
|
+
*
|
|
35
|
+
* All three registered algorithms -- zlib, brotli and zstd -- are implemented,
|
|
36
|
+
* and each is offered only where the running Node can decompress it safely. A
|
|
37
|
+
* decompressor must fault on a frame it could not finish; where one instead
|
|
38
|
+
* returns a short result and reports the whole input consumed, a peer could cut
|
|
39
|
+
* a frame's tail and have the receiver read a prefix as the whole message. Any
|
|
40
|
+
* algorithm whose decompressor behaves that way is dropped at startup and is
|
|
41
|
+
* then neither advertised nor accepted. On the current long-term-support Node
|
|
42
|
+
* this leaves zlib and brotli.
|
|
43
|
+
*
|
|
44
|
+
* @card
|
|
45
|
+
* Encode and decode RFC 8879 TLS compressed certificate messages and the
|
|
46
|
+
* RFC 8446 Certificate message inside them -- a two-sided decompression
|
|
47
|
+
* bound, exact-length enforcement, per-entry certificate DER surfaced raw,
|
|
48
|
+
* fail-closed. All three registered algorithms are implemented; each is
|
|
49
|
+
* offered only where the running Node decompresses it safely.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
var C = require("./constants");
|
|
53
|
+
var guard = require("./guard-all");
|
|
54
|
+
var frameworkError = require("./framework-error");
|
|
55
|
+
var validator = require("./validator-all");
|
|
56
|
+
var ByteWriter = require("./byte-writer");
|
|
57
|
+
var zlib = require("zlib");
|
|
58
|
+
|
|
59
|
+
var TlsError = frameworkError.TlsError;
|
|
60
|
+
function _err(code, message, cause) { return new TlsError(code, message, cause); }
|
|
61
|
+
|
|
62
|
+
// RFC 8879 sec. 7.3 -- the CertificateCompressionAlgorithm registry. The wire value is
|
|
63
|
+
// what travels; the name is what this toolkit's guard dispatches on. 0 is Reserved and
|
|
64
|
+
// 16384..65535 are Experimental Use, but neither needs a row: anything absent here is
|
|
65
|
+
// refused the same way, and adding a row for a value we cannot decompress would be the
|
|
66
|
+
// fail-open. Every name here is checked against the decompression guard at load, so a
|
|
67
|
+
// runtime without one of the three cannot advertise it.
|
|
68
|
+
var ALG_BY_NUMBER = { 1: "zlib", 2: "brotli", 3: "zstd" };
|
|
69
|
+
var ALG_BY_NAME = {};
|
|
70
|
+
Object.keys(ALG_BY_NUMBER).forEach(function (n) { ALG_BY_NAME[ALG_BY_NUMBER[n]] = Number(n); });
|
|
71
|
+
|
|
72
|
+
// The compressors, paired with the guard's decompressors. RFC 8879 sec. 4 binds each
|
|
73
|
+
// algorithm to its format: zlib -> RFC 1950, brotli -> RFC 7932, zstd -> RFC 8478.
|
|
74
|
+
var COMPRESS = { zlib: zlib.deflateSync, brotli: zlib.brotliCompressSync, zstd: zlib.zstdCompressSync };
|
|
75
|
+
|
|
76
|
+
// Each codec takes its compression level through a DIFFERENT option: zlib as a top-level
|
|
77
|
+
// `level`, brotli and zstd as numbered entries in `params`. Passing `{ level: n }` to the
|
|
78
|
+
// latter two is silently ignored by node, so a caller asking for a level would quietly get the
|
|
79
|
+
// default. Each is mapped to the option its codec actually reads.
|
|
80
|
+
var LEVEL_OPT = {
|
|
81
|
+
zlib: function (n) { return { level: n }; },
|
|
82
|
+
brotli: function (n) { var p = {}; p[zlib.constants.BROTLI_PARAM_QUALITY] = n; return { params: p }; },
|
|
83
|
+
zstd: function (n) { var p = {}; p[zlib.constants.ZSTD_c_compressionLevel] = n; return { params: p }; },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// The names that are BOTH registered by RFC 8879 and reachable through the decompression
|
|
87
|
+
// guard on this runtime. An algorithm the guard cannot decompress must never be offered
|
|
88
|
+
// or accepted -- advertising one would mean accepting a message we then cannot open.
|
|
89
|
+
var SUPPORTED = guard.compress.algorithms().filter(function (n) {
|
|
90
|
+
return Object.prototype.hasOwnProperty.call(ALG_BY_NAME, n) && typeof COMPRESS[n] === "function";
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The minimum Certificate message size and the widest opaque<..2^24-1> vector -- both owned
|
|
94
|
+
// by validator-tls, alongside the rest of the framing rule set. A declared
|
|
95
|
+
// uncompressed_length below the minimum cannot be a Certificate message, and refusing it
|
|
96
|
+
// also keeps a zero-length declaration away from the decompression cap, which is a positive
|
|
97
|
+
// integer by contract.
|
|
98
|
+
var MIN_CERT_MSG_BYTES = validator.tls.MIN_CERT_MSG_BYTES;
|
|
99
|
+
var MAX_VECTOR_24 = validator.tls.MAX_VECTOR_24;
|
|
100
|
+
|
|
101
|
+
// The fault codes validator-tls raises for each framing violation it owns.
|
|
102
|
+
var FRAMING_CODES = { truncated: "tls/truncated", framing: "tls/bad-framing", trailing: "tls/trailing-data" };
|
|
103
|
+
|
|
104
|
+
// The fixed part of a CompressedCertificate: uint16 algorithm + uint24 uncompressed_length +
|
|
105
|
+
// the uint24 length prefix of the compressed body (RFC 8879 sec. 4).
|
|
106
|
+
var HEADER_BYTES = 8;
|
|
107
|
+
|
|
108
|
+
// Resolve the caller's policy cap, which may only tighten the protocol ceiling DOWNWARD.
|
|
109
|
+
// A cap that could be raised would let an option undo the framing limit RFC 8879 sec. 5
|
|
110
|
+
// requires be applied "as if no compression were used".
|
|
111
|
+
function _resolveCap(opts) {
|
|
112
|
+
var cap = C.LIMITS.TLS_CERT_MSG_MAX_BYTES;
|
|
113
|
+
if (opts.maxOutputBytes !== undefined) {
|
|
114
|
+
var mo = opts.maxOutputBytes;
|
|
115
|
+
if (typeof mo !== "number" || !isFinite(mo) || mo <= 0 || Math.floor(mo) !== mo) {
|
|
116
|
+
throw _err("tls/bad-input", "opts.maxOutputBytes must be a positive integer");
|
|
117
|
+
}
|
|
118
|
+
if (mo < cap) cap = mo;
|
|
119
|
+
}
|
|
120
|
+
return cap;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The algorithm names the caller will accept, defaulting to everything supported. This is
|
|
124
|
+
// the peer's `compress_certificate` advertisement: RFC 8879 sec. 4 requires the algorithm
|
|
125
|
+
// to be one the receiver offered, so accepting an unadvertised one would decompress bytes
|
|
126
|
+
// under a codec we never agreed to.
|
|
127
|
+
function _resolveAllowed(opts) {
|
|
128
|
+
if (opts.allowedAlgorithms === undefined) return SUPPORTED.slice();
|
|
129
|
+
var list = opts.allowedAlgorithms;
|
|
130
|
+
if (!Array.isArray(list)) throw _err("tls/bad-input", "opts.allowedAlgorithms must be an array of algorithm names or numbers");
|
|
131
|
+
return list.map(function (a) {
|
|
132
|
+
var name = (typeof a === "number") ? ALG_BY_NUMBER[a] : a;
|
|
133
|
+
if (typeof name !== "string" || SUPPORTED.indexOf(name) === -1) {
|
|
134
|
+
throw _err("tls/bad-input", "opts.allowedAlgorithms names an algorithm this toolkit cannot decompress: " + JSON.stringify(a));
|
|
135
|
+
}
|
|
136
|
+
return name;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @primitive pki.tls.decompressCertificate
|
|
142
|
+
* @signature pki.tls.decompressCertificate(bytes, opts?) -> { algorithm, algorithmName, uncompressedLength, certificateMessage, certificate }
|
|
143
|
+
* @since 0.4.3
|
|
144
|
+
* @status experimental
|
|
145
|
+
* @spec RFC 8879, RFC 8446
|
|
146
|
+
* @related pki.tls.compressCertificate, pki.tls.parseCertificateMessage, pki.schema.x509.parse
|
|
147
|
+
*
|
|
148
|
+
* Decode an RFC 8879 sec. 4 `CompressedCertificate`: the `algorithm` code point, the
|
|
149
|
+
* declared `uncompressedLength`, the recovered `certificateMessage` (the raw RFC 8446
|
|
150
|
+
* sec. 4.4.2 `Certificate` message bytes, surfaced verbatim), and `certificate` -- that
|
|
151
|
+
* message already decoded into its request context and per-entry certificate DER.
|
|
152
|
+
*
|
|
153
|
+
* The decompression bound is two-sided, as RFC 8879 sec. 5 requires. The decompressor is
|
|
154
|
+
* capped at `min(uncompressedLength, policy cap)`, so a bomb is refused the moment its
|
|
155
|
+
* output would exceed what the message itself declared -- it is never allocated. The
|
|
156
|
+
* recovered length must then equal `uncompressedLength` exactly, which catches the
|
|
157
|
+
* under-length direction no cap can see. A message that fails either way is refused;
|
|
158
|
+
* RFC 8879 sec. 5 maps both to the `bad_certificate` alert, but they keep distinct codes
|
|
159
|
+
* here because "this is a bomb" and "these bytes are not that codec" are different
|
|
160
|
+
* diagnoses.
|
|
161
|
+
*
|
|
162
|
+
* An algorithm outside the RFC 8879 registry, one this runtime cannot decompress, or one
|
|
163
|
+
* absent from `opts.allowedAlgorithms` is refused before any decompressor runs -- the
|
|
164
|
+
* algorithm MUST be one the receiver advertised (RFC 8879 sec. 4). An empty compressed
|
|
165
|
+
* body is a framing violation (`opaque<1..2^24-1>`), not an empty certificate list.
|
|
166
|
+
*
|
|
167
|
+
* Throws `TlsError` with a stable `tls/*` code on any malformed input, never a raw
|
|
168
|
+
* `TypeError`. Structure only -- no certificate here is verified or path-built.
|
|
169
|
+
*
|
|
170
|
+
* @opts
|
|
171
|
+
* maxOutputBytes - tighten the decompression cap DOWNWARD from the RFC 8446 sec. 4
|
|
172
|
+
* framing ceiling (2^24-1). A value above it does not raise it.
|
|
173
|
+
* allowedAlgorithms- the algorithms the receiver advertised, as names or code points;
|
|
174
|
+
* defaults to every algorithm this runtime can decompress.
|
|
175
|
+
* certificateType - "X509" (default) or "RawPublicKey" (RFC 7250). Not self-describing
|
|
176
|
+
* on the wire -- it is negotiated by a separate extension -- so it is
|
|
177
|
+
* declared, never guessed.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* // The Certificate message this codec carries: an empty request context, then one
|
|
181
|
+
* // entry -- the certificate DER followed by its (here empty) extensions vector.
|
|
182
|
+
* var u24 = function (n) { var b = Buffer.alloc(3); b.writeUIntBE(n, 0, 3); return b; };
|
|
183
|
+
* var entry = Buffer.concat([u24(certDer.length), certDer, Buffer.from([0, 0])]);
|
|
184
|
+
* var message = Buffer.concat([Buffer.from([0]), u24(entry.length), entry]);
|
|
185
|
+
*
|
|
186
|
+
* var out = pki.tls.decompressCertificate(pki.tls.compressCertificate(message));
|
|
187
|
+
* out.algorithmName; // "zlib"
|
|
188
|
+
* out.uncompressedLength === out.certificateMessage.length; // true
|
|
189
|
+
* pki.schema.x509.parse(out.certificate.entries[0].certData).subject;
|
|
190
|
+
*/
|
|
191
|
+
function decompressCertificate(bytes, opts) {
|
|
192
|
+
opts = opts || {};
|
|
193
|
+
var view = guard.bytes.view(bytes, TlsError, "tls/bad-input", "the compressed certificate message");
|
|
194
|
+
// The CompressedCertificate travels as a handshake message body, framed by RFC 8446 sec. 4
|
|
195
|
+
// with a uint24 length, so one longer than that ceiling could not have been sent at all.
|
|
196
|
+
if (view.length > C.LIMITS.TLS_CERT_MSG_MAX_BYTES) {
|
|
197
|
+
throw _err("tls/too-large", "the compressed certificate message is " + view.length +
|
|
198
|
+
" bytes, over the " + C.LIMITS.TLS_CERT_MSG_MAX_BYTES + "-byte handshake framing limit");
|
|
199
|
+
}
|
|
200
|
+
var cap = _resolveCap(opts);
|
|
201
|
+
var allowed = _resolveAllowed(opts);
|
|
202
|
+
|
|
203
|
+
// Framing decode + bounds live in validator-tls (the type's rule set); which algorithms
|
|
204
|
+
// are acceptable, the output cap, and the length agreement are this codec's policy and
|
|
205
|
+
// stay here. Note the currency split the two families use: the validator takes the error
|
|
206
|
+
// CLASS, the guard below takes the `_err` FACTORY.
|
|
207
|
+
var framed = validator.tls.compressedCertificate(view, TlsError, FRAMING_CODES);
|
|
208
|
+
var algorithm = framed.algorithm;
|
|
209
|
+
var name = ALG_BY_NUMBER[algorithm];
|
|
210
|
+
if (!name) {
|
|
211
|
+
throw _err("tls/unsupported-algorithm", "compression algorithm " + algorithm + " is not in the RFC 8879 registry");
|
|
212
|
+
}
|
|
213
|
+
// SUPPORTED is the RFC 8879 registry intersected with what the decompression guard can
|
|
214
|
+
// actually open safely, so this fires wherever one of the three is dropped -- which is the
|
|
215
|
+
// long-term-support Node, where zstd cannot report a truncated frame. Covered there by the
|
|
216
|
+
// dropped-algorithm vector; unreachable on a runtime where all three qualify.
|
|
217
|
+
if (SUPPORTED.indexOf(name) === -1) {
|
|
218
|
+
throw _err("tls/unsupported-algorithm", "compression algorithm " + algorithm + " (" + name + ") is not decompressible on this runtime");
|
|
219
|
+
}
|
|
220
|
+
if (allowed.indexOf(name) === -1) {
|
|
221
|
+
throw _err("tls/unsupported-algorithm", "compression algorithm " + algorithm + " (" + name + ") was not among the advertised algorithms");
|
|
222
|
+
}
|
|
223
|
+
var uncompressedLength = framed.uncompressedLength;
|
|
224
|
+
var body = framed.body;
|
|
225
|
+
if (uncompressedLength < MIN_CERT_MSG_BYTES) {
|
|
226
|
+
throw _err("tls/bad-framing", "uncompressed_length " + uncompressedLength +
|
|
227
|
+
" is below the " + MIN_CERT_MSG_BYTES + "-byte minimum of a Certificate message (RFC 8446 sec. 4.4.2)");
|
|
228
|
+
}
|
|
229
|
+
if (uncompressedLength > cap) {
|
|
230
|
+
throw _err("tls/too-large", "uncompressed_length " + uncompressedLength + " exceeds the " + cap + "-byte limit");
|
|
231
|
+
}
|
|
232
|
+
// The declared length IS the cap: a stream that expands past what the message itself
|
|
233
|
+
// claims is refused mid-decompression rather than after the memory is committed.
|
|
234
|
+
var message = guard.compress.bounded(name, Buffer.from(body), uncompressedLength, _err,
|
|
235
|
+
{ tooLarge: "tls/too-large", failed: "tls/decompress-failed" }, "the compressed certificate message");
|
|
236
|
+
// RFC 8879 sec. 5: if the length after decompression does not match the declared one,
|
|
237
|
+
// the message is invalid. The cap above only bounds the OVER direction.
|
|
238
|
+
if (message.length !== uncompressedLength) {
|
|
239
|
+
throw _err("tls/length-mismatch", "the decompressed message is " + message.length +
|
|
240
|
+
" bytes but uncompressed_length declared " + uncompressedLength);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
algorithm: algorithm,
|
|
244
|
+
algorithmName: name,
|
|
245
|
+
uncompressedLength: uncompressedLength,
|
|
246
|
+
certificateMessage: message,
|
|
247
|
+
certificate: parseCertificateMessage(message, opts),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @primitive pki.tls.parseCertificateMessage
|
|
253
|
+
* @signature pki.tls.parseCertificateMessage(bytes, opts?) -> { certificateRequestContext, entries }
|
|
254
|
+
* @since 0.4.3
|
|
255
|
+
* @status experimental
|
|
256
|
+
* @spec RFC 8446, RFC 7250
|
|
257
|
+
* @related pki.tls.decompressCertificate, pki.schema.x509.parse
|
|
258
|
+
*
|
|
259
|
+
* Decode an RFC 8446 sec. 4.4.2 `Certificate` message into its
|
|
260
|
+
* `certificateRequestContext` (raw, empty in a server's handshake certificate) and its
|
|
261
|
+
* `entries`. Each entry surfaces `certData` -- the certificate DER exactly as it appeared
|
|
262
|
+
* on the wire, ready for `pki.schema.x509.parse` and never re-serialized -- plus the raw
|
|
263
|
+
* `extensions` bytes that follow it and `extensionList`, those bytes decoded to their
|
|
264
|
+
* RFC 8446 sec. 4.2 records (`type` and raw `data` per extension). The vector's framing is
|
|
265
|
+
* validated rather than accepted opaquely, so a vector that cannot be a whole number of
|
|
266
|
+
* Extensions is refused instead of being reported as a well-formed message.
|
|
267
|
+
*
|
|
268
|
+
* `certificate_type` is negotiated by a separate extension (RFC 7250) and is NOT present
|
|
269
|
+
* in this message, so it cannot be inferred from the bytes. It is declared through
|
|
270
|
+
* `opts.certificateType` and defaults to X509; under `"RawPublicKey"` the same slot is a
|
|
271
|
+
* `SubjectPublicKeyInfo` and is surfaced as `spki` instead of `certData`.
|
|
272
|
+
*
|
|
273
|
+
* Throws `TlsError` with a stable `tls/*` code on any framing violation -- a lying vector
|
|
274
|
+
* length, a field past its bound, or bytes trailing the entry list.
|
|
275
|
+
*
|
|
276
|
+
* @opts
|
|
277
|
+
* certificateType - "X509" (default) or "RawPublicKey" (RFC 7250).
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* var u24 = function (n) { var b = Buffer.alloc(3); b.writeUIntBE(n, 0, 3); return b; };
|
|
281
|
+
* var entry = Buffer.concat([u24(certDer.length), certDer, Buffer.from([0, 0])]);
|
|
282
|
+
* var message = Buffer.concat([Buffer.from([0]), u24(entry.length), entry]);
|
|
283
|
+
*
|
|
284
|
+
* var msg = pki.tls.parseCertificateMessage(message);
|
|
285
|
+
* msg.certificateRequestContext.length; // 0
|
|
286
|
+
* pki.schema.x509.parse(msg.entries[0].certData).subject;
|
|
287
|
+
*/
|
|
288
|
+
function parseCertificateMessage(bytes, opts) {
|
|
289
|
+
opts = opts || {};
|
|
290
|
+
var type = opts.certificateType === undefined ? "X509" : opts.certificateType;
|
|
291
|
+
if (type !== "X509" && type !== "RawPublicKey") {
|
|
292
|
+
throw _err("tls/bad-input", "opts.certificateType must be \"X509\" or \"RawPublicKey\"");
|
|
293
|
+
}
|
|
294
|
+
var view = guard.bytes.view(bytes, TlsError, "tls/bad-input", "the certificate message");
|
|
295
|
+
// A Certificate message is a handshake message body, framed by RFC 8446 sec. 4 with a uint24
|
|
296
|
+
// length, so one longer than that ceiling could not have appeared on the wire. This entry point
|
|
297
|
+
// is public and reachable without going through decompression, so it applies the bound itself
|
|
298
|
+
// rather than inheriting the one the compression paths enforce.
|
|
299
|
+
if (view.length > C.LIMITS.TLS_CERT_MSG_MAX_BYTES) {
|
|
300
|
+
throw _err("tls/too-large", "the certificate message is " + view.length +
|
|
301
|
+
" bytes, over the " + C.LIMITS.TLS_CERT_MSG_MAX_BYTES + "-byte handshake framing limit");
|
|
302
|
+
}
|
|
303
|
+
return validator.tls.certificateMessage(view, TlsError, FRAMING_CODES, type);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @primitive pki.tls.compressCertificate
|
|
308
|
+
* @signature pki.tls.compressCertificate(certificateMessage, opts?) -> Buffer
|
|
309
|
+
* @since 0.4.3
|
|
310
|
+
* @status experimental
|
|
311
|
+
* @spec RFC 8879
|
|
312
|
+
* @related pki.tls.decompressCertificate
|
|
313
|
+
*
|
|
314
|
+
* Build an RFC 8879 sec. 4 `CompressedCertificate` around an already-encoded RFC 8446
|
|
315
|
+
* sec. 4.4.2 `Certificate` message. `opts.algorithm` selects the codec by name
|
|
316
|
+
* (`"zlib"` / `"brotli"` / `"zstd"`) or by its registry code point; zlib is the default,
|
|
317
|
+
* being the one every RFC 8879 implementation supports. An algorithm the running Node
|
|
318
|
+
* cannot decompress safely is refused here as well as on decode, so this never produces a
|
|
319
|
+
* message it could not itself read back.
|
|
320
|
+
*
|
|
321
|
+
* The result is verified before it is returned: the emitted message is decoded back
|
|
322
|
+
* through `decompressCertificate` and the recovered bytes compared to the input, so a
|
|
323
|
+
* message this toolkit produces can never be one this toolkit's own decoder refuses.
|
|
324
|
+
*
|
|
325
|
+
* Throws `TlsError` with a stable `tls/*` code -- for an unknown algorithm, a message
|
|
326
|
+
* that cannot be framed (empty, or past the 2^24-1 ceiling either compressed or not).
|
|
327
|
+
*
|
|
328
|
+
* @opts
|
|
329
|
+
* algorithm - the compression algorithm, by name or RFC 8879 code point. Default "zlib".
|
|
330
|
+
* level - the codec's compression level, passed through unchanged where it applies.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* var u24 = function (n) { var b = Buffer.alloc(3); b.writeUIntBE(n, 0, 3); return b; };
|
|
334
|
+
* var entry = Buffer.concat([u24(certDer.length), certDer, Buffer.from([0, 0])]);
|
|
335
|
+
* var message = Buffer.concat([Buffer.from([0]), u24(entry.length), entry]);
|
|
336
|
+
*
|
|
337
|
+
* var wire = pki.tls.compressCertificate(message, { algorithm: "brotli" });
|
|
338
|
+
* wire.readUInt16BE(0); // 2 -- the brotli code point
|
|
339
|
+
* wire.length < message.length; // true
|
|
340
|
+
*/
|
|
341
|
+
function compressCertificate(certificateMessage, opts) {
|
|
342
|
+
opts = opts || {};
|
|
343
|
+
var view = guard.bytes.view(certificateMessage, TlsError, "tls/bad-input", "the certificate message");
|
|
344
|
+
var sel = opts.algorithm === undefined ? "zlib" : opts.algorithm;
|
|
345
|
+
var name = (typeof sel === "number") ? ALG_BY_NUMBER[sel] : sel;
|
|
346
|
+
if (typeof name !== "string" || SUPPORTED.indexOf(name) === -1) {
|
|
347
|
+
throw _err("tls/unsupported-algorithm", "opts.algorithm names an algorithm this toolkit cannot compress: " + JSON.stringify(sel));
|
|
348
|
+
}
|
|
349
|
+
if (view.length < MIN_CERT_MSG_BYTES) {
|
|
350
|
+
throw _err("tls/bad-input", "a Certificate message is at least " + MIN_CERT_MSG_BYTES + " bytes (RFC 8446 sec. 4.4.2)");
|
|
351
|
+
}
|
|
352
|
+
if (view.length > C.LIMITS.TLS_CERT_MSG_MAX_BYTES) {
|
|
353
|
+
throw _err("tls/too-large", "the certificate message is " + view.length + " bytes, over the " +
|
|
354
|
+
C.LIMITS.TLS_CERT_MSG_MAX_BYTES + "-byte handshake framing limit");
|
|
355
|
+
}
|
|
356
|
+
if (opts.level !== undefined && (typeof opts.level !== "number" || !isFinite(opts.level) || Math.floor(opts.level) !== opts.level)) {
|
|
357
|
+
throw _err("tls/bad-input", "opts.level must be an integer");
|
|
358
|
+
}
|
|
359
|
+
var stream;
|
|
360
|
+
try {
|
|
361
|
+
stream = COMPRESS[name](view, opts.level !== undefined ? LEVEL_OPT[name](opts.level) : undefined);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
throw _err("tls/bad-input", "the certificate message could not be compressed (check opts.level)", e);
|
|
364
|
+
}
|
|
365
|
+
// The CompressedCertificate is itself a handshake message body, so the WHOLE emitted message --
|
|
366
|
+
// not just the message it carries -- must fit the RFC 8446 sec. 4 uint24 framing. Incompressible
|
|
367
|
+
// content near the ceiling grows by the 8-byte header, so bounding only the input would emit a
|
|
368
|
+
// message that cannot be framed and no peer could receive. Checked BEFORE the writer so this
|
|
369
|
+
// stays one verdict rather than surfacing as the body vector's own bound.
|
|
370
|
+
var wireLength = HEADER_BYTES + stream.length;
|
|
371
|
+
if (wireLength > C.LIMITS.TLS_CERT_MSG_MAX_BYTES) {
|
|
372
|
+
throw _err("tls/too-large", "the compressed certificate message would be " + wireLength +
|
|
373
|
+
" bytes, over the " + C.LIMITS.TLS_CERT_MSG_MAX_BYTES + "-byte handshake framing limit");
|
|
374
|
+
}
|
|
375
|
+
var w = new ByteWriter(TlsError, "tls/bad-input");
|
|
376
|
+
w.u16(ALG_BY_NAME[name]);
|
|
377
|
+
w.u24(view.length);
|
|
378
|
+
w.vector(3, 1, MAX_VECTOR_24, stream, "tls/bad-framing");
|
|
379
|
+
var wire = w.build();
|
|
380
|
+
// Self-check: a produced message must be one this decoder accepts, and must recover the
|
|
381
|
+
// input byte-for-byte. This is what stops a codec-level defect from shipping a message
|
|
382
|
+
// that only a permissive peer can read.
|
|
383
|
+
var back = decompressCertificate(wire, { allowedAlgorithms: [name] });
|
|
384
|
+
// Coverage residual -- unreachable while the codec is correct, which is the point: this
|
|
385
|
+
// fires only if a compressor and its decompressor disagree, and there is no input that
|
|
386
|
+
// makes them. It stays as the assertion that a message this toolkit emits is one this
|
|
387
|
+
// toolkit accepts, so a future codec defect surfaces here rather than at a peer.
|
|
388
|
+
if (!back.certificateMessage.equals(view)) {
|
|
389
|
+
throw _err("tls/bad-input", "the compressed certificate message did not round-trip to the input");
|
|
390
|
+
}
|
|
391
|
+
return wire;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
decompressCertificate: decompressCertificate,
|
|
396
|
+
compressCertificate: compressCertificate,
|
|
397
|
+
parseCertificateMessage: parseCertificateMessage,
|
|
398
|
+
};
|
package/lib/validator-all.js
CHANGED
|
@@ -38,6 +38,7 @@ var sig = require("./validator-sig");
|
|
|
38
38
|
var attcert = require("./validator-attcert");
|
|
39
39
|
var keydesc = require("./validator-keydesc");
|
|
40
40
|
var tpm = require("./validator-tpm");
|
|
41
|
+
var tls = require("./validator-tls");
|
|
41
42
|
|
|
42
43
|
module.exports = {
|
|
43
44
|
cose: cose,
|
|
@@ -45,4 +46,5 @@ module.exports = {
|
|
|
45
46
|
attcert: attcert,
|
|
46
47
|
keydesc: keydesc,
|
|
47
48
|
tpm: tpm,
|
|
49
|
+
tls: tls,
|
|
48
50
|
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- no operator-facing namespace. The documented surface is the codec whose
|
|
6
|
+
// TLS handshake handling composes this validator (pki.tls).
|
|
7
|
+
//
|
|
8
|
+
// validator-tls -- the SINGLE home for the framing conformance of the TLS handshake
|
|
9
|
+
// structures that carry certificates: the RFC 8879 sec. 4 CompressedCertificate and the
|
|
10
|
+
// RFC 8446 sec. 4.4.2 Certificate message it decompresses to. Sibling to the guard family:
|
|
11
|
+
// a validator owns a decoded TYPE's COMPLETE framing rule set once, so the vector bounds,
|
|
12
|
+
// the minimum sizes and the trailing-byte rejection cannot drift as consumers are added.
|
|
13
|
+
//
|
|
14
|
+
// Every field is unsigned big-endian, packed with no padding, in the TLS presentation
|
|
15
|
+
// language: `uintN` is a fixed-width integer and `opaque<min..max>` is a length prefix wide
|
|
16
|
+
// enough for `max` followed by exactly that many bytes. The caller supplies its typed error
|
|
17
|
+
// CONSTRUCTOR E (a validator takes a class -- `new E(code, msg)` -- where the guard family
|
|
18
|
+
// takes a factory) plus the `codes` its domain uses for each fault.
|
|
19
|
+
//
|
|
20
|
+
// Rule set (verbatim against RFC 8879 sec. 4 and RFC 8446 sec. 4.4.2):
|
|
21
|
+
// - compressedCertificate: uint16 algorithm, uint24 uncompressed_length, then
|
|
22
|
+
// opaque compressed_certificate_message<1..2^24-1> -- the minimum of 1 makes an EMPTY
|
|
23
|
+
// compressed body a framing violation, which matters because the three decompressors do
|
|
24
|
+
// not agree on what empty input means. Bytes after the vector are rejected: one chain
|
|
25
|
+
// must have exactly one encoding.
|
|
26
|
+
// - certificateMessage: opaque certificate_request_context<0..2^8-1>, then
|
|
27
|
+
// CertificateEntry certificate_list<0..2^24-1>, each entry an opaque<1..2^24-1> payload
|
|
28
|
+
// (cert_data, or a SubjectPublicKeyInfo under RFC 7250 RawPublicKey) plus
|
|
29
|
+
// Extension extensions<0..2^16-1>. Bytes after the list are rejected. A zero-length
|
|
30
|
+
// entry payload is a framing violation, not a blank certificate.
|
|
31
|
+
//
|
|
32
|
+
// This validator decodes and bounds STRUCTURE only. Which algorithms are acceptable, what
|
|
33
|
+
// the output cap is, and whether the recovered length matches its declaration are the
|
|
34
|
+
// codec's policy and stay with the codec.
|
|
35
|
+
//
|
|
36
|
+
// The RFC 8446 sec. 4.4.2 / sec. 4.2 rules that bind a Certificate-message DECODER, and where
|
|
37
|
+
// each one lands -- enumerated so the ones deliberately NOT enforced are a recorded decision
|
|
38
|
+
// rather than an oversight:
|
|
39
|
+
// ENFORCED here:
|
|
40
|
+
// - RawPublicKey carries at most one CertificateEntry (sec. 4.4.2).
|
|
41
|
+
// - No extension type appears twice in one extension block (sec. 4.2).
|
|
42
|
+
// - Every vector's framing, its declared minimum, and no trailing bytes at either level.
|
|
43
|
+
// NOT ENFORCED, deliberately:
|
|
44
|
+
// - "The sender's certificate MUST come in the first CertificateEntry" is about which
|
|
45
|
+
// certificate a consumer treats as the leaf, not about framing. Entries are surfaced in
|
|
46
|
+
// wire order and never reordered, so the caller sees exactly what arrived -- and the same
|
|
47
|
+
// section tells implementations to tolerate extraneous certificates and arbitrary
|
|
48
|
+
// orderings beyond the first, so refusing any order here would be wrong.
|
|
49
|
+
// - "The server's certificate_list MUST always be non-empty" is conditioned on the SENDER's
|
|
50
|
+
// role: a client legitimately sends an empty list when it has no certificate to offer.
|
|
51
|
+
// This decoder has no role, so an empty list decodes to zero entries and the caller, which
|
|
52
|
+
// does know the role, decides.
|
|
53
|
+
// - Extension correspondence ("extensions MUST correspond to ones from the client") and any
|
|
54
|
+
// extension's own contents (a status_request body being a CertificateStatus) need the
|
|
55
|
+
// negotiation state and the extension's semantics. Neither exists here: extension values
|
|
56
|
+
// are surfaced raw.
|
|
57
|
+
// - The OpenPGP certificate type is unreachable rather than checked -- the only accepted
|
|
58
|
+
// types are X509 and RawPublicKey, and anything else is refused at the entry point.
|
|
59
|
+
|
|
60
|
+
var C = require("./constants");
|
|
61
|
+
var ByteReader = require("./byte-reader");
|
|
62
|
+
|
|
63
|
+
// The widest an opaque<..2^24-1> vector can be, and the width of the length prefix that
|
|
64
|
+
// frames it. Both come straight from the TLS presentation language.
|
|
65
|
+
var MAX_VECTOR_24 = 0xffffff;
|
|
66
|
+
var MAX_VECTOR_16 = 0xffff;
|
|
67
|
+
|
|
68
|
+
// RFC 8446 sec. 4.4.2 frames a Certificate message as a 1-byte-prefixed request context plus
|
|
69
|
+
// a 3-byte-prefixed entry list, so the smallest well-formed message is 4 bytes.
|
|
70
|
+
var MIN_CERT_MSG_BYTES = 4;
|
|
71
|
+
|
|
72
|
+
// compressedCertificate(view, E, codes) -> { algorithm, uncompressedLength, body }.
|
|
73
|
+
// Decode the RFC 8879 sec. 4 framing. `codes` names { truncated, framing, trailing }.
|
|
74
|
+
// @enforced-by behavioral -- a packed TLS presentation-language decode has no rename-proof
|
|
75
|
+
// code shape; the framing rules are pinned by the RED conformance vectors that drive the
|
|
76
|
+
// shipped consumer (pki.tls.decompressCertificate) on each malformed shape.
|
|
77
|
+
function compressedCertificate(view, E, codes) {
|
|
78
|
+
var r = new ByteReader(view, 0, view.length, E, codes.truncated);
|
|
79
|
+
var algorithm = r.u16(codes.truncated);
|
|
80
|
+
var uncompressedLength = r.u24(codes.truncated);
|
|
81
|
+
// opaque compressed_certificate_message<1..2^24-1> -- minimum 1, so an empty body is a
|
|
82
|
+
// framing violation rather than something a decompressor gets to interpret.
|
|
83
|
+
var body = r.vector(3, 1, MAX_VECTOR_24, codes.framing);
|
|
84
|
+
if (!r.atEnd()) {
|
|
85
|
+
throw new E(codes.trailing, "the CompressedCertificate carries " + r.remaining() +
|
|
86
|
+
" byte(s) after the compressed message");
|
|
87
|
+
}
|
|
88
|
+
return { algorithm: algorithm, uncompressedLength: uncompressedLength, body: Buffer.from(body) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The extensions vector of a CertificateEntry, decoded to its records. RFC 8446 sec. 4.2 makes an
|
|
92
|
+
// Extension a uint16 type plus a uint16-prefixed value, so the vector is a whole number of them and
|
|
93
|
+
// the smallest is 4 bytes. Walking it is what keeps a malformed Certificate message from passing as
|
|
94
|
+
// structurally valid: a one-byte vector cannot be an Extension, and accepting the bytes opaquely
|
|
95
|
+
// would report it as well-formed. Each record's value is surfaced raw -- this decodes the framing,
|
|
96
|
+
// never the extension's own contents.
|
|
97
|
+
function _extensionRecords(view, E, codes) {
|
|
98
|
+
var r = new ByteReader(view, 0, view.length, E, codes.truncated);
|
|
99
|
+
var out = [];
|
|
100
|
+
var seen = Object.create(null);
|
|
101
|
+
while (!r.atEnd()) {
|
|
102
|
+
// Each iteration consumes at least 4 bytes and every read is bounds-checked, so the walk
|
|
103
|
+
// always terminates or faults.
|
|
104
|
+
var type = r.u16(codes.framing);
|
|
105
|
+
var data = r.vector(2, 0, MAX_VECTOR_16, codes.framing);
|
|
106
|
+
// RFC 8446 sec. 4.2: "There MUST NOT be more than one extension of the same type in a given
|
|
107
|
+
// extension block." Left unenforced, one type could appear twice and a consumer reading only
|
|
108
|
+
// the first (or only the last) would act on a different value than another implementation --
|
|
109
|
+
// the same one-structure-two-meanings ambiguity a duplicate DER SET member creates.
|
|
110
|
+
if (seen[type]) {
|
|
111
|
+
throw new E(codes.framing, "the extension block carries more than one extension of type " +
|
|
112
|
+
type + " (RFC 8446 sec. 4.2)");
|
|
113
|
+
}
|
|
114
|
+
seen[type] = true;
|
|
115
|
+
out.push({ type: type, data: Buffer.from(data) });
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// certificateMessage(view, E, codes, certificateType) -> { certificateRequestContext, entries }.
|
|
121
|
+
// Decode the RFC 8446 sec. 4.4.2 Certificate message. `codes` names { truncated, framing,
|
|
122
|
+
// trailing }. `certificateType` is "X509" or "RawPublicKey" and is DECLARED by the caller --
|
|
123
|
+
// it is negotiated by a separate extension (RFC 7250) and is not present in these bytes, so
|
|
124
|
+
// it can never be inferred from them.
|
|
125
|
+
// @enforced-by behavioral -- a packed TLS presentation-language decode has no rename-proof
|
|
126
|
+
// code shape; the framing rules are pinned by the RED conformance vectors that drive the
|
|
127
|
+
// shipped consumer (pki.tls.parseCertificateMessage) on each malformed shape.
|
|
128
|
+
function certificateMessage(view, E, codes, certificateType) {
|
|
129
|
+
var r = new ByteReader(view, 0, view.length, E, codes.truncated);
|
|
130
|
+
var ctxLen = r.u8(codes.truncated);
|
|
131
|
+
var context = r.fixed(ctxLen, codes.framing);
|
|
132
|
+
var listLen = r.u24(codes.truncated);
|
|
133
|
+
var list = r.subReader(listLen, codes.framing);
|
|
134
|
+
if (!r.atEnd()) {
|
|
135
|
+
throw new E(codes.trailing, "the Certificate message carries " + r.remaining() +
|
|
136
|
+
" byte(s) after the certificate list");
|
|
137
|
+
}
|
|
138
|
+
// RFC 8446 sec. 4.4.2: under a negotiated RawPublicKey certificate type the list "MUST contain
|
|
139
|
+
// no more than one CertificateEntry". Otherwise the count is bounded because the byte ceiling
|
|
140
|
+
// does not bound it -- the smallest legal entry is 6 bytes, so a message well inside the
|
|
141
|
+
// framing limit can declare millions, each costing far more heap than wire.
|
|
142
|
+
var maxEntries = certificateType === "RawPublicKey" ? 1 : C.LIMITS.TLS_CERT_MAX_ENTRIES;
|
|
143
|
+
var entries = [];
|
|
144
|
+
while (!list.atEnd()) {
|
|
145
|
+
if (entries.length >= maxEntries) {
|
|
146
|
+
throw new E(codes.framing, "the certificate list carries more than " + maxEntries +
|
|
147
|
+
" entr" + (maxEntries === 1 ? "y" : "ies") +
|
|
148
|
+
(certificateType === "RawPublicKey" ? " (RFC 8446 sec. 4.4.2 permits at most one under RawPublicKey)" : ""));
|
|
149
|
+
}
|
|
150
|
+
var data = list.vector(3, 1, MAX_VECTOR_24, codes.framing);
|
|
151
|
+
var extensions = list.vector(2, 0, MAX_VECTOR_16, codes.framing);
|
|
152
|
+
var entry = {
|
|
153
|
+
extensions: Buffer.from(extensions),
|
|
154
|
+
extensionList: _extensionRecords(extensions, E, codes),
|
|
155
|
+
};
|
|
156
|
+
if (certificateType === "RawPublicKey") entry.spki = Buffer.from(data);
|
|
157
|
+
else entry.certData = Buffer.from(data);
|
|
158
|
+
entries.push(entry);
|
|
159
|
+
}
|
|
160
|
+
return { certificateRequestContext: Buffer.from(context), entries: entries };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
compressedCertificate: compressedCertificate,
|
|
165
|
+
certificateMessage: certificateMessage,
|
|
166
|
+
MIN_CERT_MSG_BYTES: MIN_CERT_MSG_BYTES,
|
|
167
|
+
MAX_VECTOR_24: MAX_VECTOR_24,
|
|
168
|
+
};
|