GeezerVison is showing github.com — tls: add certificateCompression option · nodejs/node@ee29465
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: deps/openssl/openssl.gyp +9-5Lines changed: 9 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 19 | 19 | { | 20 | 20 | 'target_name': 'openssl', | 21 | 21 | 'type': '<(library)', | 22 | | - 'dependencies': [ | 23 | | - '../zlib/zlib.gyp:zlib', | 24 | | - '../brotli/brotli.gyp:brotli', | 25 | | - '../zstd/zstd.gyp:zstd', | 26 | | - ], | 27 | 22 | 'includes': ['./openssl_common.gypi'], | 28 | 23 | 'defines': [ | 29 | 24 | # Compile out hardware engines. Most are stubs that dynamically load | | 54 | 49 | 'MODULESDIR="<(modules_dir)"', | 55 | 50 | ] | 56 | 51 | }], | | 52 | + ['node_shared_zlib=="false"', { | | 53 | + 'dependencies': [ '../zlib/zlib.gyp:zlib' ], | | 54 | + }], | | 55 | + ['node_shared_brotli=="false"', { | | 56 | + 'dependencies': [ '../brotli/brotli.gyp:brotli' ], | | 57 | + }], | | 58 | + ['node_shared_zstd=="false"', { | | 59 | + 'dependencies': [ '../zstd/zstd.gyp:zstd' ], | | 60 | + }], | 57 | 61 | ], | 58 | 62 | 'direct_dependent_settings': { | 59 | 63 | 'include_dirs': [ 'openssl/include', 'openssl/crypto/include'] | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/tls.md +29Lines changed: 29 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1895,6 +1895,9 @@ argument. | 1895 | 1895 | <!-- YAML | 1896 | 1896 | added: v0.11.13 | 1897 | 1897 | changes: | | 1898 | + - version: REPLACEME | | 1899 | + pr-url: https://github.com/nodejs/node/pull/62217 | | 1900 | + description: The `certificateCompression` option has been added. | 1898 | 1901 | - version: | 1899 | 1902 | - v22.9.0 | 1900 | 1903 | - v20.18.0 | @@ -1988,6 +1991,12 @@ changes: | 1988 | 1991 | the same order as their private keys in `key`. If the intermediate | 1989 | 1992 | certificates are not provided, the peer will not be able to validate the | 1990 | 1993 | 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). | 1991 | 2000 | * `sigalgs` {string} Colon-separated list of supported signature algorithms. | 1992 | 2001 | The list can contain digest algorithms (`SHA256`, `MD5` etc.), public key | 1993 | 2002 | algorithms (`RSA-PSS`, `ECDSA` etc.), combination of both (e.g | @@ -2378,6 +2387,25 @@ TLSv1.2 and below. | 2378 | 2387 | console.log(tls.getCiphers()); // ['aes128-gcm-sha256', 'aes128-sha', ...] | 2379 | 2388 | ``` | 2380 | 2389 |
| | 2390 | +## `tls.getCertificateCompressionAlgorithms()` | | 2391 | + | | 2392 | +<!-- YAML | | 2393 | +added: REPLACEME | | 2394 | +--> | | 2395 | + | | 2396 | +* Returns: {string\[]} | | 2397 | + | | 2398 | +Returns an array with the names of the RFC 8879 certificate compression | | 2399 | +algorithms supported by the current OpenSSL build, suitable for use in the | | 2400 | +`certificateCompression` option of [`tls.createSecureContext()`][]. Possible | | 2401 | +values include `'zlib'`, `'brotli'`, and `'zstd'`. | | 2402 | + | | 2403 | +The array is empty when certificate compression is unavailable. | | 2404 | + | | 2405 | +```js | | 2406 | +console.log(tls.getCertificateCompressionAlgorithms()); // ['zlib', 'brotli', 'zstd'] | | 2407 | +``` | | 2408 | + | 2381 | 2409 | ## `tls.rootCertificates` | 2382 | 2410 |
| 2383 | 2411 | <!-- YAML | @@ -2469,6 +2497,7 @@ added: v0.11.3 | 2469 | 2497 | [RFC 4279]: https://tools.ietf.org/html/rfc4279 | 2470 | 2498 | [RFC 5077]: https://tools.ietf.org/html/rfc5077 | 2471 | 2499 | [RFC 5929]: https://tools.ietf.org/html/rfc5929 | | 2500 | +[RFC 8879]: https://tools.ietf.org/html/rfc8879 | 2472 | 2501 | [SSL_METHODS]: https://www.openssl.org/docs/man1.1.1/man7/ssl.html#Dealing-with-Protocol-Methods | 2473 | 2502 | [Session Resumption]: #session-resumption | 2474 | 2503 | [Stream]: stream.md#stream | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/tls/secure-context.js +38Lines changed: 38 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -133,6 +133,7 @@ function configSecureContext(context, options = kEmptyObject, name = 'options') | 133 | 133 | allowPartialTrustChain, | 134 | 134 | ca, | 135 | 135 | cert, | | 136 | + certificateCompression, | 136 | 137 | ciphers = getDefaultCiphers(), | 137 | 138 | clientCertEngine, | 138 | 139 | crl, | @@ -210,6 +211,43 @@ function configSecureContext(context, options = kEmptyObject, name = 'options') | 210 | 211 | } | 211 | 212 | } | 212 | 213 |
| | 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 | + // Pack length + algorithm IDs into a single Uint32 for a cheap | | 222 | + // JS->C++ crossing. Layout: | | 223 | + // bits 0-7 : length (1..3) | | 224 | + // bits 8-15: alg id at position 0 | | 225 | + // bits 16-23: alg id at position 1 | | 226 | + // bits 24-31: alg id at position 2 | | 227 | + // IDs match OpenSSL's TLSEXT_comp_cert_zlib (1), _brotli (2), _zstd (3) | | 228 | + if (certificateCompression.length > 3) { | | 229 | + throw new ERR_INVALID_ARG_VALUE( | | 230 | + `${name}.certificateCompression`, certificateCompression, | | 231 | + 'can specify at most 3 algorithms'); | | 232 | + } | | 233 | + let packed = certificateCompression.length; | | 234 | + for (let i = 0; i < certificateCompression.length; i++) { | | 235 | + const algoName = certificateCompression[i]; | | 236 | + let id; | | 237 | + if (algoName === 'zlib') id = 1; | | 238 | + else if (algoName === 'brotli') id = 2; | | 239 | + else if (algoName === 'zstd') id = 3; | | 240 | + else { | | 241 | + throw new ERR_INVALID_ARG_VALUE( | | 242 | + `${name}.certificateCompression[${i}]`, algoName, | | 243 | + "must be 'zlib', 'brotli', or 'zstd'"); | | 244 | + } | | 245 | + packed |= id << (8 * (i + 1)); | | 246 | + } | | 247 | + context.setCertificateCompression(packed); | | 248 | + } | | 249 | + } | | 250 | + | 213 | 251 | if (sigalgs !== undefined && sigalgs !== null) { | 214 | 252 | validateString(sigalgs, `${name}.sigalgs`); | 215 | 253 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/tls/wrap.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1620,6 +1620,7 @@ Server.prototype.setSecureContext = function(options) { | 1620 | 1620 |
| 1621 | 1621 | this.privateKeyIdentifier = options.privateKeyIdentifier; | 1622 | 1622 | this.privateKeyEngine = options.privateKeyEngine; | | 1623 | + this.certificateCompression = options.certificateCompression; | 1623 | 1624 |
| 1624 | 1625 | this._sharedCreds = tls.createSecureContext({ | 1625 | 1626 | pfx: this.pfx, | @@ -1643,6 +1644,7 @@ Server.prototype.setSecureContext = function(options) { | 1643 | 1644 | sessionTimeout: this.sessionTimeout, | 1644 | 1645 | privateKeyIdentifier: this.privateKeyIdentifier, | 1645 | 1646 | privateKeyEngine: this.privateKeyEngine, | | 1647 | + certificateCompression: this.certificateCompression, | 1646 | 1648 | }); | 1647 | 1649 | }; | 1648 | 1650 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/tls.js +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -47,6 +47,7 @@ const { | 47 | 47 | resetRootCertStore, | 48 | 48 | getUserRootCertificates, | 49 | 49 | getSSLCiphers, | | 50 | + getCertificateCompressionAlgorithms, | 50 | 51 | startLoadingCertificatesOffThread, | 51 | 52 | } = internalBinding('crypto'); | 52 | 53 |
| @@ -113,6 +114,10 @@ exports.getCiphers = internalUtil.cachedResult( | 113 | 114 | () => internalUtil.filterDuplicateStrings(getSSLCiphers(), true), | 114 | 115 | ); | 115 | 116 |
| | 117 | +exports.getCertificateCompressionAlgorithms = internalUtil.cachedResult( | | 118 | + () => getCertificateCompressionAlgorithms(), | | 119 | +); | | 120 | + | 116 | 121 | let bundledRootCertificates; | 117 | 122 | function cacheBundledRootCertificates() { | 118 | 123 | bundledRootCertificates ||= ObjectFreeze(getBundledRootCertificates()); | |
Collapse file Copy file name to clipboardExpand all lines: src/crypto/crypto_context.cc +112-1Lines changed: 112 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 12 | 12 | #include "util.h" | 13 | 13 | #include "v8.h" | 14 | 14 |
| 15 | | -#include <openssl/x509.h> | | 15 | +#include <openssl/comp.h> | 16 | 16 | #include <openssl/pkcs12.h> | 17 | 17 | #include <openssl/rand.h> | | 18 | +#include <openssl/x509.h> | 18 | 19 | #ifndef OPENSSL_NO_ENGINE | 19 | 20 | #include <openssl/engine.h> | 20 | 21 | #endif // !OPENSSL_NO_ENGINE | @@ -1378,6 +1379,8 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate( | 1378 | 1379 | SetProtoMethod(isolate, tmpl, "setOptions", SetOptions); | 1379 | 1380 | SetProtoMethod(isolate, tmpl, "setSessionIdContext", SetSessionIdContext); | 1380 | 1381 | SetProtoMethod(isolate, tmpl, "setSessionTimeout", SetSessionTimeout); | | 1382 | + SetProtoMethod( | | 1383 | + isolate, tmpl, "setCertificateCompression", SetCertificateCompression); | 1381 | 1384 | SetProtoMethod(isolate, tmpl, "close", Close); | 1382 | 1385 | SetProtoMethod(isolate, tmpl, "loadPKCS12", LoadPKCS12); | 1383 | 1386 | SetProtoMethod(isolate, tmpl, "setTicketKeys", SetTicketKeys); | @@ -1442,6 +1445,10 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) { | 1442 | 1445 | target, | 1443 | 1446 | "startLoadingCertificatesOffThread", | 1444 | 1447 | StartLoadingCertificatesOffThread); | | 1448 | + SetMethodNoSideEffect(context, | | 1449 | + target, | | 1450 | + "getCertificateCompressionAlgorithms", | | 1451 | + GetCertificateCompressionAlgorithms); | 1445 | 1452 | } | 1446 | 1453 |
| 1447 | 1454 | void SecureContext::RegisterExternalReferences( | @@ -1466,6 +1473,7 @@ void SecureContext::RegisterExternalReferences( | 1466 | 1473 | registry->Register(SetOptions); | 1467 | 1474 | registry->Register(SetSessionIdContext); | 1468 | 1475 | registry->Register(SetSessionTimeout); | | 1476 | + registry->Register(SetCertificateCompression); | 1469 | 1477 | registry->Register(Close); | 1470 | 1478 | registry->Register(LoadPKCS12); | 1471 | 1479 | registry->Register(SetTicketKeys); | @@ -1487,6 +1495,7 @@ void SecureContext::RegisterExternalReferences( | 1487 | 1495 | registry->Register(ResetRootCertStore); | 1488 | 1496 | registry->Register(GetUserRootCertificates); | 1489 | 1497 | registry->Register(StartLoadingCertificatesOffThread); | | 1498 | + registry->Register(GetCertificateCompressionAlgorithms); | 1490 | 1499 | } | 1491 | 1500 |
| 1492 | 1501 | SecureContext* SecureContext::Create(Environment* env) { | @@ -1624,6 +1633,14 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) { | 1624 | 1633 | env->external_memory_accounter()->Increase(env->isolate(), kExternalSize); | 1625 | 1634 | SSL_CTX_set_app_data(sc->ctx_.get(), sc); | 1626 | 1635 |
| | 1636 | + // OpenSSL populates cert_comp_prefs with all available algorithms by | | 1637 | + // default when compression libraries are linked. Clear them so that | | 1638 | + // certificate compression (RFC 8879) is always opt-in for now, via | | 1639 | + // the certificateCompression option. | | 1640 | +#ifdef NODE_OPENSSL_HAS_CERT_COMP | | 1641 | + SSL_CTX_set1_cert_comp_preference(sc->ctx_.get(), nullptr, 0); | | 1642 | +#endif | | 1643 | + | 1627 | 1644 | // Disable SSLv2 in the case when method == TLS_method() and the | 1628 | 1645 | // cipher list contains SSLv2 ciphers (not the default, should be rare.) | 1629 | 1646 | // The bundled OpenSSL doesn't have SSLv2 support but the system OpenSSL may. | @@ -2131,6 +2148,100 @@ void SecureContext::SetSessionTimeout(const FunctionCallbackInfo<Value>& args) { | 2131 | 2148 | SSL_CTX_set_timeout(sc->ctx_.get(), sessionTimeout); | 2132 | 2149 | } | 2133 | 2150 |
| | 2151 | +void SecureContext::SetCertificateCompression( | | 2152 | + const FunctionCallbackInfo<Value>& args) { | | 2153 | + SecureContext* sc; | | 2154 | + ASSIGN_OR_RETURN_UNWRAP(&sc, args.This()); | | 2155 | + Environment* env = sc->env(); | | 2156 | + | | 2157 | + CHECK_GE(args.Length(), 1); | | 2158 | + CHECK(args[0]->IsUint32()); | | 2159 | + | | 2160 | + // Cert compression requires TLS 1.3: | | 2161 | + long max_proto = // NOLINT(runtime/int) | | 2162 | + SSL_CTX_get_max_proto_version(sc->ctx_.get()); | | 2163 | + if (max_proto != 0 && max_proto < TLS1_3_VERSION) { | | 2164 | + return THROW_ERR_INVALID_ARG_VALUE( | | 2165 | + env, | | 2166 | + "certificateCompression requires a TLS protocol range that includes " | | 2167 | + "TLSv1.3"); | | 2168 | + } | | 2169 | + | | 2170 | +#ifdef NODE_OPENSSL_HAS_CERT_COMP | | 2171 | + // JS packs (length | alg0<<8 | alg1<<16 | alg2<<24) into a single Uint32. | | 2172 | + // IDs match TLSEXT_comp_cert_zlib (1), _brotli (2), _zstd (3). | | 2173 | + uint32_t packed = args[0].As<v8::Uint32>()->Value(); | | 2174 | + size_t len = packed & 0xff; | | 2175 | + | | 2176 | + // TLSEXT_comp_cert_limit is the limit for a zero-terminated algs array, | | 2177 | + // total number of available algs is one fewer. | | 2178 | + constexpr size_t kMaxCompAlgs = TLSEXT_comp_cert_limit - 1; | | 2179 | + if (len == 0 || len > kMaxCompAlgs) { | | 2180 | + return THROW_ERR_INVALID_ARG_VALUE( | | 2181 | + env, | | 2182 | + "certificateCompression must specify fewer than %d algorithms", | | 2183 | + static_cast<int>(kMaxCompAlgs)); | | 2184 | + } | | 2185 | + | | 2186 | + int algs[kMaxCompAlgs]; | | 2187 | + for (size_t i = 0; i < len; i++) { | | 2188 | + algs[i] = (packed >> (8 * (i + 1))) & 0xff; | | 2189 | + } | | 2190 | + if (!SSL_CTX_set1_cert_comp_preference( | | 2191 | + sc->ctx_.get(), algs, static_cast<size_t>(len))) { | | 2192 | + return THROW_ERR_CRYPTO_OPERATION_FAILED( | | 2193 | + env, "Failed to set certificate compression preference"); | | 2194 | + } | | 2195 | + | | 2196 | + // Pre-compress the loaded certificate(s) for all supported algorithms. | | 2197 | + // Returns 0 when no certificate is loaded (e.g. client-only context) or | | 2198 | + // when compression did not reduce size - both are non-fatal. | | 2199 | + constexpr int kCompressAllAlgs = 0; | | 2200 | + SSL_CTX_compress_certs(sc->ctx_.get(), kCompressAllAlgs); | | 2201 | + | | 2202 | + // Store preferences for propagation during SNI context switches. | | 2203 | + memcpy(sc->cert_comp_prefs_, algs, sizeof(int) * len); | | 2204 | + sc->cert_comp_prefs_len_ = len; | | 2205 | + | | 2206 | + // Cache pre-compressed cert data for SNI context switches. | | 2207 | + // setSniContext uses SSL_use_certificate which doesn't carry comp_cert data, | | 2208 | + // so we extract it here and re-apply via SSL_set1_compressed_cert later. | | 2209 | + sc->compressed_certs_.clear(); | | 2210 | + for (size_t i = 0; i < len; i++) { | | 2211 | + unsigned char* data = nullptr; | | 2212 | + size_t orig_len = 0; | | 2213 | + size_t comp_len = | | 2214 | + SSL_CTX_get1_compressed_cert(sc->ctx_.get(), algs[i], &data, &orig_len); | | 2215 | + ncrypto::DataPointer comp(data, comp_len); | | 2216 | + if (comp_len > 0 && data != nullptr) { | | 2217 | + sc->compressed_certs_.push_back( | | 2218 | + {algs[i], | | 2219 | + std::vector<unsigned char>(data, data + comp_len), | | 2220 | + orig_len}); | | 2221 | + } | | 2222 | + } | | 2223 | +#else | | 2224 | + return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION( | | 2225 | + env, "Certificate compression is not supported by this OpenSSL build"); | | 2226 | +#endif | | 2227 | +} | | 2228 | + | | 2229 | +void SecureContext::GetCertificateCompressionAlgorithms( | | 2230 | + const FunctionCallbackInfo<Value>& args) { | | 2231 | + Environment* env = Environment::GetCurrent(args); | | 2232 | + LocalVector<Value> algs(env->isolate()); | | 2233 | +#ifdef NODE_OPENSSL_HAS_CERT_COMP | | 2234 | + if (BIO_f_zlib() != nullptr) | | 2235 | + algs.push_back(FIXED_ONE_BYTE_STRING(env->isolate(), "zlib")); | | 2236 | + if (BIO_f_brotli() != nullptr) | | 2237 | + algs.push_back(FIXED_ONE_BYTE_STRING(env->isolate(), "brotli")); | | 2238 | + if (BIO_f_zstd() != nullptr) | | 2239 | + algs.push_back(FIXED_ONE_BYTE_STRING(env->isolate(), "zstd")); | | 2240 | +#endif | | 2241 | + args.GetReturnValue().Set( | | 2242 | + Array::New(env->isolate(), algs.data(), algs.size())); | | 2243 | +} | | 2244 | + | 2134 | 2245 | void SecureContext::Close(const FunctionCallbackInfo<Value>& args) { | 2135 | 2246 | SecureContext* sc; | 2136 | 2247 | ASSIGN_OR_RETURN_UNWRAP(&sc, args.This()); | |
Collapse file Copy file name to clipboardExpand all lines: src/crypto/crypto_context.h +35Lines changed: 35 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 10 | 10 | #include "memory_tracker.h" | 11 | 11 | #include "v8.h" | 12 | 12 |
| | 13 | +#ifdef NODE_OPENSSL_HAS_CERT_COMP | | 14 | +#include <vector> | | 15 | +#endif | | 16 | + | 13 | 17 | namespace node { | 14 | 18 | namespace crypto { | 15 | 19 | // A maxVersion of 0 means "any", but OpenSSL may support TLS versions that | @@ -47,6 +51,27 @@ class SecureContext final : public BaseObject { | 47 | 51 | // the SecureContext. | 48 | 52 | ncrypto::SSLCtxPointer& ctx() { return ctx_; } | 49 | 53 |
| | 54 | +#ifdef NODE_OPENSSL_HAS_CERT_COMP | | 55 | + bool HasCertCompression() const { | | 56 | + return cert_comp_prefs_len_ > 0; | | 57 | + } | | 58 | + int* CertCompPrefs() { | | 59 | + return cert_comp_prefs_; | | 60 | + } | | 61 | + size_t CertCompPrefsLen() const { | | 62 | + return cert_comp_prefs_len_; | | 63 | + } | | 64 | + | | 65 | + struct CompressedCertData { | | 66 | + int algorithm; | | 67 | + std::vector<unsigned char> data; | | 68 | + size_t orig_length; | | 69 | + }; | | 70 | + const std::vector<CompressedCertData>& CompressedCerts() const { | | 71 | + return compressed_certs_; | | 72 | + } | | 73 | +#endif | | 74 | + | 50 | 75 | ncrypto::SSLPointer CreateSSL(); | 51 | 76 |
| 52 | 77 | void SetGetSessionCallback(GetSessionCb cb); | @@ -106,6 +131,10 @@ class SecureContext final : public BaseObject { | 106 | 131 | const v8::FunctionCallbackInfo<v8::Value>& args); | 107 | 132 | static void SetSessionTimeout( | 108 | 133 | const v8::FunctionCallbackInfo<v8::Value>& args); | | 134 | + static void SetCertificateCompression( | | 135 | + const v8::FunctionCallbackInfo<v8::Value>& args); | | 136 | + static void GetCertificateCompressionAlgorithms( | | 137 | + const v8::FunctionCallbackInfo<v8::Value>& args); | 109 | 138 | static void SetMinProto(const v8::FunctionCallbackInfo<v8::Value>& args); | 110 | 139 | static void SetMaxProto(const v8::FunctionCallbackInfo<v8::Value>& args); | 111 | 140 | static void GetMinProto(const v8::FunctionCallbackInfo<v8::Value>& args); | @@ -156,6 +185,12 @@ class SecureContext final : public BaseObject { | 156 | 185 | unsigned char ticket_key_name_[16]; | 157 | 186 | unsigned char ticket_key_aes_[16]; | 158 | 187 | unsigned char ticket_key_hmac_[16]; | | 188 | + | | 189 | +#ifdef NODE_OPENSSL_HAS_CERT_COMP | | 190 | + int cert_comp_prefs_[TLSEXT_comp_cert_limit] = {}; | | 191 | + size_t cert_comp_prefs_len_ = 0; | | 192 | + std::vector<CompressedCertData> compressed_certs_; | | 193 | +#endif | 159 | 194 | }; | 160 | 195 |
| 161 | 196 | int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, | |
|