Skip to content

Commit aee82a2

Browse files
committed
tls: add certificateCompression option
This changes enables compression within OpenSSL *without* enabling record compression, so this only affects compression of certificates delivered within the TLS handshake. This certificate compression remains disabled by default for now, but becomes available via the new certificateCompression option in TLS context APIs. Enabling this shrinks handshakes significantly, and also reduces fingerprintability of Node.js client handshakes, as these are enabled in all modern browsers by default.
1 parent f824418 commit aee82a2

File tree

7 files changed

+361
-0
lines changed

7 files changed

+361
-0
lines changed

doc/api/tls.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,9 @@ argument.
18951895
<!-- YAML
18961896
added: v0.11.13
18971897
changes:
1898+
- version: REPLACEME
1899+
pr-url: https://github.com/nodejs/node/pull/62217
1900+
description: The `certificateCompression` option has been added.
18981901
- version:
18991902
- v22.9.0
19001903
- v20.18.0
@@ -1988,6 +1991,12 @@ changes:
19881991
the same order as their private keys in `key`. If the intermediate
19891992
certificates are not provided, the peer will not be able to validate the
19901993
certificate, and the handshake will fail.
1994+
* `certificateCompression` {string\[]} An array of supported certificate
1995+
compression algorithm names, in preference order. Supported values are
1996+
`'zlib'`, `'brotli'`, and `'zstd'`. When set, enables TLS certificate
1997+
compression ([RFC 8879][]) which compresses certificates during the TLS
1998+
handshake, reducing handshake size. Only effective with TLSv1.3.
1999+
**Default:** `[]` (disabled).
19912000
* `sigalgs` {string} Colon-separated list of supported signature algorithms.
19922001
The list can contain digest algorithms (`SHA256`, `MD5` etc.), public key
19932002
algorithms (`RSA-PSS`, `ECDSA` etc.), combination of both (e.g
@@ -2469,6 +2478,7 @@ added: v0.11.3
24692478
[RFC 4279]: https://tools.ietf.org/html/rfc4279
24702479
[RFC 5077]: https://tools.ietf.org/html/rfc5077
24712480
[RFC 5929]: https://tools.ietf.org/html/rfc5929
2481+
[RFC 8879]: https://tools.ietf.org/html/rfc8879
24722482
[SSL_METHODS]: https://www.openssl.org/docs/man1.1.1/man7/ssl.html#Dealing-with-Protocol-Methods
24732483
[Session Resumption]: #session-resumption
24742484
[Stream]: stream.md#stream

lib/internal/tls/secure-context.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
133133
allowPartialTrustChain,
134134
ca,
135135
cert,
136+
certificateCompression,
136137
ciphers = getDefaultCiphers(),
137138
clientCertEngine,
138139
crl,
@@ -210,6 +211,17 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
210211
}
211212
}
212213

