Class: EchSpec::Spec::Spec9

Inherits:
Object
  • Object
show all
Defined in:
lib/echspec/spec/9.rb

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.descriptionObject (readonly)

Returns the value of attribute description.



16
17
18
# File 'lib/echspec/spec/9.rb', line 16

def description
  @description
end

.sectionObject (readonly)

Returns the value of attribute section.



16
17
18
# File 'lib/echspec/spec/9.rb', line 16

def section
  @section
end

Class Method Details

.fetch_well_known(hostname) ⇒ EchSpec::Ok | Err

Parameters:

  • hostname (String)

Returns:



108
109
110
111
112
113
114
115
116
# File 'lib/echspec/spec/9.rb', line 108

def fetch_well_known(hostname)
  uri = URI("https://#{hostname}#{WK_ECH_PATH}")
  res = Net::HTTP.get_response(uri)
  return Err.new("GET #{uri} returned #{res.code}.", nil) unless res.is_a?(Net::HTTPSuccess)

  Ok.new(res.body)
rescue StandardError => e
  Err.new(e.message, nil)
end

.parse_origin_svcb(body) ⇒ EchSpec::Ok | Err

{ "regeninterval": 3600, "endpoints": [ { "priority": 1, "params": { "ipv4hint": [ "192.0.2.1", "192.0.2.254" ], "ech": "AD7+DQA6NAAgACCn3zNTeX/WOD...AAA==", "ipv6hint": [ "2001:DB::ec4" ], "alpn": [ "h2", "http/1.1" ] } } ] }

https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech-12#section-5

Parameters:

  • body (String)

Returns:



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/echspec/spec/9.rb', line 138

def parse_origin_svcb(body)
  h = JSON.parse(body)
  echs = h['endpoints']&.filter_map { |e| e.dig('params', 'ech') }
  return Err.new('The origin-svcb well-known resource does NOT have ech SvcParams.', nil) if echs.nil? || echs.empty?

  # if parsing any one of the ech values fails, return Err
  ech_configs = echs.flat_map do |ech|
    octet = Base64.decode64(ech)
    ECHConfig.decode_vectors(octet.slice(2..))
  end

  Ok.new(ech_configs)
rescue StandardError => e
  Err.new(e.message, nil)
end

.parse_pem(pem) ⇒ EchSpec::Ok<Array<ECHConfig>> | Err

Parameters:

  • pem (String)

Returns:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/echspec/spec/9.rb', line 157

def parse_pem(pem)
  s = pem.scan(/-----BEGIN ECHCONFIG-----(.*)-----END ECHCONFIG-----/m)
         .first
         .first
         .gsub("\n", '')
  b = Base64.decode64(s)
  ech_configs = ECHConfig.decode_vectors(b.slice(2..))
  Ok.new(ech_configs)
rescue StandardError
  # https://datatracker.ietf.org/doc/html/rfc9934#section-3
  example = <<~PEM
    -----BEGIN PRIVATE KEY-----
    MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V
    -----END PRIVATE KEY-----
    -----BEGIN ECHCONFIG-----
    AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEA
    AQALZXhhbXBsZS5jb20AAA==
    -----END ECHCONFIG-----
  PEM
  Err.new("Failed to parse ECHConfig PEM file, expected ECHConfig PEM like following: \n\n#{example}", nil)
end

.resolve_ech_configs(hostname) ⇒ EchSpec::Ok<Array<ECHConfig>> | Err

Parameters:

  • hostname (String)

Returns:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/echspec/spec/9.rb', line 65

def resolve_ech_configs(hostname)
  begin
    rr = Resolv::DNS.new.getresource(
      hostname,
      Resolv::DNS::Resource::IN::HTTPS
    )
  rescue Resolv::ResolvError => e
    return Err.new(e.message, nil)
  end

  # https://datatracker.ietf.org/doc/html/rfc9934#section-3
  ech = 5
  return Err.new("HTTPS resource record for #{hostname} does NOT have ech SvcParams.", nil) if rr.params[ech].nil?

  octet = rr.params[ech].value
  Err.new('Failed to parse ECHConfig on HTTPS resource record.', nil) \
    unless octet.length == octet.slice(0, 2).unpack1('n') + 2

  Ok.new(ECHConfig.decode_vectors(octet.slice(2..)))
end

.try_get_ech_config(fpath, hostname, force_compliant) ⇒ EchSpec::Ok<ECHConfig> | Err

Parameters:

  • fpath (String | NilClass)
  • hostname (String)
  • force_compliant (Boolean)

Returns:



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/echspec/spec/9.rb', line 23

def try_get_ech_config(fpath, hostname, force_compliant)
  result = if fpath.nil?
             resolve_ech_configs(hostname)
           else
             parse_pem(File.open(fpath).read)
           end

  ech_configs = case result
                in Ok(obj)
                  obj
                in Err
                  return result
                end

  if force_compliant
    validate_compliant_ech_configs(ech_configs)
  else
    Ok.new(ech_configs.first)
  end
end

.validate_compliant_ech_configs(ech_configs) ⇒ EchSpec::Ok | Err

Parameters:

  • ech_configs (Array<ECHConfig>)

Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/echspec/spec/9.rb', line 47

def validate_compliant_ech_configs(ech_configs)
  ech_config = ech_configs.find do |c|
    kconfig = c.echconfig_contents.key_config
    valid_kem_id = kconfig.kem_id.uint16 == HPKE::DHKEM_X25519_HKDF_SHA256
    valid_cipher_suite = kconfig.cipher_suites.any? do |cs|
      cs.kdf_id.uint16 == HPKE::HKDF_SHA256 && cs.aead_id.uint16 == HPKE::AES_128_GCM
    end

    valid_kem_id && valid_cipher_suite
  end
  return Ok.new(ech_config) unless ech_config.nil?

  Err.new('ECHConfigs does NOT include HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM.', nil)
end

.well_known_origin_svcb(hostname) ⇒ EchSpec::Ok<Array<ECHConfig>> | Err

Parameters:

  • hostname (String)

Returns:



89
90
91
92
93
94
95
96
97
98
99
# File 'lib/echspec/spec/9.rb', line 89

def well_known_origin_svcb(hostname)
  res = fetch_well_known(hostname)
  body = case res
         in Ok(obj)
           obj
         in Err
           return res
         end

  parse_origin_svcb(body)
end