class FidoMetadata::Client

Constants

DEFAULT_HEADERS
FIDO_ROOT_CERTIFICATES

Public Class Methods

new(token) click to toggle source
# File lib/fido_metadata/client.rb, line 26
def initialize(token)
  @token = token
end

Public Instance Methods

download_entry(uri, expected_hash:) click to toggle source
# File lib/fido_metadata/client.rb, line 47
def download_entry(uri, expected_hash:)
  response = get_with_token(uri)
  decoded_hash = Base64.urlsafe_decode64(expected_hash)
  unless OpenSSL.fixed_length_secure_compare(OpenSSL::Digest::SHA256.digest(response), decoded_hash)
    raise(InvalidHashError)
  end

  decoded_body = Base64.urlsafe_decode64(response)
  JSON.parse(decoded_body)
end
download_toc(uri, trusted_certs: FIDO_ROOT_CERTIFICATES) click to toggle source
# File lib/fido_metadata/client.rb, line 30
def download_toc(uri, trusted_certs: FIDO_ROOT_CERTIFICATES)
  response = get_with_token(uri)
  payload, _ = JWT.decode(response, nil, true, algorithms: ["ES256"]) do |headers|
    jwt_certificates = headers["x5c"].map do |encoded|
      OpenSSL::X509::Certificate.new(Base64.strict_decode64(encoded))
    end
    crls = download_crls(jwt_certificates)

    begin
      X5cKeyFinder.from(jwt_certificates, trusted_certs, crls)
    rescue JWT::VerificationError => e
      raise(UnverifiedSigningKeyError, e.message)
    end
  end
  payload
end

Private Instance Methods

download_crls(certificates) click to toggle source
# File lib/fido_metadata/client.rb, line 86
def download_crls(certificates)
  uris = extract_crl_distribution_points(certificates)

  crls = uris.compact.uniq.map do |uri|
    begin
      get(uri)
    rescue Net::ProtocolError
      # TODO: figure out why test endpoint specifies a missing and unused CRL in the cert chain, and see if this
      # rescue can be removed. If the CRL is used, OpenSSL error 3 (unable to get certificate CRL) will raise.
      nil
    end
  end
  crls.compact.map { |crl| OpenSSL::X509::CRL.new(crl) }
end
extract_crl_distribution_points(certificates) click to toggle source
# File lib/fido_metadata/client.rb, line 101
def extract_crl_distribution_points(certificates)
  certificates.map do |certificate|
    extension = certificate.extensions.detect { |ext| ext.oid == "crlDistributionPoints" }
    # TODO: replace this with proper parsing of deeply nested ASN1 structures
    match = extension&.value&.match(/URI:(?<uri>\S*)/)
    URI(match[:uri]) if match
  end
end
get(uri) click to toggle source
# File lib/fido_metadata/client.rb, line 68
def get(uri)
  get = Net::HTTP::Get.new(uri, DEFAULT_HEADERS)
  response = http(uri).request(get)
  response.value
  response.body
end
get_with_token(uri) click to toggle source
# File lib/fido_metadata/client.rb, line 60
def get_with_token(uri)
  if @token && !@token.empty?
    uri.path += "/" unless uri.path.end_with?("/")
    uri.query = "token=#{@token}"
  end
  get(uri)
end
http(uri) click to toggle source
# File lib/fido_metadata/client.rb, line 75
def http(uri)
  @http ||= begin
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.port == 443
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    http.open_timeout = 5
    http.read_timeout = 5
    http
  end
end