Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metadata service support #208

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
/Gemfile.lock
/gemfiles/*.gemfile.lock
.byebug_history

/spec/conformance/metadata.zip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't having any effect because the file is also included in the diff here.

The intention was to ignore right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That must've gone wrong during a rebase, it was not meant to be included :( It's governed by the conformance tools terms of service and not open source.

110 changes: 110 additions & 0 deletions docs/attestation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Attestation

The gem supports verifying the authenticator attestation, an advanced feature that allows your application to check the provenance, features and security status of an authenticator.
See [FIDO TechNotes: The Truth about Attestation](https://fidoalliance.org/fido-technotes-the-truth-about-attestation/) for a generic overview.

Consumer use cases generally do not require attestation, like you would not care for which type of phone your user receives SMS codes with/generates TOTP codes on.
If you decide you may need attestation, continue on reading to understand the work and tradeoffs involved.

## FIDO Metadata Service (MDS)

You will need a token to use the FIDO Metadata Service, which can be obtained after registration at https://mds2.fidoalliance.org/tokens/

You can configure the gem as such:

```ruby
WebAuthn.configure do |config|
config.verify_attestation_statement = true
config.metadata_token = "your token"
end
```

## Integrating the cache backend

The gem uses a caching abstraction you need to implement in your application for performance and resiliency. The interface is inspired by Rails' `ActiveSupport::Cache`.
This allows you to use any datastore you like, and using different strategies to keep data fresh such as just-in-time retrieval or a daily job.

The interface you need to implement is as follows:

```ruby
class FidoMetadataCacheStore
def read(name, _options = nil)
# return `value`
end

def write(name, value, _options = nil)
# store `value` so it can be looked up using `name`
end
end
```

Configure the gem to use it:
```ruby
WebAuthn.configure do |config|
config.cache_backend = FidoMetadataCacheStore.new
end
```

If you want to use the daily job strategy, look at how the gem uses the `WebAuthn::Metadata::Client` class internally.

## Integration in your registration and authentication flows

The gem supports 'direct' attestation, also known as 'batch attestation' in FIDO parleance.

Some notes about the implementation of different attestation formats:
- U2F and packed formats use the FIDO Metadata Service to look up authenticator metadata which includes root certificates. Without metadata verification fails.
- Android Safetynet and Android Keystore formats attempt lookup in the MDS, if no metadata is found bundled Google certificates are used.
- TPM format attempts lookup in the MDS, if no metadata is found bundled Trusted Computing Group certificates from Microsoft are used.

This means that depending on your risk profile, you need to decide how to handle:
- Successful attestation verification with metadata present means you will need to interpret the metadata:
- `metadata_entry` among other things contains an array of `status_reports` which indicate the authenticator security status.
- `metadata_statement` describes detailed characteristics about the authenticator, such as the accuracy of user verification (PIN, biometrics) employed
- Successful attestation verification without metadata present
- Failed attestation verification (`AttestationStatementVerificationError` is raised)

During registration you should store the AAGUID (or attestation key identifier) alongside other authenticator data:

```ruby
begin
attestation_response.verify(expected_challenge)

if (entry = attestation_response.metadata_entry)
# If a MetadataTOCPayloadEntry is found, you'll want to verify if `entry.status_reports.last.status` is acceptable
# A list if possible values can be found at
# https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-service-v2.0-rd-20180702.html#authenticatorstatus-enum

# You can also find the MetadataStatement by invoking `attestation_response.metadata_statement`
# For a description of it's contents see
# https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-statement-v2.0-rd-20180702.html
else
# Decide how to handle successful attestation verification without metadata
end

# For future reference, alongside the credential ID and public key (as described in the README) also store:
# - `attestation_response.aaguid` for CTAP2 devices, or if that is nil
# - `attestation_response.attestation_certificate_key_id` for CTAP1 (U2F) devices
rescue WebAuthn::VerificationError => e
# Handle error
end
```

It is possible that an authenticator was discovered to be compromised after registration in your application.
During authentication or a periodic job you can use the AAGUID (or attestation key identifier) to see if the authenticator is still considered secure.

```ruby
begin
assertion_response.verify(expected_challenge, public_key: credential.public_key, sign_count: credential.sign_count)

if (entry = WebAuthn::Metadata::Store.fetch_entry(aaguid: credential.aaguid))
# Use similar logic as in the registration example to determine acceptable status
# You can also use `WebAuthn::Metadata::Store.fetch_statement` to retrieve the metadata_statement
else
# Decide how to handle registered authenticators without metadata
end

# If authenticator is still acceptable, update the stored credential sign count and sign in the user (as described in the README)
rescue WebAuthn::VerificationError => e
# Handle error
end
```
3 changes: 3 additions & 0 deletions lib/webauthn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
require "webauthn/credential"
require "webauthn/credential_creation_options"
require "webauthn/credential_request_options"
require "webauthn/metadata/client"
require "webauthn/metadata/statement"
require "webauthn/metadata/table_of_contents"
require "webauthn/version"

module WebAuthn
Expand Down
20 changes: 20 additions & 0 deletions lib/webauthn/attestation_statement/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "openssl"
require "webauthn/authenticator_data/attested_credential_data"
require "webauthn/error"
require "webauthn/metadata/store"

module WebAuthn
module AttestationStatement
Expand All @@ -18,6 +19,8 @@ class NotSupportedError < Error; end

AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"

attr_reader :metadata_entry, :metadata_statement

def initialize(statement)
@statement = statement
end
Expand Down Expand Up @@ -67,6 +70,23 @@ def raw_ecdaa_key_id
def signature
statement["sig"]
end

def metadata_store
@metadata_store ||= WebAuthn::Metadata::Store.new
end

def find_metadata(aaguid)
@metadata_entry = metadata_store.fetch_entry(aaguid: aaguid)
@metadata_statement = metadata_store.fetch_statement(aaguid: aaguid)
end

def build_trust_store(root_certificates)
trust_store = OpenSSL::X509::Store.new
root_certificates.each do |certificate|
trust_store.add_cert(certificate)
end
trust_store
end
end
end
end
34 changes: 34 additions & 0 deletions lib/webauthn/attestation_statement/fido_u2f.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,27 @@ def valid?(authenticator_data, client_data_hash)
valid_credential_public_key?(authenticator_data.credential.public_key) &&
valid_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
valid_signature?(authenticator_data, client_data_hash) &&
certificate_chain_trusted? &&
[WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, [attestation_certificate]]
end

def attestation_certificate_key_id
return @attestation_certificate_key_id if defined?(@attestation_certificate_key_id)

@attestation_certificate_key_id = begin
extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
return if extension.nil? || extension.critical?

sequence = OpenSSL::ASN1.decode(extension.to_der)
octet_string = sequence.detect do |value|
value.tag_class == :UNIVERSAL && value.tag == OpenSSL::ASN1::OCTET_STRING
end
return unless octet_string

OpenSSL::ASN1.decode(octet_string.value).value.unpack("H*")[0]
end
end

private

def valid_format?
Expand Down Expand Up @@ -51,6 +69,14 @@ def valid_signature?(authenticator_data, client_data_hash)
.verify(signature, verification_data(authenticator_data, client_data_hash))
end

def certificate_chain_trusted?
find_metadata
return false unless metadata_statement
Copy link
Collaborator Author

@bdewater bdewater Aug 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this fails conformance testing at the moment because metadata.zip doesn't contain entries/statements with the attestationCertificateKeyIdentifiers array containing any strings: fido-alliance/conformance-test-tools-resources#504


trust_store = build_trust_store(metadata_statement.attestation_root_certificates)
trust_store.verify(attestation_certificate, attestation_certificate_chain[1..-1])
end

def verification_data(authenticator_data, client_data_hash)
"\x00" +
authenticator_data.rp_id_hash +
Expand All @@ -62,6 +88,14 @@ def verification_data(authenticator_data, client_data_hash)
def public_key_u2f(cose_key_data)
PublicKey.new(cose_key_data)
end

def find_metadata
key_id = attestation_certificate_key_id
return unless key_id

@metadata_entry = metadata_store.fetch_entry(attestation_certificate_key_id: key_id)
@metadata_statement = metadata_store.fetch_statement(attestation_certificate_key_id: key_id)
end
end
end
end
17 changes: 7 additions & 10 deletions lib/webauthn/attestation_statement/packed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def valid?(authenticator_data, client_data_hash)

valid_format? &&
valid_algorithm?(authenticator_data.credential) &&
valid_certificate_chain? &&
valid_ec_public_keys?(authenticator_data.credential) &&
meet_certificate_requirement? &&
matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
valid_signature?(authenticator_data, client_data_hash) &&
certificate_chain_trusted?(authenticator_data.attested_credential_data.aaguid) &&
attestation_type_and_trust_path
end

Expand All @@ -45,9 +45,13 @@ def check_unsupported_feature
end
end

def valid_certificate_chain?
def certificate_chain_trusted?(aaguid)
if attestation_certificate_chain
attestation_certificate_chain[1..-1].all? { |c| certificate_in_use?(c) }
find_metadata(aaguid)
return false unless metadata_statement

trust_store = build_trust_store(metadata_statement.attestation_root_certificates)
trust_store.verify(attestation_certificate, attestation_certificate_chain[1..-1])
else
true
end
Expand All @@ -65,20 +69,13 @@ def meet_certificate_requirement?
subject = attestation_certificate.subject.to_a

attestation_certificate.version == 2 &&
certificate_in_use?(attestation_certificate) &&
subject.assoc('OU')&.at(1) == "Authenticator Attestation" &&
attestation_certificate.extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == 'CA:FALSE'
else
true
end
end

def certificate_in_use?(certificate)
now = Time.now

certificate.not_before < now && now < certificate.not_after
end

def valid_signature?(authenticator_data, client_data_hash)
signature_verifier = WebAuthn::SignatureVerifier.new(
algorithm,
Expand Down
15 changes: 4 additions & 11 deletions lib/webauthn/authenticator_attestation_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ def aaguid
end
end

def attestation_certificate_key
raw_subject_key_identifier(attestation_statement.attestation_certificate)&.unpack("H*")&.[](0)
def attestation_certificate_key_id
if attestation_statement.respond_to?(:attestation_certificate_key_id)
attestation_statement.attestation_certificate_key_id
end
end

private
Expand All @@ -89,14 +91,5 @@ def valid_attested_credential?
def valid_attestation_statement?
@attestation_type, @attestation_trust_path = attestation_statement.valid?(authenticator_data, client_data.hash)
end

def raw_subject_key_identifier(certificate)
extension = certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
return unless extension

ext_asn1 = OpenSSL::ASN1.decode(extension.to_der)
ext_value = ext_asn1.value.last
OpenSSL::ASN1.decode(ext_value.value).value
end
end
end
2 changes: 2 additions & 0 deletions lib/webauthn/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def self.if_pss_supported(algorithm)
attr_accessor :verify_attestation_statement
attr_accessor :credential_options_timeout
attr_accessor :silent_authentication
attr_accessor :metadata_token
attr_accessor :cache_backend

def initialize
@algorithms = DEFAULT_ALGORITHMS.dup
Expand Down
15 changes: 15 additions & 0 deletions lib/webauthn/metadata/Root.cer
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICQzCCAcigAwIBAgIORqmxkzowRM99NQZJurcwCgYIKoZIzj0EAwMwUzELMAkG
A1UEBhMCVVMxFjAUBgNVBAoTDUZJRE8gQWxsaWFuY2UxHTAbBgNVBAsTFE1ldGFk
YXRhIFRPQyBTaWduaW5nMQ0wCwYDVQQDEwRSb290MB4XDTE1MDYxNzAwMDAwMFoX
DTQ1MDYxNzAwMDAwMFowUzELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUZJRE8gQWxs
aWFuY2UxHTAbBgNVBAsTFE1ldGFkYXRhIFRPQyBTaWduaW5nMQ0wCwYDVQQDEwRS
b290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFEoo+6jdxg6oUuOloqPjK/nVGyY+
AXCFz1i5JR4OPeFJs+my143ai0p34EX4R1Xxm9xGi9n8F+RxLjLNPHtlkB3X4ims
rfIx7QcEImx1cMTgu5zUiwxLX1ookVhIRSoso2MwYTAOBgNVHQ8BAf8EBAMCAQYw
DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU0qUfC6f2YshA1Ni9udeO0VS7vEYw
HwYDVR0jBBgwFoAU0qUfC6f2YshA1Ni9udeO0VS7vEYwCgYIKoZIzj0EAwMDaQAw
ZgIxAKulGbSFkDSZusGjbNkAhAkqTkLWo3GrN5nRBNNk2Q4BlG+AvM5q9wa5WciW
DcMdeQIxAMOEzOFsxX9Bo0h4LOFE5y5H8bdPFYW+l5gy1tQiJv+5NUyM2IBB55XU
YjdBz56jSA==
-----END CERTIFICATE-----
38 changes: 38 additions & 0 deletions lib/webauthn/metadata/attributes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module WebAuthn
module Metadata
module Attributes
def underscore_name(name)
name
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.downcase
.to_sym
end
private :underscore_name

def json_accessor(name, coercer = nil)
underscored_name = underscore_name(name)
attr_accessor underscored_name

if coercer
define_method(:"#{underscored_name}=") do |value|
coerced_value = coercer.coerce(value)
instance_variable_set(:"@#{underscored_name}", coerced_value)
end
end
end

def from_json(hash = {})
instance = new
hash.each do |k, v|
method_name = :"#{underscore_name(k)}="
instance.public_send(method_name, v) if instance.respond_to?(method_name)
end

instance
end
end
end
end
17 changes: 17 additions & 0 deletions lib/webauthn/metadata/biometric_accuracy_descriptor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require "webauthn/metadata/attributes"

module WebAuthn
module Metadata
class BiometricAccuracyDescriptor
extend Attributes

json_accessor("selfAttestedFRR")
json_accessor("selfAttestedFAR")
json_accessor("maxTemplates")
json_accessor("maxRetries")
json_accessor("blockSlowdown")
end
end
end
Loading