Skip to content

Commit

Permalink
Merge pull request #94 from thekuwayama/ech_outer_extensions
Browse files Browse the repository at this point in the history
feat: support ech_outer_extensions
  • Loading branch information
thekuwayama authored Apr 18, 2024
2 parents 4b7c92d 7f185fe commit 4439b06
Show file tree
Hide file tree
Showing 18 changed files with 288 additions and 34 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 4,9 @@ AllCops:
Gemspec/RequiredRubyVersion:
Enabled: false

Semicolon:
AllowAsExpressionSeparator: true

Style/ConditionalAssignment:
Enabled: false

Expand Down
11 changes: 11 additions & 0 deletions example/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 94,14 @@ def parse_echconfigs_pem(pem)

ECHConfig.decode_vectors(b.slice(2..))
end

def resolve_echconfig(hostname)
rr = Resolv::DNS.new.getresources(
hostname,
Resolv::DNS::Resource::IN::HTTPS
)
raise "failed to resolve echconfig via #{hostname} HTTPS RR" \
if rr.first.nil? || !rr.first.svc_params.keys.include?('ech')

rr.first.svc_params['ech'].echconfiglist.first
end
6 changes: 4 additions & 2 deletions example/https_client_using_0rtt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 9,8 @@

settings_2nd = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1']
alpn: ['http/1.1'],
sslkeylogfile: '/tmp/sslkeylogfile.log'
}
process_new_session_ticket = lambda do |nst, rms, cs|
return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
Expand All @@ -24,7 25,8 @@
settings_1st = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1'],
process_new_session_ticket: process_new_session_ticket
process_new_session_ticket: process_new_session_ticket,
sslkeylogfile: '/tmp/sslkeylogfile.log'
}

succeed_early_data = false
Expand Down
6 changes: 1 addition & 5 deletions example/https_client_using_ech.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 9,7 @@
ech_config = if ARGV.length > 1
parse_echconfigs_pem(File.open(ARGV[1]).read).first
else
rr = Resolv::DNS.new.getresources(
uri.host,
Resolv::DNS::Resource::IN::HTTPS
)
rr.first.svc_params['ech'].echconfiglist.first
resolve_echconfig(uri.host)
end

socket = TCPSocket.new(uri.host, uri.port)
Expand Down
3 changes: 2 additions & 1 deletion example/https_client_using_hrr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 11,8 @@
settings = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
key_share_groups: [], # empty KeyShareClientHello.client_shares
alpn: ['http/1.1']
alpn: ['http/1.1'],
sslkeylogfile: '/tmp/sslkeylogfile.log'
}
client = TTTLS13::Client.new(socket, uri.host, **settings)
client.connect
Expand Down
6 changes: 1 addition & 5 deletions example/https_client_using_hrr_and_ech.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 9,7 @@
ech_config = if ARGV.length > 1
parse_echconfigs_pem(File.open(ARGV[1]).read).first
else
rr = Resolv::DNS.new.getresources(
uri.host,
Resolv::DNS::Resource::IN::HTTPS
)
rr.first.svc_params['ech'].echconfiglist.first
resolve_echconfig(uri.host)
end

socket = TCPSocket.new(uri.host, uri.port)
Expand Down
6 changes: 4 additions & 2 deletions example/https_client_using_hrr_and_ticket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 9,8 @@

settings_2nd = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1']
alpn: ['http/1.1'],
sslkeylogfile: '/tmp/sslkeylogfile.log'
}
process_new_session_ticket = lambda do |nst, rms, cs|
return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
Expand All @@ -25,7 26,8 @@
settings_1st = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1'],
process_new_session_ticket: process_new_session_ticket
process_new_session_ticket: process_new_session_ticket,
sslkeylogfile: '/tmp/sslkeylogfile.log'
}

[
Expand Down
3 changes: 2 additions & 1 deletion example/https_client_using_status_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 19,8 @@
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1'],
check_certificate_status: true,
process_certificate_status: process_certificate_status
process_certificate_status: process_certificate_status,
sslkeylogfile: '/tmp/sslkeylogfile.log'
}
client = TTTLS13::Client.new(socket, uri.host, **settings)
client.connect
Expand Down
6 changes: 4 additions & 2 deletions example/https_client_using_ticket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 9,8 @@

settings_2nd = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1']
alpn: ['http/1.1'],
sslkeylogfile: '/tmp/sslkeylogfile.log'
}
process_new_session_ticket = lambda do |nst, rms, cs|
return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
Expand All @@ -24,7 25,8 @@
settings_1st = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1'],
process_new_session_ticket: process_new_session_ticket
process_new_session_ticket: process_new_session_ticket,
sslkeylogfile: '/tmp/sslkeylogfile.log'
}

