Skip to content

Commit

Permalink
Merge pull request #62 from thekuwayama/ech__3
Browse files Browse the repository at this point in the history
[ech] 3. feat: client grease ECH
  • Loading branch information
thekuwayama authored Dec 17, 2023
2 parents 6fc732f + 3989466 commit 5e18322
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 12 deletions.
5 changes: 3 additions & 2 deletions example/https_client_using_ech.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

require_relative 'helper'
require 'svcb_rr_patch'

HpkeSymmetricCipherSuite = \
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite

hostname = 'crypto.cloudflare.com'
port = 443
ca_file = __dir__ + '/../tmp/ca.crt'
Expand All @@ -30,7 +30,8 @@
)
)
],
sslkeylogfile: '/tmp/sslkeylogfile.log'
sslkeylogfile: '/tmp/sslkeylogfile.log',
loglevel: Logger::DEBUG
}
client = TTTLS13::Client.new(socket, hostname, **settings)
client.connect
Expand Down
74 changes: 67 additions & 7 deletions lib/tttls1.3/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -763,14 +766,15 @@ 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 ECH
# FIXME: support GREASE PSK
abort('GREASE ECH') \
return [new_greased_ch(inner, new_grease_ech), nil] \
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
Expand All @@ -780,19 +784,20 @@ 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), 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)
abort('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)

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,
Expand All @@ -801,14 +806,17 @@ 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(
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
Expand Down Expand Up @@ -925,6 +933,58 @@ 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.
cipher_suite = HpkeSymmetricCipherSuite.new(
HpkeSymmetricCipherSuite::HpkeKdfId.new(
TTTLS13::Hpke::KdfId::HKDF_SHA256
),
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)
enc = hpke.setup_base_s(public_key, '')[:enc]
payload_len = placeholder_encoded_ch_inner_len \
+ Hpke.aead_id2overhead_len(Hpke::AeadId::AES_128_GCM)

Message::Extension::ECHClientHello.new_outer(
cipher_suite: cipher_suite,
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
Expand Down
15 changes: 15 additions & 0 deletions lib/tttls1.3/hpke.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ def self.kem_curve_name2dhkem(kem_curve_name)
end
end

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

module KdfId
# https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-kdf-ids
HKDF_SHA256 = 0x0001
Expand Down
3 changes: 2 additions & 1 deletion lib/tttls1.3/message/client_hello.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion lib/tttls1.3/message/encrypted_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion lib/tttls1.3/message/server_hello.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,44 @@
'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(:encoded) do
extensions, = client.send(:gen_ch_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

0 comments on commit 5e18322

Please sign in to comment.