class TTTLS13::Connection

rubocop: disable Metrics/ClassLength

Public Class Methods

new(socket) click to toggle source

@param socket [Socket]

# File lib/tttls1.3/connection.rb, line 13
def initialize(socket)
  @socket = socket
  @endpoint = nil # Symbol or String, :client or :server
  @ap_wcipher = Cryptograph::Passer.new
  @ap_rcipher = Cryptograph::Passer.new
  @alert_wcipher = Cryptograph::Passer.new
  @message_queue = [] # Array of TTTLS13::Message::$Object
  @binary_buffer = '' # deposit Record.surplus_binary
  @cipher_suite = nil # TTTLS13::CipherSuite
  @named_group = nil # TTTLS13::NamedGroup
  @signature_scheme = nil # TTTLS13::SignatureScheme
  @state = 0 # ClientState or ServerState
  @send_record_size = Message::DEFAULT_RECORD_SIZE_LIMIT
  @recv_record_size = Message::DEFAULT_RECORD_SIZE_LIMIT
  @alpn = nil # String
  @exporter_master_secret = nil # String
end

Private Class Methods

gen_ocsp_request(cid) click to toggle source

@param cid [OpenSSL::OCSP::CertificateId]

@return [OpenSSL::OCSP::Request]

# File lib/tttls1.3/connection.rb, line 546
def gen_ocsp_request(cid)
  ocsp_request = OpenSSL::OCSP::Request.new
  ocsp_request.add_certid(cid)
  ocsp_request.add_nonce
  ocsp_request
end
send_ocsp_request(ocsp_request, uri_string) click to toggle source

@param ocsp_request [OpenSSL::OCSP::Request] @param uri_string [String]

@raise [Net::OpenTimeout, OpenSSL::OCSP::OCSPError, URI::$Exception]

