From fd48f13fcd6faa47c8301712fa0b4efd3206d50d Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 19:45:01 +0900 Subject: [PATCH 1/8] add new_grease_ech & placeholder_encoded_ch_inner_length --- lib/tttls1.3/client.rb | 29 +++++++++++++++++++++++++++++ lib/tttls1.3/hpke.rb | 13 +++++++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index 83babef..1b40c53 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -925,6 +925,35 @@ def select_ech_hpke_cipher_suite(conf) end end + # @return [Message::Extension::ECHClientHello] + def new_grease_ech + # Set the enc field to a randomly-generated valid encapsulated public key + # output by the HPKE KEM. + # Set the payload field to a randomly-generated string of L+C bytes, where + # C is the ciphertext expansion of the selected AEAD scheme and L is the + # size of the EncodedClientHelloInner the client would compute when + # offering ECH, padded according to Section 6.1.3. + public_key = OpenSSL::PKey.read( + OpenSSL::PKey.generate_key('X25519').public_to_pem + ) + hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm) + ctx = hpke.setup_base_s(public_key, '')[:context_s] + payload_len = placeholder_encoded_ch_inner_length \ + + Hpke.aead_id2overhead_len(:aes_128_gcm) + + Message::Extension::ECHClientHello.new_outer( + cipher_suite: cipher_suite, + config_id: OpenSSL::Random.random_bytes(1), + enc: ctx[:enc], + payload: OpenSSL::Random.random_bytes(payload_len) + ) + end + + def placeholder_encoded_ch_inner_length + # FIXME + 207 + end + # @return [Integer] def calc_obfuscated_ticket_age # the "ticket_lifetime" field in the NewSessionTicket message is diff --git a/lib/tttls1.3/hpke.rb b/lib/tttls1.3/hpke.rb index 739eee9..2c77cd3 100644 --- a/lib/tttls1.3/hpke.rb +++ b/lib/tttls1.3/hpke.rb @@ -40,6 +40,19 @@ def self.kem_curve_name2dhkem(kem_curve_name) HPKE::DHKEM::X25519 when :x448 HPKE::DHKEM::X448 + + def self.kem_id2enc_len(kem_id) + case kem_id + when KemId::P_256_SHA256 + 65 + when KemId::P_384_SHA384 + 97 + when KemId::P_521_SHA512 + 133 + when KemId::X25519_SHA256 + 32 + when KemId::X448_SHA512 + 56 end end From 925c09fea09bd74779be3655f95ebf92dcb25fd4 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 20:17:14 +0900 Subject: [PATCH 2/8] rename placeholder_encoded_ch_inner_length => placeholder_encoded_ch_inner_len --- lib/tttls1.3/client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index 1b40c53..16cbef3 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -938,7 +938,7 @@ def new_grease_ech ) hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm) ctx = hpke.setup_base_s(public_key, '')[:context_s] - payload_len = placeholder_encoded_ch_inner_length \ + payload_len = placeholder_encoded_ch_inner_len \ + Hpke.aead_id2overhead_len(:aes_128_gcm) Message::Extension::ECHClientHello.new_outer( @@ -949,7 +949,7 @@ def new_grease_ech ) end - def placeholder_encoded_ch_inner_length + def placeholder_encoded_ch_inner_len # FIXME 207 end From b808d39bb316ccf856aa2cac7ffb8db2df515136 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 20:17:40 +0900 Subject: [PATCH 3/8] add testcase for placeholder_encoded_ch_inner_len --- lib/tttls1.3/client.rb | 3 +-- spec/client_spec.rb | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index 16cbef3..9501ddc 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -950,8 +950,7 @@ def new_grease_ech end def placeholder_encoded_ch_inner_len - # FIXME - 207 + 448 end # @return [Integer] diff --git a/spec/client_spec.rb b/spec/client_spec.rb index a1090c3..c200806 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -270,4 +270,49 @@ 'SHA256')).to eq TESTBINARY_0_RTT_PSK end end + + context 'EncodedClientHelloInner length' do + let(:server_name) do + 'localhost' + end + + let(:client) do + Client.new(nil, server_name) + end + + let(:maximum_name_length) do + 0 + end + + let(:extensions) do + extensions, = client.send(:gen_ch_extensions) + extensions + end + + let(:encoded) do + extensions + inner_ech = Message::Extension::ECHClientHello.new_inner + Message::ClientHello.new( + legacy_session_id: '', + cipher_suites: CipherSuites.new(DEFAULT_CH_CIPHER_SUITES), + extensions: extensions.merge( + Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech + ) + ) + end + + let(:padding_encoded_ch_inner) do + client.send( + :padding_encoded_ch_inner, + encoded.serialize[4..], + server_name.length, + maximum_name_length + ) + end + + it 'should be equal placeholder_encoded_ch_inner_len' do + expect(client.send(:placeholder_encoded_ch_inner_len)) + .to eq padding_encoded_ch_inner.length + end + end end From 37c2e54a95038e48c287fdafaef6756ac4371945 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 20:43:57 +0900 Subject: [PATCH 4/8] add new_greased_ch --- lib/tttls1.3/client.rb | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index 9501ddc..f44c51c 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -768,7 +768,7 @@ def sign_psk_binder(ch1: nil, hrr: nil, ch:, binder_key:) def offer_ech(inner, ech_config) # FIXME: support GREASE ECH # FIXME: support GREASE PSK - abort('GREASE ECH') \ + return new_greased_ch(inner, new_grease_ech) \ unless SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version) public_name = ech_config.echconfig_contents.public_name @@ -780,13 +780,13 @@ def offer_ech(inner, ech_config) overhead_len = Hpke.aead_id2overhead_len(cipher_suite&.aead_id&.uint16) aead_cipher = Hpke.aead_id2aead_cipher(cipher_suite&.aead_id&.uint16) kdf_hash = Hpke.kdf_id2kdf_hash(cipher_suite&.kdf_id&.uint16) - abort('GREASE ECH') \ + return new_greased_ch(inner, new_grease_ech) \ if [kem_id, overhead_len, aead_cipher, kdf_hash].any?(&:nil?) kem_curve_name, kem_hash = Hpke.kem_id2dhkem(kem_id) dhkem = Hpke.kem_curve_name2dhkem(kem_curve_name) pkr = dhkem&.new(kem_hash)&.deserialize_public_key(public_key) - abort('GREASE ECH') if pkr.nil? + return new_greased_ch(inner, new_grease_ech) if pkr.nil? hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher) ctx = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode) @@ -933,26 +933,50 @@ def new_grease_ech # C is the ciphertext expansion of the selected AEAD scheme and L is the # size of the EncodedClientHelloInner the client would compute when # offering ECH, padded according to Section 6.1.3. + cipher_suite = ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite.new( + ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId.new( + TTTLS13::Hpke::KdfId::HKDF_SHA256 + ), + ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId.new( + TTTLS13::Hpke::AeadId::AES_128_GCM + ) + ) public_key = OpenSSL::PKey.read( OpenSSL::PKey.generate_key('X25519').public_to_pem ) hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm) - ctx = hpke.setup_base_s(public_key, '')[:context_s] + enc = hpke.setup_base_s(public_key, '')[:enc] payload_len = placeholder_encoded_ch_inner_len \ - + Hpke.aead_id2overhead_len(:aes_128_gcm) + + Hpke.aead_id2overhead_len(Hpke::AeadId::AES_128_GCM) Message::Extension::ECHClientHello.new_outer( cipher_suite: cipher_suite, - config_id: OpenSSL::Random.random_bytes(1), - enc: ctx[:enc], + config_id: Convert.bin2i(OpenSSL::Random.random_bytes(1)), + enc: enc, payload: OpenSSL::Random.random_bytes(payload_len) ) end + # @return [Integer] def placeholder_encoded_ch_inner_len 448 end + # @param inner [TTTLS13::Message::ClientHello] + # @param ech [Message::Extension::ECHClientHello] + def new_greased_ch(inner, ech) + Message::ClientHello.new( + legacy_version: inner.legacy_version, + random: inner.random, + legacy_session_id: inner.legacy_session_id, + cipher_suites: inner.cipher_suites, + legacy_compression_methods: inner.legacy_compression_methods, + extensions: inner.extensions.merge( + Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => ech + ) + ) + end + # @return [Integer] def calc_obfuscated_ticket_age # the "ticket_lifetime" field in the NewSessionTicket message is From 80a4f12fdff5619fbb7cde69c8bcaebc4dd25e38 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 22:26:47 +0900 Subject: [PATCH 5/8] clean: for rubocop fix: git-rebase --- example/https_client_using_ech.rb | 4 ++-- lib/tttls1.3/client.rb | 8 +++++--- lib/tttls1.3/hpke.rb | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/example/https_client_using_ech.rb b/example/https_client_using_ech.rb index e51b263..8d5a2a1 100644 --- a/example/https_client_using_ech.rb +++ b/example/https_client_using_ech.rb @@ -3,9 +3,9 @@ require_relative 'helper' require 'svcb_rr_patch' - -HpkeSymmetricCipherSuite = \ +self.class.include \ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite + hostname = 'crypto.cloudflare.com' port = 443 ca_file = __dir__ + '/../tmp/ca.crt' diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index f44c51c..cf084f9 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -933,11 +933,13 @@ def new_grease_ech # C is the ciphertext expansion of the selected AEAD scheme and L is the # size of the EncodedClientHelloInner the client would compute when # offering ECH, padded according to Section 6.1.3. - cipher_suite = ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite.new( - ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId.new( + self.class.include \ + ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuit + cipher_suite = HpkeSymmetricCipherSuite.new( + HpkeSymmetricCipherSuite::HpkeKdfId.new( TTTLS13::Hpke::KdfId::HKDF_SHA256 ), - ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId.new( + HpkeSymmetricCipherSuite::HpkeAeadId.new( TTTLS13::Hpke::AeadId::AES_128_GCM ) ) diff --git a/lib/tttls1.3/hpke.rb b/lib/tttls1.3/hpke.rb index 2c77cd3..648a7e4 100644 --- a/lib/tttls1.3/hpke.rb +++ b/lib/tttls1.3/hpke.rb @@ -40,6 +40,8 @@ def self.kem_curve_name2dhkem(kem_curve_name) HPKE::DHKEM::X25519 when :x448 HPKE::DHKEM::X448 + end + end def self.kem_id2enc_len(kem_id) case kem_id From f8a5047f5658de2e39b6bf6fa92191df91d164f9 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 22:29:08 +0900 Subject: [PATCH 6/8] clean: rm unnecessary let --- spec/client_spec.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index c200806..0535c2c 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -284,13 +284,8 @@ 0 end - let(:extensions) do - extensions, = client.send(:gen_ch_extensions) - extensions - end - let(:encoded) do - extensions + extensions, = client.send(:gen_ch_extensions) inner_ech = Message::Extension::ECHClientHello.new_inner Message::ClientHello.new( legacy_session_id: '', From 2e39ef07b20a6991bf1f56a35cfd7e529653cba5 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 22:34:36 +0900 Subject: [PATCH 7/8] docs: comments --- lib/tttls1.3/client.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index cf084f9..c63207e 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -766,11 +766,11 @@ def sign_psk_binder(ch1: nil, hrr: nil, ch:, binder_key:) # rubocop: disable Metrics/AbcSize # rubocop: disable Metrics/MethodLength def offer_ech(inner, ech_config) - # FIXME: support GREASE ECH # FIXME: support GREASE PSK return new_greased_ch(inner, new_grease_ech) \ unless SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version) + # Encrypted ClientHello Configuration public_name = ech_config.echconfig_contents.public_name key_config = ech_config.echconfig_contents.key_config public_key = key_config.public_key.opaque @@ -790,9 +790,10 @@ def offer_ech(inner, ech_config) hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher) ctx = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode) - mnl = ech_config.echconfig_contents.maximum_name_length encoded = encode_ch_inner(inner, mnl) + + # Encoding the ClientHelloInner aad = new_ch_outer_aad( inner, cipher_suite, @@ -801,6 +802,7 @@ def offer_ech(inner, ech_config) encoded.length + overhead_len, public_name ) + # Authenticating the ClientHelloOuter # which does not include the Handshake structure's four byte header. new_ch_outer( aad, From 3989466414e9beec2899016f2160cf63fba6b048 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sun, 17 Dec 2023 23:01:11 +0900 Subject: [PATCH 8/8] fix: appearable_extensions? fix: using Class alias --- example/https_client_using_ech.rb | 5 +++-- lib/tttls1.3/client.rb | 18 +++++++++++------- lib/tttls1.3/message/client_hello.rb | 3 ++- lib/tttls1.3/message/encrypted_extensions.rb | 3 ++- lib/tttls1.3/message/server_hello.rb | 3 ++- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/example/https_client_using_ech.rb b/example/https_client_using_ech.rb index 8d5a2a1..f6bb362 100644 --- a/example/https_client_using_ech.rb +++ b/example/https_client_using_ech.rb @@ -3,7 +3,7 @@ require_relative 'helper' require 'svcb_rr_patch' -self.class.include \ +HpkeSymmetricCipherSuite = \ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite hostname = 'crypto.cloudflare.com' @@ -30,7 +30,8 @@ ) ) ], - sslkeylogfile: '/tmp/sslkeylogfile.log' + sslkeylogfile: '/tmp/sslkeylogfile.log', + loglevel: Logger::DEBUG } client = TTTLS13::Client.new(socket, hostname, **settings) client.connect diff --git a/lib/tttls1.3/client.rb b/lib/tttls1.3/client.rb index c63207e..9ae60a6 100644 --- a/lib/tttls1.3/client.rb +++ b/lib/tttls1.3/client.rb @@ -79,6 +79,9 @@ module ClientState # rubocop: disable Metrics/ClassLength class Client < Connection + HpkeSymmetricCipherSuit = \ + ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite + # @param socket [Socket] # @param hostname [String] # @param settings [Hash] @@ -712,7 +715,7 @@ def send_client_hello(extensions, binder_key = nil) inner_ech = Message::Extension::ECHClientHello.new_inner inner.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] \ = inner_ech - ch = offer_ech(inner, @settings[:ech_config]) + ch, inner = offer_ech(inner, @settings[:ech_config]) end send_handshakes(Message::ContentType::HANDSHAKE, [ch], @@ -763,11 +766,12 @@ def sign_psk_binder(ch1: nil, hrr: nil, ch:, binder_key:) # @param ech_config [ECHConfig] # # @return [TTTLS13::Message::ClientHello] + # @return [TTTLS13::Message::ClientHello] # rubocop: disable Metrics/AbcSize # rubocop: disable Metrics/MethodLength def offer_ech(inner, ech_config) # FIXME: support GREASE PSK - return new_greased_ch(inner, new_grease_ech) \ + return [new_greased_ch(inner, new_grease_ech), nil] \ unless SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version) # Encrypted ClientHello Configuration @@ -780,13 +784,13 @@ def offer_ech(inner, ech_config) overhead_len = Hpke.aead_id2overhead_len(cipher_suite&.aead_id&.uint16) aead_cipher = Hpke.aead_id2aead_cipher(cipher_suite&.aead_id&.uint16) kdf_hash = Hpke.kdf_id2kdf_hash(cipher_suite&.kdf_id&.uint16) - return new_greased_ch(inner, new_grease_ech) \ + return [new_greased_ch(inner, new_grease_ech), nil] \ if [kem_id, overhead_len, aead_cipher, kdf_hash].any?(&:nil?) kem_curve_name, kem_hash = Hpke.kem_id2dhkem(kem_id) dhkem = Hpke.kem_curve_name2dhkem(kem_curve_name) pkr = dhkem&.new(kem_hash)&.deserialize_public_key(public_key) - return new_greased_ch(inner, new_grease_ech) if pkr.nil? + return [new_greased_ch(inner, new_grease_ech), nil] if pkr.nil? hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher) ctx = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode) @@ -804,13 +808,15 @@ def offer_ech(inner, ech_config) ) # Authenticating the ClientHelloOuter # which does not include the Handshake structure's four byte header. - new_ch_outer( + outer = new_ch_outer( aad, cipher_suite, config_id, ctx[:enc], ctx[:context_s].seal(aad.serialize[4..], encoded) ) + + [outer, inner] end # rubocop: enable Metrics/AbcSize # rubocop: enable Metrics/MethodLength @@ -935,8 +941,6 @@ def new_grease_ech # C is the ciphertext expansion of the selected AEAD scheme and L is the # size of the EncodedClientHelloInner the client would compute when # offering ECH, padded according to Section 6.1.3. - self.class.include \ - ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuit cipher_suite = HpkeSymmetricCipherSuite.new( HpkeSymmetricCipherSuite::HpkeKdfId.new( TTTLS13::Hpke::KdfId::HKDF_SHA256 diff --git a/lib/tttls1.3/message/client_hello.rb b/lib/tttls1.3/message/client_hello.rb index cb7339e..58c9cbb 100644 --- a/lib/tttls1.3/message/client_hello.rb +++ b/lib/tttls1.3/message/client_hello.rb @@ -31,7 +31,8 @@ module Message ExtensionType::CERTIFICATE_AUTHORITIES, ExtensionType::POST_HANDSHAKE_AUTH, ExtensionType::SIGNATURE_ALGORITHMS_CERT, - ExtensionType::KEY_SHARE + ExtensionType::KEY_SHARE, + ExtensionType::ENCRYPTED_CLIENT_HELLO ].freeze private_constant :APPEARABLE_CH_EXTENSIONS diff --git a/lib/tttls1.3/message/encrypted_extensions.rb b/lib/tttls1.3/message/encrypted_extensions.rb index 828496a..69abe2f 100644 --- a/lib/tttls1.3/message/encrypted_extensions.rb +++ b/lib/tttls1.3/message/encrypted_extensions.rb @@ -15,7 +15,8 @@ module Message ExtensionType::CLIENT_CERTIFICATE_TYPE, ExtensionType::SERVER_CERTIFICATE_TYPE, ExtensionType::RECORD_SIZE_LIMIT, - ExtensionType::EARLY_DATA + ExtensionType::EARLY_DATA, + ExtensionType::ENCRYPTED_CLIENT_HELLO ].freeze private_constant :APPEARABLE_EE_EXTENSIONS diff --git a/lib/tttls1.3/message/server_hello.rb b/lib/tttls1.3/message/server_hello.rb index 2d9ed5e..24e2324 100644 --- a/lib/tttls1.3/message/server_hello.rb +++ b/lib/tttls1.3/message/server_hello.rb @@ -17,7 +17,8 @@ module Message ExtensionType::COOKIE, ExtensionType::PASSWORD_SALT, ExtensionType::SUPPORTED_VERSIONS, - ExtensionType::KEY_SHARE + ExtensionType::KEY_SHARE, + ExtensionType::ENCRYPTED_CLIENT_HELLO ].freeze private_constant :APPEARABLE_HRR_EXTENSIONS