[
Expand Down
57 changes: 57 additions & 0 deletions example/https_client_using_ticket_and_ech.rb
Original file line number Diff line number Diff line change
@@ -0,0 1,57 @@
# encoding: ascii-8bit
# frozen_string_literal: true

require_relative 'helper'

uri = URI.parse(ARGV[0] || 'https://localhost:4433')
ca_file = __dir__ '/../tmp/ca.crt'
req = simple_http_request(uri.host, uri.path)
ech_config = if ARGV.length > 1
parse_echconfigs_pem(File.open(ARGV[1]).read).first
else
resolve_echconfig(uri.host)
end

settings_2nd = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1'],
ech_config: ech_config,
ech_hpke_cipher_suites:
TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
sslkeylogfile: '/tmp/sslkeylogfile.log'
}
process_new_session_ticket = lambda do |nst, rms, cs|
return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime

settings_2nd[:ticket] = nst.ticket
settings_2nd[:resumption_main_secret] = rms
settings_2nd[:psk_cipher_suite] = cs
settings_2nd[:ticket_nonce] = nst.ticket_nonce
settings_2nd[:ticket_age_add] = nst.ticket_age_add
settings_2nd[:ticket_timestamp] = nst.timestamp
end
settings_1st = {
ca_file: File.exist?(ca_file) ? ca_file : nil,
alpn: ['http/1.1'],
ech_config: ech_config,
ech_hpke_cipher_suites:
TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
process_new_session_ticket: process_new_session_ticket,
sslkeylogfile: '/tmp/sslkeylogfile.log'
}

[
# Initial Handshake:
settings_1st,
# Subsequent Handshake:
settings_2nd
].each do |settings|
socket = TCPSocket.new(uri.host, uri.port)
client = TTTLS13::Client.new(socket, uri.host, **settings)
client.connect
client.write(req)

print recv_http_response(client)
client.close unless client.eof?
socket.close
end
48 changes: 32 additions & 16 deletions lib/tttls1.3/ech.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 7,11 @@ module TTTLS13
SUPPORTED_ECHCONFIG_VERSIONS = ["\xfe\x0d"].freeze
private_constant :SUPPORTED_ECHCONFIG_VERSIONS

DEFAULT_ECH_OUTER_EXTENSIONS = [
Message::ExtensionType::KEY_SHARE
].freeze
private_constant :DEFAULT_ECH_OUTER_EXTENSIONS

# rubocop: disable Metrics/ClassLength
class Ech
# @param inner [TTTLS13::Message::ClientHello]
Expand All @@ -30,12 35,15 @@ def self.offer_ech(inner, ech_config, hpke_cipher_suite_selector)
return [new_greased_ch(inner, new_grease_ech), nil, nil] \
if ech_state.nil? || enc.nil?

encoded = encode_ch_inner(inner, ech_state.maximum_name_length)
overhead_len = aead_id2overhead_len(
ech_state.cipher_suite.aead_id.uint16
)
# for ech_outer_extensions
replaced = \
inner.extensions.remove_and_replace!(DEFAULT_ECH_OUTER_EXTENSIONS)

# Encoding the ClientHelloInner
encoded = encode_ch_inner(inner, ech_state.maximum_name_length, replaced)
overhead_len = aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)

# Authenticating the ClientHelloOuter
aad = new_ch_outer_aad(
inner,
ech_state.cipher_suite,
Expand All @@ -44,13 52,13 @@ def self.offer_ech(inner, ech_config, hpke_cipher_suite_selector)
encoded.length overhead_len,
ech_state.public_name
)
# Authenticating the ClientHelloOuter
# which does not include the Handshake structure's four byte header.

outer = new_ch_outer(
aad,
ech_state.cipher_suite,
ech_state.config_id,
enc,
# which does not include the Handshake structure's four byte header.
ech_state.ctx.seal(aad.serialize[4..], encoded)
)

Expand Down Expand Up @@ -104,9 112,14 @@ def self.encrypted_ech_config(ech_config, hpke_cipher_suite_selector)
# @return [TTTLS13::Message::ClientHello]
# @return [TTTLS13::Message::ClientHello]
def self.offer_new_ech(inner, ech_state)
encoded = encode_ch_inner(inner, ech_state.maximum_name_length)
overhead_len \
= aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)
# for ech_outer_extensions
replaced = \
inner.extensions.remove_and_replace!(DEFAULT_ECH_OUTER_EXTENSIONS)

# Encoding the ClientHelloInner
encoded = encode_ch_inner(inner, ech_state.maximum_name_length, replaced)
overhead_len = \
aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)

# It encrypts EncodedClientHelloInner as described in Section 6.1.1, using
# the second partial ClientHelloOuterAAD, to obtain a second
Expand All @@ -122,13 135,14 @@ def self.offer_new_ech(inner, ech_state)
encoded.length overhead_len,
ech_state.public_name
)

# Authenticating the ClientHelloOuter
# which does not include the Handshake structure's four byte header.
outer = new_ch_outer(
aad,
ech_state.cipher_suite,
ech_state.config_id,
'',
# which does not include the Handshake structure's four byte header.
ech_state.ctx.seal(aad.serialize[4..], encoded)
)

Expand All @@ -137,23 151,23 @@ def self.offer_new_ech(inner, ech_state)

# @param inner [TTTLS13::Message::ClientHello]
# @param maximum_name_length [Integer]
# @param replaced [TTTLS13::Message::Extensions]
#
# @return [String] EncodedClientHelloInner
def self.encode_ch_inner(inner, maximum_name_length)
# TODO: ech_outer_extensions
def self.encode_ch_inner(inner, maximum_name_length, replaced)
encoded = Message::ClientHello.new(
legacy_version: inner.legacy_version,
random: inner.random,
legacy_session_id: '',
cipher_suites: inner.cipher_suites,
legacy_compression_methods: inner.legacy_compression_methods,
extensions: inner.extensions
extensions: replaced
)
server_name_length = \
inner.extensions[Message::ExtensionType::SERVER_NAME].server_name.length
replaced[Message::ExtensionType::SERVER_NAME].server_name.length

# which does not include the Handshake structure's four byte header.
padding_encoded_ch_inner(
# which does not include the Handshake structure's four byte header.
encoded.serialize[4..],
server_name_length,
maximum_name_length
Expand Down Expand Up @@ -284,6 298,8 @@ def self.placeholder_encoded_ch_inner_len

# @param inner [TTTLS13::Message::ClientHello]
# @param ech [Message::Extension::ECHClientHello]
#
# @return [TTTLS13::Message::ClientHello]
def self.new_greased_ch(inner, ech)
Message::ClientHello.new(
legacy_version: inner.legacy_version,
Expand Down Expand Up @@ -393,7 409,7 @@ class EchState
# @param config_id [Integer]
# @param cipher_suite [HpkeSymmetricCipherSuite]
# @param public_name [String]
# @param ctx [[HPKE::ContextS]
# @param ctx [HPKE::ContextS]
def initialize(maximum_name_length,
config_id,
cipher_suite,
Expand Down
1 change: 1 addition & 0 deletions lib/tttls1.3/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 74,7 @@ module ExtensionType
KEY_SHARE = "\x00\x33"
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-11.1
ENCRYPTED_CLIENT_HELLO = "\xfe\x0d"
ECH_OUTER_EXTENSIONS = "\xfd\x00"
end

DEFINED_EXTENSIONS = ExtensionType.constants.map do |c|
Expand Down
52 changes: 52 additions & 0 deletions lib/tttls1.3/message/extension/ech_outer_extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 1,52 @@
# encoding: ascii-8bit
# frozen_string_literal: true

module TTTLS13
using Refinements
module Message
module Extension
# NOTE:
# ExtensionType OuterExtensions<2..254>;
class ECHOuterExtensions
attr_reader :extension_type
attr_reader :outer_extensions

# @param outer_extensions [Array of TTTLS13::Message::ExtensionType]
def initialize(outer_extensions)
@extension_type = ExtensionType::ECH_OUTER_EXTENSIONS
@outer_extensions = outer_extensions
end

# @raise [TTTLS13::Error::ErrorAlerts]
#
# @return [String]
def serialize
binary = @outer_extensions.join.prefix_uint8_length
@extension_type binary.prefix_uint16_length
end

# @param binary [String]
#
# @raise [TTTLS13::Error::ErrorAlerts]
#
# @return [TTTLS13::Message::Extensions::ECHOuterExtensions]
def self.deserialize(binary)
raise Error::ErrorAlerts, :internal_error if binary.nil?

return nil if binary.length < 2

exlist_len = Convert.bin2i(binary.slice(0, 1))
i = 1
outer_extensions = []
while i < exlist_len 1
outer_extensions << binary.slice(i, 2)
i = 2
end
return nil unless outer_extensions.length * 2 == exlist_len

ECHOuterExtensions.new(outer_extensions)
end
end
end
end
end
Loading

0 comments on commit 4439b06

Please sign in to comment.