@return [OpenSSL::OCSP::Response, n

# File lib/tttls1.3/connection.rb, line 559
def send_ocsp_request(ocsp_request, uri_string)
  # send HTTP POST
  uri = URI.parse(uri_string)
  path = uri.path
  path = '/' if path.nil? || path.empty?
  http_response = Net::HTTP.start(uri.host, uri.port) do |http|
    http.post(
      path,
      ocsp_request.to_der,
      'content-type' => 'application/ocsp-request'
    )
  end

  OpenSSL::OCSP::Response.new(http_response.body)
end

Public Instance Methods

close() click to toggle source
# File lib/tttls1.3/connection.rb, line 77
def close
  return if @state == EOF

  send_alert(:close_notify)
  @state = EOF

  nil
end
eof?() click to toggle source

@return [Boolean]

# File lib/tttls1.3/connection.rb, line 60
def eof?
  @state == EOF
end
exporter(label, context, key_length) click to toggle source

@param label [String] @param context [String] @param key_length [Integer]

@return [String, nil]

# File lib/tttls1.3/connection.rb, line 111
def exporter(label, context, key_length)
  return nil if @exporter_master_secret.nil? || @cipher_suite.nil?

  digest = CipherSuite.digest(@cipher_suite)
  do_exporter(@exporter_master_secret, digest, label, context, key_length)
end
negotiated_alpn() click to toggle source

@return [String]

# File lib/tttls1.3/connection.rb, line 102
def negotiated_alpn
  @alpn
end
negotiated_cipher_suite() click to toggle source

@return [TTTLS13::CipherSuite, nil]

# File lib/tttls1.3/connection.rb, line 87
def negotiated_cipher_suite
  @cipher_suite
end
negotiated_named_group() click to toggle source

@return [TTTLS13::NamedGroup, nil]

# File lib/tttls1.3/connection.rb, line 92
def negotiated_named_group
  @named_group
end
negotiated_signature_scheme() click to toggle source

@return [TTTLS13::SignatureScheme, nil]

# File lib/tttls1.3/connection.rb, line 97
def negotiated_signature_scheme
  @signature_scheme
end
read() click to toggle source

@raise [TTTLS13::Error::ConfigError]

@return [String] rubocop: disable Metrics/CyclomaticComplexity rubocop: disable Metrics/PerceivedComplexity

# File lib/tttls1.3/connection.rb, line 36
def read
  # secure channel has not established yet
  raise Error::ConfigError \
    unless (@endpoint == :client && @state == ClientState::CONNECTED) ||
           (@endpoint == :server && @state == ServerState::CONNECTED)
  return '' if @state == EOF

  message = nil
  loop do
    message = recv_message(receivable_ccs: false, cipher: @ap_rcipher)
    # At any time after the server has received the client Finished
    # message, it MAY send a NewSessionTicket message.
    break unless message.is_a?(Message::NewSessionTicket)

    process_new_session_ticket(message)
  end
  return '' if message.nil?

  message.fragment
end
write(binary) click to toggle source

@param binary [String]

@raise [TTTLS13::Error::ConfigError]

# File lib/tttls1.3/connection.rb, line 67
def write(binary)
  # secure channel has not established yet
  raise Error::ConfigError \
    unless (@endpoint == :client && @state == ClientState::CONNECTED) ||
           (@endpoint == :server && @state == ServerState::CONNECTED)

  ap = Message::ApplicationData.new(binary)
  send_application_data(ap, @ap_wcipher)
end

Private Instance Methods

do_exporter(secret, digest, label, context, key_length) click to toggle source

@param secret [String] (early_)exporter_master_secret @param digest [String] name of digest algorithm @param label [String] @param context [String] @param key_length [Integer]

@return [String]

# File lib/tttls1.3/connection.rb, line 127
def do_exporter(secret, digest, label, context, key_length)
  derived_secret = KeySchedule.hkdf_expand_label(
    secret,
    label,
    OpenSSL::Digest.digest(digest, ''),
    OpenSSL::Digest.new(digest).digest_length,
    digest
  )

  KeySchedule.hkdf_expand_label(
    derived_secret,
    'exporter',
    OpenSSL::Digest.digest(digest, context),
    key_length,
    digest
  )
end
do_select_signature_algorithms(signature_algorithms, crt) click to toggle source

@param signature_algorithms [Array of SignatureAlgorithms] @param crt [OpenSSL::X509::Certificate]

@return [Array of TTTLS13::Message::Extension::SignatureAlgorithms]

# File lib/tttls1.3/connection.rb, line 514
def do_select_signature_algorithms(signature_algorithms, crt)
  spki = OpenSSL::Netscape::SPKI.new
  spki.public_key = crt.public_key
  pka = OpenSSL::ASN1.decode(spki.to_der)
                     .value.first.value.first.value.first.value.first.value
  signature_algorithms.select do |sa|
    case sa
    when SignatureScheme::ECDSA_SECP256R1_SHA256,
         SignatureScheme::ECDSA_SECP384R1_SHA384,
         SignatureScheme::ECDSA_SECP521R1_SHA512
      pka == 'id-ecPublicKey'
    when SignatureScheme::RSA_PSS_PSS_SHA256,
         SignatureScheme::RSA_PSS_PSS_SHA384,
         SignatureScheme::RSA_PSS_PSS_SHA512
      pka == 'rsassaPss'
    when SignatureScheme::RSA_PSS_RSAE_SHA256,
         SignatureScheme::RSA_PSS_RSAE_SHA384,
         SignatureScheme::RSA_PSS_RSAE_SHA512
      pka == 'rsaEncryption'
    else
      # RSASSA-PKCS1-v1_5 algorithms refer solely to signatures which appear
      # in certificates and are not defined for use in signed TLS handshake
      # messages
      false
    end
  end
end
do_sign_certificate_verify(key:, signature_scheme:, context:, hash:) click to toggle source

@param key [OpenSSL::PKey::PKey] @param signature_scheme [TTTLS13::SignatureScheme] @param context [String] @param hash [String]

@raise [TTTLS13::Error::ErrorAlerts]

@return [String] rubocop: disable Metrics/CyclomaticComplexity

# File lib/tttls1.3/connection.rb, line 314
def do_sign_certificate_verify(key:, signature_scheme:, context:, hash:)
  content = "\x20" * 64 + context + "\x00" + hash

  # RSA signatures MUST use an RSASSA-PSS algorithm, regardless of whether
  # RSASSA-PKCS1-v1_5 algorithms appear in "signature_algorithms".
  case signature_scheme
  when SignatureScheme::RSA_PKCS1_SHA256,
       SignatureScheme::RSA_PSS_RSAE_SHA256,
       SignatureScheme::RSA_PSS_PSS_SHA256
    key.sign_pss('SHA256', content, salt_length: :digest,
                                    mgf1_hash: 'SHA256')
  when SignatureScheme::RSA_PKCS1_SHA384,
       SignatureScheme::RSA_PSS_RSAE_SHA384,
       SignatureScheme::RSA_PSS_PSS_SHA384
    key.sign_pss('SHA384', content, salt_length: :digest,
                                    mgf1_hash: 'SHA384')
  when SignatureScheme::RSA_PKCS1_SHA512,
       SignatureScheme::RSA_PSS_RSAE_SHA512,
       SignatureScheme::RSA_PSS_PSS_SHA512
    key.sign_pss('SHA512', content, salt_length: :digest,
                                    mgf1_hash: 'SHA512')
  when SignatureScheme::ECDSA_SECP256R1_SHA256
    key.sign('SHA256', content)
  when SignatureScheme::ECDSA_SECP384R1_SHA384
    key.sign('SHA384', content)
  when SignatureScheme::ECDSA_SECP521R1_SHA512
    key.sign('SHA512', content)
  else # TODO: ED25519, ED448
    terminate(:internal_error)
  end
end
do_sign_psk_binder(ch1:, hrr:, ch:, binder_key:, digest:) click to toggle source

@param ch1 [TTTLS13::Message::ClientHello] @param hrr [TTTLS13::Message::ServerHello] @param ch [TTTLS13::Message::ClientHello] @param binder_key [String] @param digest [String] name of digest algorithm

@return [String]

# File lib/tttls1.3/connection.rb, line 291
def do_sign_psk_binder(ch1:, hrr:, ch:, binder_key:, digest:)
  # TODO: ext binder
  hash_len = OpenSSL::Digest.new(digest).digest_length
  tt = Transcript.new
  tt.merge!(
    CH1 => ch1,
    HRR => hrr,
    CH => ch
  )
  # transcript-hash (CH1 + HRR +) truncated-CH
  hash = tt.truncate_hash(digest, CH, hash_len + 3)
  OpenSSL::HMAC.digest(digest, binder_key, hash)
end
do_verified_certificate_verify?(public_key:, signature_scheme:, signature:, context:, hash:) click to toggle source

@param public_key [OpenSSL::PKey::PKey] @param signature_scheme [TTTLS13::SignatureScheme] @param signature [String] @param context [String] @param hash [String]

@raise [TTTLS13::Error::ErrorAlerts]

@return [Boolean] rubocop: disable Metrics/CyclomaticComplexity

# File lib/tttls1.3/connection.rb, line 357
def do_verified_certificate_verify?(public_key:, signature_scheme:,
                                    signature:, context:, hash:)
  content = "\x20" * 64 + context + "\x00" + hash

  # RSA signatures MUST use an RSASSA-PSS algorithm, regardless of whether
  # RSASSA-PKCS1-v1_5 algorithms appear in "signature_algorithms".
  case signature_scheme
  when SignatureScheme::RSA_PKCS1_SHA256,
       SignatureScheme::RSA_PSS_RSAE_SHA256,
       SignatureScheme::RSA_PSS_PSS_SHA256
    public_key.verify_pss('SHA256', signature, content, salt_length: :auto,
                                                        mgf1_hash: 'SHA256')
  when SignatureScheme::RSA_PKCS1_SHA384,
       SignatureScheme::RSA_PSS_RSAE_SHA384,
       SignatureScheme::RSA_PSS_PSS_SHA384
    public_key.verify_pss('SHA384', signature, content, salt_length: :auto,
                                                        mgf1_hash: 'SHA384')
  when SignatureScheme::RSA_PKCS1_SHA512,
       SignatureScheme::RSA_PSS_RSAE_SHA512,
       SignatureScheme::RSA_PSS_PSS_SHA512
    public_key.verify_pss('SHA512', signature, content, salt_length: :auto,
                                                        mgf1_hash: 'SHA512')
  when SignatureScheme::ECDSA_SECP256R1_SHA256
    public_key.verify('SHA256', signature, content)
  when SignatureScheme::ECDSA_SECP384R1_SHA384
    public_key.verify('SHA384', signature, content)
  when SignatureScheme::ECDSA_SECP521R1_SHA512
    public_key.verify('SHA512', signature, content)
  else # TODO: ED25519, ED448
    terminate(:internal_error)
  end
end
gen_cipher(cipher_suite, write_key, write_iv) click to toggle source

@param cipher_suite [TTTLS13::CipherSuite] @param write_key [String] @param write_iv [String]

@return [TTTLS13::Cryptograph::Aead]

# File lib/tttls1.3/connection.rb, line 150
def gen_cipher(cipher_suite, write_key, write_iv)
  seq_num = SequenceNumber.new
  Cryptograph::Aead.new(
    cipher_suite: cipher_suite,
    write_key: write_key,
    write_iv: write_iv,
    sequence_number: seq_num
  )
end
gen_shared_secret(key_exchange, priv_key, group) click to toggle source

@param key_exchange [String] @param priv_key [OpenSSL::PKey::$Object] @param group [TTTLS13::NamedGroup]

@return [String]

# File lib/tttls1.3/connection.rb, line 416
def gen_shared_secret(key_exchange, priv_key, group)
  curve = NamedGroup.curve_name(group)
  terminate(:internal_error) if curve.nil?

  pub_key = OpenSSL::PKey::EC::Point.new(
    OpenSSL::PKey::EC::Group.new(curve),
    OpenSSL::BN.new(key_exchange, 2)
  )

  priv_key.dh_compute_key(pub_key)
end
handle_received_alert(alert) click to toggle source

@param alert [TTTLS13::Message::Alert]

@raise [TTTLS13::Error::ErrorAlerts]

# File lib/tttls1.3/connection.rb, line 454
def handle_received_alert(alert)
  unless alert.description == Message::ALERT_DESCRIPTION[:close_notify] ||
         alert.description == Message::ALERT_DESCRIPTION[:user_canceled]
    raise alert.to_error
  end

  @state = EOF
  nil
end
matching_san?(cert, name) click to toggle source

@param cert [OpenSSL::X509::Certificate] @param name [String]

@return [Boolean]

# File lib/tttls1.3/connection.rb, line 498
def matching_san?(cert, name)
  san = cert.extensions.find { |ex| ex.oid == 'subjectAltName' }
  return false if san.nil?

  ostr = OpenSSL::ASN1.decode(san.to_der).value.last
  matching = OpenSSL::ASN1.decode(ostr.value).map(&:value)
                          .map { |s| s.gsub('.', '\.').gsub('*', '.*') }
                          .any? { |s| name.match(/#{s}/) }

  matching
end
process_new_session_ticket(_nst) click to toggle source

@param _nst [TTTLS13::Message::NewSessionTicket]

@raise [TTTLS13::Error::ErrorAlerts]

# File lib/tttls1.3/connection.rb, line 467
def process_new_session_ticket(_nst)
  terminate(:unexpected_message) if @endpoint == :server
end
receivable_ccs?(transcript) click to toggle source

@param transcript [TTTLS13::Transcript]

@return [Boolean]

# File lib/tttls1.3/connection.rb, line 431
def receivable_ccs?(transcript)
  # Received ccs before the first ClientHello message or after the peer's
  # Finished message, peer MUST abort.
  #
  # Server may receive an unprotected record of type change_cipher_spec
  # between the first and second ClientHello
  finished = (@endpoint == :client ? SF : CF)

  (transcript.include?(CH) || transcript.include?(CH1)) &&
    !transcript.include?(finished)
end
recv_message(receivable_ccs:, cipher:) click to toggle source

@param receivable_ccs [Boolean] @param cipher [TTTLS13::Cryptograph::Aead, Passer]

@raise [TTTLS13::Error::ErrorAlerts

@return [TTTLS13::Message::$Object] rubocop: disable Metrics/CyclomaticComplexity

# File lib/tttls1.3/connection.rb, line 224
def recv_message(receivable_ccs:, cipher:)
  return @message_queue.shift unless @message_queue.empty?

  messages = nil
  loop do
    record = recv_record(cipher)
    case record.type
    when Message::ContentType::HANDSHAKE,
         Message::ContentType::APPLICATION_DATA
      messages = record.messages
      break unless messages.empty?
    when Message::ContentType::CCS
      terminate(:unexpected_message) unless receivable_ccs
      next
    when Message::ContentType::ALERT
      handle_received_alert(record.messages.first)
      return nil
    else
      terminate(:unexpected_message)
    end
  end

  @message_queue += messages[1..]
  message = messages.first
  if message.is_a?(Message::Alert)
    handle_received_alert(message)
    return nil
  end

  message
end
recv_record(cipher) click to toggle source

@param cipher [TTTLS13::Cryptograph::Aead, Passer]

@return [TTTLS13::Message::Record]

# File lib/tttls1.3/connection.rb, line 260
def recv_record(cipher)
  binary = @socket.read(5)
  record_len = Convert.bin2i(binary.slice(3, 2))
  binary += @socket.read(record_len)

  begin
    buffer = @binary_buffer
    record = Message::Record.deserialize(binary, cipher, buffer,
                                         @recv_record_size)
    @binary_buffer = record.surplus_binary
  rescue Error::ErrorAlerts => e
    terminate(e.message.to_sym)
  end

  # Received a protected ccs, peer MUST abort the handshake.
  if record.type == Message::ContentType::APPLICATION_DATA &&
     record.messages.any? { |m| m.is_a?(Message::ChangeCipherSpec) }
    terminate(:unexpected_message)
  end

  logger.debug("receive \n" + record.pretty_inspect)
  record
end
send_alert(symbol) click to toggle source

@param symbol [Symbol] key of ALERT_DESCRIPTION

# File lib/tttls1.3/connection.rb, line 195
def send_alert(symbol)
  message = Message::Alert.new(
    description: Message::ALERT_DESCRIPTION[symbol]
  )
  type = Message::ContentType::ALERT
  type = Message::ContentType::APPLICATION_DATA \
    if @alert_wcipher.is_a?(Cryptograph::Aead)
  alert_record = Message::Record.new(
    type: type,
    legacy_record_version: Message::ProtocolVersion::TLS_1_2,
    messages: [message],
    cipher: @alert_wcipher
  )
  send_record(alert_record)
end
send_application_data(message, cipher) click to toggle source

@param message [TTTLS13::Message::ApplicationData] @param cipher [TTTLS13::Cryptograph::Aead]

# File lib/tttls1.3/connection.rb, line 184
def send_application_data(message, cipher)
  ap_record = Message::Record.new(
    type: Message::ContentType::APPLICATION_DATA,
    legacy_record_version: Message::ProtocolVersion::TLS_1_2,
    messages: [message],
    cipher: cipher
  )
  send_record(ap_record)
end
send_ccs() click to toggle source
# File lib/tttls1.3/connection.rb, line 172
def send_ccs
  ccs_record = Message::Record.new(
    type: Message::ContentType::CCS,
    legacy_record_version: Message::ProtocolVersion::TLS_1_2,
    messages: [Message::ChangeCipherSpec.new],
    cipher: Cryptograph::Passer.new
  )
  send_record(ccs_record)
end
send_handshakes(type, messages, cipher) click to toggle source

@param type [TTTLS13::Message::ContentType] @param messages [Array of TTTLS13::Message::$Object] handshake messages @param cipher [TTTLS13::Cryptograph::Aead, Passer]

# File lib/tttls1.3/connection.rb, line 163
def send_handshakes(type, messages, cipher)
  record = Message::Record.new(
    type: type,
    messages: messages,
    cipher: cipher
  )
  send_record(record)
end
send_record(record) click to toggle source

@param record [TTTLS13::Message::Record]

# File lib/tttls1.3/connection.rb, line 212
def send_record(record)
  logger.debug("send \n" + record.pretty_inspect)
  @socket.write(record.serialize(@send_record_size))
end
sign_finished(digest:, finished_key:, hash:) click to toggle source

@param digest [String] name of digest algorithm @param finished_key [String] @param hash [String]

@return [String]

# File lib/tttls1.3/connection.rb, line 396
def sign_finished(digest:, finished_key:, hash:)
  OpenSSL::HMAC.digest(digest, finished_key, hash)
end
terminate(symbol) click to toggle source

@param symbol [Symbol] key of ALERT_DESCRIPTION

@raise [TTTLS13::Error::ErrorAlerts]

# File lib/tttls1.3/connection.rb, line 446
def terminate(symbol)
  send_alert(symbol)
  raise Error::ErrorAlerts, symbol
end
trusted_certificate?(certificate_list, ca_file = nil, hostname = nil) click to toggle source

@param certificate_list [Array of CertificateEntry] @param ca_file [String] path to ca.crt @param hostname [String]

@return [Boolean]

# File lib/tttls1.3/connection.rb, line 476
def trusted_certificate?(certificate_list, ca_file = nil, hostname = nil)
  chain = certificate_list.map(&:cert_data).map do |c|
    OpenSSL::X509::Certificate.new(c)
  end
  cert = chain.shift

  # not support CN matching, only support SAN matching
  return false if !hostname.nil? && !matching_san?(cert, hostname)

  store = OpenSSL::X509::Store.new
  store.set_default_paths
  store.add_file(ca_file) unless ca_file.nil?
  # TODO: parse authorityInfoAccess::CA Issuers
  ctx = OpenSSL::X509::StoreContext.new(store, cert, chain)
  now = Time.now
  ctx.verify && cert.not_before < now && now < cert.not_after
end
verified_finished?(finished:, digest:, finished_key:, hash:) click to toggle source

@param finished [TTTLS13::Message::Finished] @param digest [String] name of digest algorithm @param finished_key [String] @param hash [String]

@return [Boolean]

# File lib/tttls1.3/connection.rb, line 406
def verified_finished?(finished:, digest:, finished_key:, hash:)
  sign_finished(digest: digest, finished_key: finished_key, hash: hash) \
  == finished.verify_data
end