214+
if (certificateCompression != null) {
215+
if (!ArrayIsArray(certificateCompression)) {
216+
throw new ERR_INVALID_ARG_TYPE(
217+
`${name}.certificateCompression`, 'Array', certificateCompression);
218+
}
219+
220+
if (certificateCompression.length > 0) {
221+
context.setCertificateCompression(certificateCompression);
222+
}
223+
}
224+
213225
if (sigalgs !== undefined && sigalgs !== null) {
214226
validateString(sigalgs, `${name}.sigalgs`);
215227

lib/internal/tls/wrap.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,7 @@ Server.prototype.setSecureContext = function(options) {
15061506

15071507
this.privateKeyIdentifier = options.privateKeyIdentifier;
15081508
this.privateKeyEngine = options.privateKeyEngine;
1509+
this.certificateCompression = options.certificateCompression;
15091510

15101511
this._sharedCreds = tls.createSecureContext({
15111512
pfx: this.pfx,
@@ -1529,6 +1530,7 @@ Server.prototype.setSecureContext = function(options) {
15291530
sessionTimeout: this.sessionTimeout,
15301531
privateKeyIdentifier: this.privateKeyIdentifier,
15311532
privateKeyEngine: this.privateKeyEngine,
1533+
certificateCompression: this.certificateCompression,
15321534
});
15331535
};
15341536

src/crypto/crypto_context.cc

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,8 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate(
13191319
SetProtoMethod(isolate, tmpl, "setOptions", SetOptions);
13201320
SetProtoMethod(isolate, tmpl, "setSessionIdContext", SetSessionIdContext);
13211321
SetProtoMethod(isolate, tmpl, "setSessionTimeout", SetSessionTimeout);
1322+
SetProtoMethod(
1323+
isolate, tmpl, "setCertificateCompression", SetCertificateCompression);
13221324
SetProtoMethod(isolate, tmpl, "close", Close);
13231325
SetProtoMethod(isolate, tmpl, "loadPKCS12", LoadPKCS12);
13241326
SetProtoMethod(isolate, tmpl, "setTicketKeys", SetTicketKeys);
@@ -1407,6 +1409,7 @@ void SecureContext::RegisterExternalReferences(
14071409
registry->Register(SetOptions);
14081410
registry->Register(SetSessionIdContext);
14091411
registry->Register(SetSessionTimeout);
1412+
registry->Register(SetCertificateCompression);
14101413
registry->Register(Close);
14111414
registry->Register(LoadPKCS12);
14121415
registry->Register(SetTicketKeys);
@@ -1565,6 +1568,14 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
15651568
env->external_memory_accounter()->Increase(env->isolate(), kExternalSize);
15661569
SSL_CTX_set_app_data(sc->ctx_.get(), sc);
15671570

1571+
// OpenSSL populates cert_comp_prefs with all available algorithms by
1572+
// default when compression libraries are linked. Clear them so that
1573+
// certificate compression (RFC 8879) is always opt-in for now, via
1574+
// the certificateCompression option.
1575+
#ifndef OPENSSL_NO_COMP_ALG
1576+
SSL_CTX_set1_cert_comp_preference(sc->ctx_.get(), nullptr, 0);
1577+
#endif
1578+
15681579
// Disable SSLv2 in the case when method == TLS_method() and the
15691580
// cipher list contains SSLv2 ciphers (not the default, should be rare.)
15701581
// The bundled OpenSSL doesn't have SSLv2 support but the system OpenSSL may.
@@ -2067,6 +2078,84 @@ void SecureContext::SetSessionTimeout(const FunctionCallbackInfo<Value>& args) {
20672078
SSL_CTX_set_timeout(sc->ctx_.get(), sessionTimeout);
20682079
}
20692080

2081+
void SecureContext::SetCertificateCompression(
2082+
const FunctionCallbackInfo<Value>& args) {
2083+
SecureContext* sc;
2084+
ASSIGN_OR_RETURN_UNWRAP(&sc, args.This());
2085+
Environment* env = sc->env();
2086+
2087+
CHECK_GE(args.Length(), 1);
2088+
CHECK(args[0]->IsArray());
2089+
2090+
Local<Array> arr = args[0].As<Array>();
2091+
uint32_t len = arr->Length();
2092+
2093+
if (len == 0 || len > TLSEXT_comp_cert_limit) {
2094+
return THROW_ERR_INVALID_ARG_VALUE(
2095+
env, "certificateCompression must contain 1 to 3 algorithm names");
2096+
}
2097+
2098+
#ifndef OPENSSL_NO_COMP_ALG
2099+
int algs[TLSEXT_comp_cert_limit];
2100+
for (uint32_t i = 0; i < len; i++) {
2101+
Local<Value> val;
2102+
if (!arr->Get(env->context(), i).ToLocal(&val) || !val->IsString()) {
2103+
return THROW_ERR_INVALID_ARG_VALUE(
2104+
env, "certificateCompression entries must be strings");
2105+
}
2106+
Utf8Value name(env->isolate(), val);
2107+
if (strcmp(*name, "zlib") == 0) {
2108+
algs[i] = TLSEXT_comp_cert_zlib;
2109+
} else if (strcmp(*name, "brotli") == 0) {
2110+
algs[i] = TLSEXT_comp_cert_brotli;
2111+
} else if (strcmp(*name, "zstd") == 0) {
2112+
algs[i] = TLSEXT_comp_cert_zstd;
2113+
} else {
2114+
return THROW_ERR_INVALID_ARG_VALUE(
2115+
env,
2116+
"certificateCompression algorithm must be 'zlib', 'brotli', or "
2117+
"'zstd'");
2118+
}
2119+
}
2120+
if (!SSL_CTX_set1_cert_comp_preference(
2121+
sc->ctx_.get(), algs, static_cast<size_t>(len))) {
2122+
return THROW_ERR_CRYPTO_OPERATION_FAILED(
2123+
env, "Failed to set certificate compression preference");
2124+
}
2125+
2126+
// Pre-compress the loaded certificate(s) for all supported algorithms, where
2127+
// 0 arg means 'compress with all algorithms in the preference list'.
2128+
// Returns 0 when no certificate is loaded (e.g. client-only context) or
2129+
// when compression did not reduce size — both are non-fatal.
2130+
SSL_CTX_compress_certs(sc->ctx_.get(), 0);
2131+
2132+
// Store preferences for propagation during SNI context switches.
2133+
memcpy(sc->cert_comp_prefs_, algs, sizeof(int) * len);
2134+
sc->cert_comp_prefs_len_ = len;
2135+
2136+
// Cache pre-compressed cert data for SNI context switches.
2137+
// setSniContext uses SSL_use_certificate which doesn't carry comp_cert data,
2138+
// so we extract it here and re-apply via SSL_set1_compressed_cert later.
2139+
sc->compressed_certs_.clear();
2140+
for (uint32_t i = 0; i < len; i++) {
2141+
unsigned char* data = nullptr;
2142+
size_t orig_len = 0;
2143+
size_t comp_len =
2144+
SSL_CTX_get1_compressed_cert(sc->ctx_.get(), algs[i], &data, &orig_len);
2145+
if (comp_len > 0 && data != nullptr) {
2146+
sc->compressed_certs_.push_back(
2147+
{algs[i],
2148+
std::vector<unsigned char>(data, data + comp_len),
2149+
orig_len});
2150+
OPENSSL_free(data);
2151+
}
2152+
}
2153+
#else
2154+
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
2155+
env, "Certificate compression is not supported by this OpenSSL build");
2156+
#endif
2157+
}
2158+
20702159
void SecureContext::Close(const FunctionCallbackInfo<Value>& args) {
20712160
SecureContext* sc;
20722161
ASSIGN_OR_RETURN_UNWRAP(&sc, args.This());

src/crypto/crypto_context.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#include "memory_tracker.h"
1111
#include "v8.h"
1212

13+
#include <vector>
14+
1315
namespace node {
1416
namespace crypto {
1517
// A maxVersion of 0 means "any", but OpenSSL may support TLS versions that
@@ -47,6 +49,27 @@ class SecureContext final : public BaseObject {
4749
// the SecureContext.
4850
ncrypto::SSLCtxPointer& ctx() { return ctx_; }
4951

52+
#ifndef OPENSSL_NO_COMP_ALG
53+
bool HasCertCompression() const {
54+
return cert_comp_prefs_len_ > 0;
55+
}
56+
int* CertCompPrefs() {
57+
return cert_comp_prefs_;
58+
}
59+
size_t CertCompPrefsLen() const {
60+
return cert_comp_prefs_len_;
61+
}
62+
63+
struct CompressedCertData {
64+
int algorithm;
65+
std::vector<unsigned char> data;
66+
size_t orig_length;
67+
};
68+
const std::vector<CompressedCertData>& CompressedCerts() const {
69+
return compressed_certs_;
70+
}
71+
#endif
72+
5073
ncrypto::SSLPointer CreateSSL();
5174

5275
void SetGetSessionCallback(GetSessionCb cb);
@@ -107,6 +130,8 @@ class SecureContext final : public BaseObject {
107130
const v8::FunctionCallbackInfo<v8::Value>& args);
108131
static void SetSessionTimeout(
109132
const v8::FunctionCallbackInfo<v8::Value>& args);
133+
static void SetCertificateCompression(
134+
const v8::FunctionCallbackInfo<v8::Value>& args);
110135
static void SetMinProto(const v8::FunctionCallbackInfo<v8::Value>& args);
111136
static void SetMaxProto(const v8::FunctionCallbackInfo<v8::Value>& args);
112137
static void GetMinProto(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -157,6 +182,12 @@ class SecureContext final : public BaseObject {
157182
unsigned char ticket_key_name_[16];
158183
unsigned char ticket_key_aes_[16];
159184
unsigned char ticket_key_hmac_[16];
185+
186+
#ifndef OPENSSL_NO_COMP_ALG
187+
int cert_comp_prefs_[TLSEXT_comp_cert_limit] = {};
188+
size_t cert_comp_prefs_len_ = 0;
189+
std::vector<CompressedCertData> compressed_certs_;
190+
#endif
160191
};
161192

162193
int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,

src/crypto/crypto_tls.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,24 @@ void TLSWrap::CertCbDone(const FunctionCallbackInfo<Value>& args) {
16141614
unsigned long err = ERR_get_error(); // NOLINT(runtime/int)
16151615
return ThrowCryptoError(env, err, "CertCbDone");
16161616
}
1617+
// setSniContext copies the cert via SSL_use_certificate which does not
1618+
// carry over pre-compressed certificate data (comp_cert). If the new
1619+
// context has certificate compression configured, set the compression
1620+
// preferences on this connection and apply the cached compressed cert
1621+
// data so the server can send CompressedCertificate messages.
1622+
#ifndef OPENSSL_NO_COMP_ALG
1623+
if (sc->HasCertCompression()) {
1624+
SSL_set1_cert_comp_preference(
1625+
w->ssl_.get(), sc->CertCompPrefs(), sc->CertCompPrefsLen());
1626+
for (const auto& cc : sc->CompressedCerts()) {
1627+
SSL_set1_compressed_cert(w->ssl_.get(),
1628+
cc.algorithm,
1629+
const_cast<unsigned char*>(cc.data.data()),
1630+
cc.data.size(),
1631+
cc.orig_length);
1632+
}
1633+
}
1634+
#endif
16171635
} else if (ctx->IsObject()) {
16181636
// Failure: incorrect SNI context object
16191637
Local<Value> err = Exception::TypeError(env->sni_context_err_string());

0 commit comments

Comments
 (0)