| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -137,8 +137,12 @@ To avoid this, servers should use compact certificate chains: | |||
| 137 | 137 | large RSA intermediates. The choice of CA directly affects handshake latency. | |
| 138 | 138 | ||
| 139 | 139 | Certificate compression ([RFC 8879][]) can also address this issue by | |
| 140 | - compressing the certificate chain during the handshake. However, Node.js does | ||
| 141 | - not currently support TLS certificate compression. | ||
| 140 | + compressing the certificate chain during the handshake, often keeping the | ||
| 141 | + server's Certificate message within the amplification limit and avoiding the | ||
| 142 | + extra round trip. Certificate compression is opt-in via the | ||
| 143 | + [`certificateCompression`][] TLS option and is disabled by default. When | ||
| 144 | + enabled, it applies to both the server's certificate and, for mutual TLS, | ||
| 145 | + the client's certificate. | ||
| 142 | 146 | ||
| 143 | 147 | ### Rate limiting | |
| 144 | 148 | ||
@@ -2894,6 +2898,34 @@ added: v23.8.0 | |||
| 2894 | 2898 | The TLS certificates to use for client sessions. For server sessions, | |
| 2895 | 2899 | certificates are specified per-identity in the [`sessionOptions.sni`][] map. | |
| 2896 | 2900 | ||
| 2901 | + #### `sessionOptions.certificateCompression` | ||
| 2902 | + | ||
| 2903 | + <!-- YAML | ||
| 2904 | + added: REPLACEME | ||
| 2905 | + --> | ||
| 2906 | + | ||
| 2907 | + * Type: {string\[]} One or more of `'zlib'`, `'brotli'`, or `'zstd'`, in | ||
| 2908 | + preference order. | ||
| 2909 | + | ||
| 2910 | + Enables TLS certificate compression ([RFC 8879][]) for this session. When | ||
| 2911 | + omitted, certificate compression is disabled. | ||
| 2912 | + | ||
| 2913 | + On the server side, the certificate chain is compressed using the first | ||
| 2914 | + listed algorithm that the client advertises support for. On the client side, | ||
| 2915 | + the listed algorithms are advertised to the server so that the server may | ||
| 2916 | + compress its certificate. When client authentication is in use, the option | ||
| 2917 | + also controls compression of the client's certificate. | ||
| 2918 | + | ||
| 2919 | + Compressing the certificate chain is especially useful for QUIC because it | ||
| 2920 | + reduces the size of the server's first flight, which is bounded by the | ||
| 2921 | + anti-amplification limit (see [Certificate size and handshake | ||
| 2922 | + performance][]). Certificate compression requires TLS 1.3, which QUIC always | ||
| 2923 | + uses. | ||
| 2924 | + | ||
| 2925 | + At most three algorithms may be specified. The option is silently ignored if | ||
| 2926 | + Node.js was built against a shared OpenSSL that lacks certificate compression | ||
| 2927 | + support. | ||
| 2928 | + | ||
| 2897 | 2929 | #### `sessionOptions.ciphers` | |
| 2898 | 2930 | ||
| 2899 | 2931 | <!-- YAML | |
@@ -4399,6 +4431,7 @@ throughput issues caused by flow control. | |||
| 4399 | 4431 | ||
| 4400 | 4432 | [Aborting a stream]: #aborting-a-stream | |
| 4401 | 4433 | [Callback error handling]: #callback-error-handling | |
| 4434 | + [Certificate size and handshake performance]: #certificate-size-and-handshake-performance | ||
| 4402 | 4435 | [JSON-SEQ]: https://www.rfc-editor.org/rfc/rfc7464 | |
| 4403 | 4436 | [NSS Key Log Format]: https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format | |
| 4404 | 4437 | [RFC 8879]: https://www.rfc-editor.org/rfc/rfc8879 | |
@@ -4426,6 +4459,7 @@ throughput issues caused by flow control. | |||
| 4426 | 4459 | [`application.enableConnectProtocol`]: #sessionoptionsapplication | |
| 4427 | 4460 | [`application.enableDatagrams`]: #sessionoptionsapplication | |
| 4428 | 4461 | [`application.qpackMaxDTableCapacity`]: #sessionoptionsapplication | |
| 4462 | + [`certificateCompression`]: #sessionoptionscertificatecompression | ||
| 4429 | 4463 | [`crypto.X509Certificate`]: crypto.md#class-x509certificate | |
| 4430 | 4464 | [`endpoint.busy`]: #endpointbusy | |
| 4431 | 4465 | [`endpoint.maxConnectionsPerHost`]: #endpointmaxconnectionsperhost | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -404,6 +404,9 @@ const endpointRegistry = new SafeSet(); | |||
| 404 | 404 | * of protocol names in preference order. | |
| 405 | 405 | * @property {string} [ciphers] The TLS ciphers | |
| 406 | 406 | * @property {string} [groups] The TLS key-exchange groups | |
| 407 | + * @property {Array<'zlib'|'brotli'|'zstd'>} [certificateCompression] The | ||
| 408 | + * TLS certificate compression algorithms to enable (RFC 8879), in | ||
| 409 | + * preference order. Certificate compression is disabled when omitted. | ||
| 407 | 410 | * @property {boolean} [keylog] Enable TLS key logging | |
| 408 | 411 | * @property {boolean} [verifyClient] Verify the client certificate (server only) | |
| 409 | 412 | * @property {boolean} [tlsTrace] Enable TLS tracing | |
@@ -4938,6 +4941,7 @@ function processTlsOptions(tls, forServer) { | |||
| 4938 | 4941 | rejectUnauthorized = true, | |
| 4939 | 4942 | enableEarlyData = true, | |
| 4940 | 4943 | tlsTrace = false, | |
| 4944 | + certificateCompression, | ||
| 4941 | 4945 | sni, | |
| 4942 | 4946 | // Client-only: identity options are specified directly (no sni map) | |
| 4943 | 4947 | keys, | |
@@ -4962,6 +4966,43 @@ function processTlsOptions(tls, forServer) { | |||
| 4962 | 4966 | validateBoolean(enableEarlyData, 'options.enableEarlyData'); | |
| 4963 | 4967 | validateBoolean(tlsTrace, 'options.tlsTrace'); | |
| 4964 | 4968 | ||
| 4969 | + // Encode the certificate compression (RFC 8879) preference. The list of | ||
| 4970 | + // algorithm names is packed into a single Uint32 for a cheap JS->C++ | ||
| 4971 | + // crossing, matching the encoding used by the node:tls implementation: | ||
| 4972 | + // bits 0-7 : number of algorithms (1..3) | ||
| 4973 | + // bits 8-15: algorithm id at position 0 | ||
| 4974 | + // bits 16-23: algorithm id at position 1 | ||
| 4975 | + // bits 24-31: algorithm id at position 2 | ||
| 4976 | + // IDs match OpenSSL's TLSEXT_comp_cert_zlib (1), _brotli (2), _zstd (3). | ||
| 4977 | + // A value of 0 (the default) leaves certificate compression disabled. | ||
| 4978 | + let packedCertificateCompression = 0; | ||
| 4979 | + if (certificateCompression !== undefined) { | ||
| 4980 | + if (!ArrayIsArray(certificateCompression)) { | ||
| 4981 | + throw new ERR_INVALID_ARG_TYPE('options.certificateCompression', | ||
| 4982 | + 'Array', certificateCompression); | ||
| 4983 | + } | ||
| 4984 | + if (certificateCompression.length > 3) { | ||
| 4985 | + throw new ERR_INVALID_ARG_VALUE('options.certificateCompression', | ||
| 4986 | + certificateCompression, | ||
| 4987 | + 'can specify at most 3 algorithms'); | ||
| 4988 | + } | ||
| 4989 | + let packed = certificateCompression.length; | ||
| 4990 | + for (let i = 0; i < certificateCompression.length; i++) { | ||
| 4991 | + const algoName = certificateCompression[i]; | ||
| 4992 | + let id; | ||
| 4993 | + if (algoName === 'zlib') id = 1; | ||
| 4994 | + else if (algoName === 'brotli') id = 2; | ||
| 4995 | + else if (algoName === 'zstd') id = 3; | ||
| 4996 | + else { | ||
| 4997 | + throw new ERR_INVALID_ARG_VALUE( | ||
| 4998 | + `options.certificateCompression[${i}]`, algoName, | ||
| 4999 | + "must be 'zlib', 'brotli', or 'zstd'"); | ||
| 5000 | + } | ||
| 5001 | + packed |= id << (8 * (i + 1)); | ||
| 5002 | + } | ||
| 5003 | + packedCertificateCompression = packed; | ||
| 5004 | + } | ||
| 5005 | + | ||
| 4965 | 5006 | // Encode the ALPN option to wire format (length-prefixed protocol names). | |
| 4966 | 5007 | // Server: array of protocol names. Client: single protocol name. | |
| 4967 | 5008 | // If not specified, the C++ default (h3) is used. | |
@@ -5027,6 +5068,7 @@ function processTlsOptions(tls, forServer) { | |||
| 5027 | 5068 | rejectUnauthorized, | |
| 5028 | 5069 | enableEarlyData, | |
| 5029 | 5070 | tlsTrace, | |
| 5071 | + certificateCompression: packedCertificateCompression, | ||
| 5030 | 5072 | ca, | |
| 5031 | 5073 | crl, | |
| 5032 | 5074 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -77,6 +77,7 @@ class SessionManager; | |||
| 77 | 77 | V(bbr, "bbr") \ | |
| 78 | 78 | V(ca, "ca") \ | |
| 79 | 79 | V(cc_algorithm, "cc") \ | |
| 80 | + V(certificate_compression, "certificateCompression") \ | ||
| 80 | 81 | V(certs, "certs") \ | |
| 81 | 82 | V(code, "code") \ | |
| 82 | 83 | V(ciphers, "ciphers") \ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,6 +12,9 @@ | |||
| 12 | 12 | #include <ngtcp2/ngtcp2_crypto_ossl.h> | |
| 13 | 13 | #include <node_sockaddr-inl.h> | |
| 14 | 14 | #include <openssl/ssl.h> | |
| 15 | + #ifdef NODE_OPENSSL_HAS_CERT_COMP | ||
| 16 | + #include <openssl/comp.h> | ||
| 17 | + #endif | ||
| 15 | 18 | #include <util-inl.h> | |
| 16 | 19 | #include <v8.h> | |
| 17 | 20 | #include "application.h" | |
@@ -594,6 +597,48 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { | |||
| 594 | 597 | } | |
| 595 | 598 | } | |
| 596 | 599 | ||
| 600 | + // TLS certificate compression (RFC 8879). OpenSSL enables all available | ||
| 601 | + // algorithms by default once compression libraries are linked, so we | ||
| 602 | + // always clear the preference first to keep certificate compression | ||
| 603 | + // opt-in (matching the behavior of node:tls). When the | ||
| 604 | + // certificateCompression option is set, apply the requested algorithms | ||
| 605 | + // and pre-compress the certificate(s) loaded above. QUIC always uses | ||
| 606 | + // TLS 1.3, which is the minimum required for certificate compression. | ||
| 607 | + #ifdef NODE_OPENSSL_HAS_CERT_COMP | ||
| 608 | + { | ||
| 609 | + ClearErrorOnReturn clear_error_on_return; | ||
| 610 | + SSL_CTX_set1_cert_comp_preference(ctx.get(), nullptr, 0); | ||
| 611 | + | ||
| 612 | + // The JS layer packs (length | alg0<<8 | alg1<<16 | alg2<<24) into a | ||
| 613 | + // single uint32. IDs match TLSEXT_comp_cert_zlib (1), _brotli (2), | ||
| 614 | + // _zstd (3). | ||
| 615 | + const uint32_t packed = options_.certificate_compression; | ||
| 616 | + const size_t len = packed & 0xff; | ||
| 617 | + if (len > 0) { | ||
| 618 | + // TLSEXT_comp_cert_limit bounds the zero-terminated algs array; the | ||
| 619 | + // number of usable algorithms is one fewer. | ||
| 620 | + constexpr size_t kMaxCompAlgs = TLSEXT_comp_cert_limit - 1; | ||
| 621 | + if (len > kMaxCompAlgs) { | ||
| 622 | + validation_error_ = "Invalid certificate compression preference"; | ||
| 623 | + return SSLCtxPointer(); | ||
| 624 | + } | ||
| 625 | + int algs[kMaxCompAlgs]; | ||
| 626 | + for (size_t i = 0; i < len; i++) { | ||
| 627 | + algs[i] = (packed >> (8 * (i + 1))) & 0xff; | ||
| 628 | + } | ||
| 629 | + if (!SSL_CTX_set1_cert_comp_preference(ctx.get(), algs, len)) { | ||
| 630 | + validation_error_ = "Failed to set certificate compression preference"; | ||
| 631 | + return SSLCtxPointer(); | ||
| 632 | + } | ||
| 633 | + // Pre-compress the loaded certificate(s) for the preferred algorithms. | ||
| 634 | + // Returns 0 when no certificate is loaded (e.g. a client context) or | ||
| 635 | + // when compression did not reduce the size; both are non-fatal. | ||
| 636 | + constexpr int kCompressAllAlgs = 0; | ||
| 637 | + SSL_CTX_compress_certs(ctx.get(), kCompressAllAlgs); | ||
| 638 | + } | ||
| 639 | + } | ||
| 640 | + #endif // NODE_OPENSSL_HAS_CERT_COMP | ||
| 641 | + | ||
| 597 | 642 | { | |
| 598 | 643 | ClearErrorOnReturn clear_error_on_return; | |
| 599 | 644 | for (const auto& key : options_.keys) { | |
@@ -730,9 +775,9 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env, | |||
| 730 | 775 | !SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) || | |
| 731 | 776 | !SET(servername) || !SET(ciphers) || !SET(groups) || | |
| 732 | 777 | !SET(verify_private_key) || !SET(keylog) || !SET(port) || | |
| 733 | - !SET(authoritative) || !SET_VECTOR(crypto::KeyObjectData, keys) || | ||
| 734 | - !SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) || | ||
| 735 | - !SET_VECTOR(Store, crl)) { | ||
| 778 | + !SET(certificate_compression) || !SET(authoritative) || | ||
| 779 | + !SET_VECTOR(crypto::KeyObjectData, keys) || !SET_VECTOR(Store, certs) || | ||
| 780 | + !SET_VECTOR(Store, ca) || !SET_VECTOR(Store, crl)) { | ||
| 736 | 781 | return Nothing<Options>(); | |
| 737 | 782 | } | |
| 738 | 783 | ||
@@ -761,6 +806,8 @@ std::string TLSContext::Options::ToString() const { | |||
| 761 | 806 | (verify_private_key ? std::string("yes") : std::string("no")); | |
| 762 | 807 | res += prefix + "ciphers: " + ciphers; | |
| 763 | 808 | res += prefix + "groups: " + groups; | |
| 809 | + res += prefix + | ||
| 810 | + "certificate compression: " + std::to_string(certificate_compression); | ||
| 764 | 811 | res += prefix + "keys: " + std::to_string(keys.size()); | |
| 765 | 812 | res += prefix + "certs: " + std::to_string(certs.size()); | |
| 766 | 813 | res += prefix + "ca: " + std::to_string(ca.size()); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -192,6 +192,16 @@ class TLSContext final : public MemoryRetainer, | |||
| 192 | 192 | // The list of TLS groups to use for this session. | |
| 193 | 193 | std::string groups = DEFAULT_GROUPS; | |
| 194 | 194 | ||
| 195 | + // TLS certificate compression (RFC 8879) preference, packed by the JS | ||
| 196 | + // layer as (length | alg0<<8 | alg1<<16 | alg2<<24). Algorithm IDs match | ||
| 197 | + // OpenSSL's TLSEXT_comp_cert_zlib (1), _brotli (2), _zstd (3). A value of | ||
| 198 | + // 0 means certificate compression is disabled (the default). Certificate | ||
| 199 | + // compression is particularly valuable for QUIC because it reduces the | ||
| 200 | + // size of the server's Certificate message, which is otherwise likely to | ||
| 201 | + // exceed the anti-amplification limit and force an extra handshake round | ||
| 202 | + // trip. JavaScript option name "certificateCompression". | ||
| 203 | + uint32_t certificate_compression = 0; | ||
| 204 | + | ||
| 195 | 205 | // When true, enables keylog output for the session. | |
| 196 | 206 | bool keylog = false; | |
| 197 | 207 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,112 @@ | |||
| 1 | + // Flags: --experimental-quic --no-warnings | ||
| 2 | + | ||
| 3 | + import { hasQuic, skip, mustCall } from '../common/index.mjs'; | ||
| 4 | + import assert from 'node:assert'; | ||
| 5 | + import * as fixtures from '../common/fixtures.mjs'; | ||
| 6 | + | ||
| 7 | + if (!hasQuic) { | ||
| 8 | + skip('QUIC is not enabled'); | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + const { listen, connect } = await import('node:quic'); | ||
| 12 | + const { createPrivateKey } = await import('node:crypto'); | ||
| 13 | + const tls = await import('node:tls'); | ||
| 14 | + | ||
| 15 | + const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); | ||
| 16 | + const cert = fixtures.readKey('agent1-cert.pem'); | ||
| 17 | + | ||
| 18 | + // Certificate compression (RFC 8879) depends on the OpenSSL build linking in | ||
| 19 | + // the compression libraries. Reuse the node:tls detection helper to decide | ||
| 20 | + // whether the feature is available. | ||
| 21 | + const supported = tls.getCertificateCompressionAlgorithms(); | ||
| 22 | + if (supported.length === 0) { | ||
| 23 | + skip('certificate compression not supported by this OpenSSL build'); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + // --------------------------------------------------------------------------- | ||
| 27 | + // Option validation. The certificateCompression option is parsed by | ||
| 28 | + // processTlsOptions during connect()/listen(), so invalid values reject the | ||
| 29 | + // returned promise before any network activity happens. | ||
| 30 | + // --------------------------------------------------------------------------- | ||
| 31 | + { | ||
| 32 | + // Non-array values are rejected. | ||
| 33 | + await assert.rejects( | ||
| 34 | + connect('127.0.0.1:4433', { certificateCompression: true }), | ||
| 35 | + { code: 'ERR_INVALID_ARG_TYPE' }); | ||
| 36 | + | ||
| 37 | + await assert.rejects( | ||
| 38 | + connect('127.0.0.1:4433', { certificateCompression: 'zlib' }), | ||
| 39 | + { code: 'ERR_INVALID_ARG_TYPE' }); | ||
| 40 | + | ||
| 41 | + // Unknown algorithm names are rejected. | ||
| 42 | + await assert.rejects( | ||
| 43 | + connect('127.0.0.1:4433', { certificateCompression: ['invalid'] }), | ||
| 44 | + { code: 'ERR_INVALID_ARG_VALUE', | ||
| 45 | + message: /must be 'zlib', 'brotli', or 'zstd'/ }); | ||
| 46 | + | ||
| 47 | + // Non-string entries are rejected. | ||
| 48 | + await assert.rejects( | ||
| 49 | + connect('127.0.0.1:4433', { certificateCompression: [1] }), | ||
| 50 | + { code: 'ERR_INVALID_ARG_VALUE', | ||
| 51 | + message: /must be 'zlib', 'brotli', or 'zstd'/ }); | ||
| 52 | + | ||
| 53 | + // At most three algorithms may be specified. | ||
| 54 | + await assert.rejects( | ||
| 55 | + connect('127.0.0.1:4433', { | ||
| 56 | + certificateCompression: ['zlib', 'brotli', 'zstd', 'zlib'], | ||
| 57 | + }), | ||
| 58 | + { code: 'ERR_INVALID_ARG_VALUE', message: /at most 3 algorithms/ }); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + // --------------------------------------------------------------------------- | ||
| 62 | + // Functional handshakes. Enabling certificate compression must not break the | ||
| 63 | + // TLS 1.3 handshake. The on-the-wire size reduction is exercised by the | ||
| 64 | + // node:tls certificate compression test; over QUIC the payload is encrypted | ||
| 65 | + // and carried in UDP datagrams, so here we assert the handshake completes | ||
| 66 | + // successfully with the option enabled on the server, the client, or both. | ||
| 67 | + // --------------------------------------------------------------------------- | ||
| 68 | + async function handshake({ serverAlgs, clientAlgs }) { | ||
| 69 | + const serverOpened = Promise.withResolvers(); | ||
| 70 | + | ||
| 71 | + const endpoint = await listen(mustCall(async (serverSession) => { | ||
| 72 | + const info = await serverSession.opened; | ||
| 73 | + assert.strictEqual(info.protocol, 'h3'); | ||
| 74 | + serverOpened.resolve(); | ||
| 75 | + serverSession.close(); | ||
| 76 | + }), { | ||
| 77 | + sni: { '*': { keys: [key], certs: [cert] } }, | ||
| 78 | + ...(serverAlgs !== undefined ? | ||
| 79 | + { certificateCompression: serverAlgs } : {}), | ||
| 80 | + }); | ||
| 81 | + | ||
| 82 | + const clientSession = await connect(endpoint.address, { | ||
| 83 | + servername: 'localhost', | ||
| 84 | + verifyPeer: 'manual', | ||
| 85 | + ...(clientAlgs !== undefined ? | ||
| 86 | + { certificateCompression: clientAlgs } : {}), | ||
| 87 | + }); | ||
| 88 | + | ||
| 89 | + const info = await clientSession.opened; | ||
| 90 | + assert.strictEqual(info.protocol, 'h3'); | ||
| 91 | + | ||
| 92 | + await serverOpened.promise; | ||
| 93 | + await clientSession.close(); | ||
| 94 | + await endpoint.close(); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + // Each supported algorithm negotiates a successful handshake when enabled on | ||
| 98 | + // both peers. | ||
| 99 | + for (const algo of supported) { | ||
| 100 | + await handshake({ serverAlgs: [algo], clientAlgs: [algo] }); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + // All supported algorithms advertised together. | ||
| 104 | + await handshake({ serverAlgs: supported, clientAlgs: supported }); | ||
| 105 | + | ||
| 106 | + // Asymmetric configurations must also complete: a peer that does not advertise | ||
| 107 | + // compression simply receives an uncompressed certificate. | ||
| 108 | + await handshake({ serverAlgs: [supported[0]], clientAlgs: undefined }); | ||
| 109 | + await handshake({ serverAlgs: undefined, clientAlgs: [supported[0]] }); | ||
| 110 | + | ||
| 111 | + // An empty array is equivalent to disabling compression. | ||
| 112 | + await handshake({ serverAlgs: [], clientAlgs: [] }); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments