webauthn 2.0.0.beta1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +65 -13
  4. data/.travis.yml +22 -18
  5. data/Appraisals +4 -0
  6. data/CHANGELOG.md +72 -25
  7. data/CONTRIBUTING.md +0 -5
  8. data/README.md +172 -15
  9. data/SECURITY.md +4 -4
  10. data/gemfiles/openssl_2_2.gemfile +7 -0
  11. data/lib/cose/rsapkcs1_algorithm.rb +43 -0
  12. data/lib/webauthn/attestation_object.rb +43 -0
  13. data/lib/webauthn/attestation_statement.rb +20 -20
  14. data/lib/webauthn/attestation_statement/android_key.rb +28 -30
  15. data/lib/webauthn/attestation_statement/android_safetynet.rb +30 -20
  16. data/lib/webauthn/attestation_statement/base.rb +124 -14
  17. data/lib/webauthn/attestation_statement/fido_u2f.rb +13 -9
  18. data/lib/webauthn/attestation_statement/packed.rb +14 -42
  19. data/lib/webauthn/attestation_statement/tpm.rb +38 -54
  20. data/lib/webauthn/authenticator_assertion_response.rb +7 -36
  21. data/lib/webauthn/authenticator_attestation_response.rb +24 -46
  22. data/lib/webauthn/authenticator_data.rb +51 -51
  23. data/lib/webauthn/authenticator_data/attested_credential_data.rb +29 -50
  24. data/lib/webauthn/authenticator_response.rb +15 -10
  25. data/lib/webauthn/configuration.rb +23 -0
  26. data/lib/webauthn/credential.rb +4 -4
  27. data/lib/webauthn/credential_creation_options.rb +1 -1
  28. data/lib/webauthn/fake_authenticator.rb +7 -3
  29. data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
  30. data/lib/webauthn/fake_authenticator/authenticator_data.rb +2 -4
  31. data/lib/webauthn/fake_client.rb +17 -4
  32. data/lib/webauthn/public_key.rb +68 -0
  33. data/lib/webauthn/public_key_credential.rb +13 -3
  34. data/lib/webauthn/public_key_credential/creation_options.rb +2 -2
  35. data/lib/webauthn/u2f_migrator.rb +5 -4
  36. data/lib/webauthn/version.rb +1 -1
  37. data/script/ci/install-openssl +7 -0
  38. data/script/ci/install-ruby +13 -0
  39. data/webauthn.gemspec +14 -9
  40. metadata +70 -42
  41. data/lib/android_safetynet/attestation_response.rb +0 -84
  42. data/lib/cose/algorithm.rb +0 -38
  43. data/lib/tpm/constants.rb +0 -22
  44. data/lib/tpm/s_attest.rb +0 -26
  45. data/lib/tpm/s_attest/s_certify_info.rb +0 -14
  46. data/lib/tpm/sized_buffer.rb +0 -13
  47. data/lib/tpm/t_public.rb +0 -32
  48. data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
  49. data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
  50. data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
  51. data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
  52. data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
  53. data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
  54. data/lib/webauthn/signature_verifier.rb +0 -65
@@ -4,11 +4,11 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------- | ------------------ |
7
+ | 2.2.z | :white_check_mark: |
8
+ | 2.1.z | :white_check_mark: |
9
+ | 2.0.z | :white_check_mark: |
7
10
  | 1.18.z | :white_check_mark: |
8
- | 1.17.z | :white_check_mark: |
9
- | 1.16.z | :white_check_mark: |
10
- | 1.15.z | :white_check_mark: |
11
- | < 1.15 | :x: |
11
+ | < 1.18 | :x: |
12
12
 
13
13
  ## Reporting a Vulnerability
14
14
 
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "openssl", "~> 2.2.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose"
4
+ require "cose/algorithm/signature_algorithm"
5
+ require "cose/error"
6
+ require "cose/key/rsa"
7
+ require "openssl/signature_algorithm/rsapkcs1"
8
+
9
+ class RSAPKCS1Algorithm < COSE::Algorithm::SignatureAlgorithm
10
+ attr_reader :hash_function
11
+
12
+ def initialize(*args, hash_function:)
13
+ super(*args)
14
+
15
+ @hash_function = hash_function
16
+ end
17
+
18
+ private
19
+
20
+ def signature_algorithm_class
21
+ OpenSSL::SignatureAlgorithm::RSAPKCS1
22
+ end
23
+
24
+ def valid_key?(key)
25
+ to_cose_key(key).is_a?(COSE::Key::RSA)
26
+ end
27
+
28
+ def to_pkey(key)
29
+ case key
30
+ when COSE::Key::RSA
31
+ key.to_pkey
32
+ when OpenSSL::PKey::RSA
33
+ key
34
+ else
35
+ raise(COSE::Error, "Incompatible key for algorithm")
36
+ end
37
+ end
38
+ end
39
+
40
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-257, "RS256", hash_function: "SHA256"))
41
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-258, "RS384", hash_function: "SHA384"))
42
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-259, "RS512", hash_function: "SHA512"))
43
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-65535, "RS1", hash_function: "SHA1"))
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cbor"
4
+ require "forwardable"
5
+ require "openssl"
6
+ require "webauthn/attestation_statement"
7
+ require "webauthn/authenticator_data"
8
+
9
+ module WebAuthn
10
+ class AttestationObject
11
+ extend Forwardable
12
+
13
+ def self.deserialize(attestation_object)
14
+ from_map(CBOR.decode(attestation_object))
15
+ end
16
+
17
+ def self.from_map(map)
18
+ new(
19
+ authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]),
20
+ attestation_statement: WebAuthn::AttestationStatement.from(map["fmt"], map["attStmt"])
21
+ )
22
+ end
23
+
24
+ attr_reader :authenticator_data, :attestation_statement
25
+
26
+ def initialize(authenticator_data:, attestation_statement:)
27
+ @authenticator_data = authenticator_data
28
+ @attestation_statement = attestation_statement
29
+ end
30
+
31
+ def valid_attested_credential?
32
+ authenticator_data.attested_credential_data_included? &&
33
+ authenticator_data.attested_credential_data.valid?
34
+ end
35
+
36
+ def valid_attestation_statement?(client_data_hash)
37
+ attestation_statement.valid?(authenticator_data, client_data_hash)
38
+ end
39
+
40
+ def_delegators :authenticator_data, :credential, :aaguid
41
+ def_delegators :attestation_statement, :attestation_certificate_key_id
42
+ end
43
+ end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "webauthn/attestation_statement/android_key"
4
+ require "webauthn/attestation_statement/android_safetynet"
5
+ require "webauthn/attestation_statement/fido_u2f"
6
+ require "webauthn/attestation_statement/none"
7
+ require "webauthn/attestation_statement/packed"
8
+ require "webauthn/attestation_statement/tpm"
3
9
  require "webauthn/error"
4
10
 
5
11
  module WebAuthn
@@ -13,28 +19,22 @@ module WebAuthn
13
19
  ATTESTATION_FORMAT_ANDROID_KEY = "android-key"
14
20
  ATTESTATION_FORMAT_TPM = "tpm"
15
21
 
22
+ FORMAT_TO_CLASS = {
23
+ ATTESTATION_FORMAT_NONE => WebAuthn::AttestationStatement::None,
24
+ ATTESTATION_FORMAT_FIDO_U2F => WebAuthn::AttestationStatement::FidoU2f,
25
+ ATTESTATION_FORMAT_PACKED => WebAuthn::AttestationStatement::Packed,
26
+ ATTESTATION_FORMAT_ANDROID_SAFETYNET => WebAuthn::AttestationStatement::AndroidSafetynet,
27
+ ATTESTATION_FORMAT_ANDROID_KEY => WebAuthn::AttestationStatement::AndroidKey,
28
+ ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM
29
+ }.freeze
30
+
16
31
  def self.from(format, statement)
17
- case format
18
- when ATTESTATION_FORMAT_NONE
19
- require "webauthn/attestation_statement/none"
20
- WebAuthn::AttestationStatement::None.new(statement)
21
- when ATTESTATION_FORMAT_FIDO_U2F
22
- require "webauthn/attestation_statement/fido_u2f"
23
- WebAuthn::AttestationStatement::FidoU2f.new(statement)
24
- when ATTESTATION_FORMAT_PACKED
25
- require "webauthn/attestation_statement/packed"
26
- WebAuthn::AttestationStatement::Packed.new(statement)
27
- when ATTESTATION_FORMAT_ANDROID_SAFETYNET
28
- require "webauthn/attestation_statement/android_safetynet"
29
- WebAuthn::AttestationStatement::AndroidSafetynet.new(statement)
30
- when ATTESTATION_FORMAT_ANDROID_KEY
31
- require "webauthn/attestation_statement/android_key"
32
- WebAuthn::AttestationStatement::AndroidKey.new(statement)
33
- when ATTESTATION_FORMAT_TPM
34
- require "webauthn/attestation_statement/tpm"
35
- WebAuthn::AttestationStatement::TPM.new(statement)
32
+ klass = FORMAT_TO_CLASS[format]
33
+
34
+ if klass
35
+ klass.new(statement)
36
36
  else
37
- raise FormatNotSupportedError, "Unsupported attestation format '#{format}'"
37
+ raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
38
38
  end
39
39
  end
40
40
  end
@@ -1,73 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "android_key_attestation"
3
4
  require "openssl"
4
- require "webauthn/attestation_statement/android_key/key_description"
5
5
  require "webauthn/attestation_statement/base"
6
- require "webauthn/security_utils"
7
- require "webauthn/signature_verifier"
8
6
 
9
7
  module WebAuthn
10
8
  module AttestationStatement
11
9
  class AndroidKey < Base
12
- EXTENSION_DATA_OID = "1.3.6.1.4.1.11129.2.1.17"
13
-
14
- # https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/keymaster_defs.h
15
- KM_ORIGIN_GENERATED = 0
16
- KM_PURPOSE_SIGN = 2
17
-
18
10
  def valid?(authenticator_data, client_data_hash)
19
11
  valid_signature?(authenticator_data, client_data_hash) &&
20
12
  matching_public_key?(authenticator_data) &&
21
13
  valid_attestation_challenge?(client_data_hash) &&
22
- all_applications_field_not_present? &&
14
+ all_applications_fields_not_set? &&
23
15
  valid_authorization_list_origin? &&
24
16
  valid_authorization_list_purpose? &&
25
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_certificate_chain]
17
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
18
+ [attestation_type, attestation_trust_path]
26
19
  end
27
20
 
28
21
  private
29
22
 
30
- def valid_signature?(authenticator_data, client_data_hash)
31
- WebAuthn::SignatureVerifier
32
- .new(algorithm, attestation_certificate.public_key)
33
- .verify(signature, authenticator_data.data + client_data_hash)
34
- end
35
-
36
23
  def matching_public_key?(authenticator_data)
37
24
  attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
38
25
  end
39
26
 
40
27
  def valid_attestation_challenge?(client_data_hash)
41
- WebAuthn::SecurityUtils.secure_compare(key_description.attestation_challenge, client_data_hash)
28
+ android_key_attestation.verify_challenge(client_data_hash)
29
+ rescue AndroidKeyAttestation::ChallengeMismatchError
30
+ false
42
31
  end
43
32
 
44
- def all_applications_field_not_present?
45
- tee_enforced.all_applications.nil? && software_enforced.all_applications.nil?
33
+ def valid_certificate_chain?(aaguid: nil, **_)
34
+ android_key_attestation.verify_certificate_chain(root_certificates: root_certificates(aaguid: aaguid))
35
+ rescue AndroidKeyAttestation::CertificateVerificationError
36
+ false
37
+ end
38
+
39
+ def all_applications_fields_not_set?
40
+ !tee_enforced.all_applications && !software_enforced.all_applications
46
41
  end
47
42
 
48
43
  def valid_authorization_list_origin?
49
- tee_enforced.origin == KM_ORIGIN_GENERATED || software_enforced.origin == KM_ORIGIN_GENERATED
44
+ tee_enforced.origin == :generated || software_enforced.origin == :generated
50
45
  end
51
46
 
52
47
  def valid_authorization_list_purpose?
53
- tee_enforced.purpose == KM_PURPOSE_SIGN || software_enforced.purpose == KM_PURPOSE_SIGN
48
+ tee_enforced.purpose == [:sign] || software_enforced.purpose == [:sign]
54
49
  end
55
50
 
56
51
  def tee_enforced
57
- key_description.tee_enforced
52
+ android_key_attestation.tee_enforced
58
53
  end
59
54
 
60
55
  def software_enforced
61
- key_description.software_enforced
56
+ android_key_attestation.software_enforced
57
+ end
58
+
59
+ def attestation_type
60
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC
62
61
  end
63
62
 
64
- def key_description
65
- @key_description ||= begin
66
- extension_data = attestation_certificate.extensions.detect { |ext| ext.oid == EXTENSION_DATA_OID }
67
- raw_key_description = OpenSSL::ASN1.decode(extension_data).value.last
63
+ def default_root_certificates
64
+ AndroidKeyAttestation::Statement::GOOGLE_ROOT_CERTIFICATES
65
+ end
68
66
 
69
- KeyDescription.new(OpenSSL::ASN1.decode(raw_key_description.value).value)
70
- end
67
+ def android_key_attestation
68
+ @android_key_attestation ||= AndroidKeyAttestation::Statement.new(*certificates)
71
69
  end
72
70
  end
73
71
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "android_safetynet/attestation_response"
3
+ require "safety_net_attestation"
4
4
  require "openssl"
5
5
  require "webauthn/attestation_statement/base"
6
6
 
@@ -8,35 +8,27 @@ module WebAuthn
8
8
  module AttestationStatement
9
9
  # Implements https://www.w3.org/TR/webauthn-1/#sctn-android-safetynet-attestation
10
10
  class AndroidSafetynet < Base
11
- def self.default_trust_store
12
- OpenSSL::X509::Store.new.tap { |trust_store| trust_store.set_default_paths }
13
- end
14
-
15
- def valid?(authenticator_data, client_data_hash, trust_store: self.class.default_trust_store)
16
- trusted_attestation_certificate?(trust_store) &&
17
- valid_response?(authenticator_data, client_data_hash) &&
11
+ def valid?(authenticator_data, client_data_hash)
12
+ valid_response?(authenticator_data, client_data_hash) &&
18
13
  valid_version? &&
19
14
  cts_profile_match? &&
20
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_certificate]
15
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
16
+ [attestation_type, attestation_trust_path]
21
17
  end
22
18
 
23
19
  def attestation_certificate
24
- attestation_response.certificate_chain[0]
20
+ attestation_trust_path.first
25
21
  end
26
22
 
27
23
  private
28
24
 
29
- # FIXME: This should be a responsibility of AndroidSafetynet::AttestationResponse#verify
30
- def trusted_attestation_certificate?(trust_store)
31
- trust_store.verify(attestation_certificate, signing_certificates)
32
- end
33
-
34
25
  def valid_response?(authenticator_data, client_data_hash)
35
26
  nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
36
27
 
37
28
  begin
38
- attestation_response.verify(nonce)
39
- rescue ::AndroidSafetynet::AttestationResponse::VerificationError
29
+ attestation_response
30
+ .verify(nonce, trusted_certificates: root_certificates(aaguid: authenticator_data.aaguid), time: time)
31
+ rescue SafetyNetAttestation::Error
40
32
  false
41
33
  end
42
34
  end
@@ -50,12 +42,30 @@ module WebAuthn
50
42
  attestation_response.cts_profile_match?
51
43
  end
52
44
 
53
- def signing_certificates
54
- attestation_response.certificate_chain[1..-1]
45
+ def valid_certificate_chain?(**_)
46
+ # Already performed as part of #valid_response?
47
+ true
48
+ end
49
+
50
+ def attestation_type
51
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC
52
+ end
53
+
54
+ # SafetyNetAttestation returns full chain including root, WebAuthn expects only the x5c certificates
55
+ def attestation_trust_path
56
+ attestation_response.certificate_chain[0..-2]
55
57
  end
56
58
 
57
59
  def attestation_response
58
- @attestation_response ||= ::AndroidSafetynet::AttestationResponse.new(statement["response"])
60
+ @attestation_response ||= SafetyNetAttestation::Statement.new(statement["response"])
61
+ end
62
+
63
+ def default_root_certificates
64
+ SafetyNetAttestation::Statement::GOOGLE_ROOT_CERTIFICATES
65
+ end
66
+
67
+ def time
68
+ Time.now
59
69
  end
60
70
  end
61
71
  end
@@ -1,21 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cose/algorithm"
4
+ require "cose/error"
5
+ require "cose/rsapkcs1_algorithm"
3
6
  require "openssl"
4
7
  require "webauthn/authenticator_data/attested_credential_data"
5
8
  require "webauthn/error"
6
9
 
7
10
  module WebAuthn
8
11
  module AttestationStatement
12
+ class UnsupportedAlgorithm < Error; end
13
+
9
14
  ATTESTATION_TYPE_NONE = "None"
10
15
  ATTESTATION_TYPE_BASIC = "Basic"
11
16
  ATTESTATION_TYPE_SELF = "Self"
12
17
  ATTESTATION_TYPE_ATTCA = "AttCA"
13
- ATTESTATION_TYPE_ECDAA = "ECDAA"
14
18
  ATTESTATION_TYPE_BASIC_OR_ATTCA = "Basic_or_AttCA"
15
19
 
16
- class Base
17
- class NotSupportedError < Error; end
20
+ ATTESTATION_TYPES_WITH_ROOT = [
21
+ ATTESTATION_TYPE_BASIC,
22
+ ATTESTATION_TYPE_BASIC_OR_ATTCA,
23
+ ATTESTATION_TYPE_ATTCA
24
+ ].freeze
18
25
 
26
+ class Base
19
27
  AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
20
28
 
21
29
  def initialize(statement)
@@ -23,11 +31,25 @@ module WebAuthn
23
31
  end
24
32
 
25
33
  def valid?(_authenticator_data, _client_data_hash)
26
- raise NotImpelementedError
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def format
38
+ WebAuthn::AttestationStatement::FORMAT_TO_CLASS.key(self.class)
27
39
  end
28
40
 
29
41
  def attestation_certificate
30
- attestation_certificate_chain&.first
42
+ certificates&.first
43
+ end
44
+
45
+ def certificate_chain
46
+ if certificates
47
+ certificates[1..-1]
48
+ end
49
+ end
50
+
51
+ def attestation_certificate_key_id
52
+ raw_subject_key_identifier&.unpack("H*")&.[](0)
31
53
  end
32
54
 
33
55
  private
@@ -46,27 +68,115 @@ module WebAuthn
46
68
  end
47
69
  end
48
70
 
49
- def attestation_certificate_chain
50
- @attestation_certificate_chain ||= raw_attestation_certificates&.map do |raw_certificate|
51
- OpenSSL::X509::Certificate.new(raw_certificate)
52
- end
71
+ def certificates
72
+ @certificates ||=
73
+ raw_certificates&.map do |raw_certificate|
74
+ OpenSSL::X509::Certificate.new(raw_certificate)
75
+ end
53
76
  end
54
77
 
55
78
  def algorithm
56
79
  statement["alg"]
57
80
  end
58
81
 
59
- def raw_attestation_certificates
82
+ def raw_certificates
60
83
  statement["x5c"]
61
84
  end
62
85
 
63
- def raw_ecdaa_key_id
64
- statement["ecdaaKeyId"]
65
- end
66
-
67
86
  def signature
68
87
  statement["sig"]
69
88
  end
89
+
90
+ def attestation_trust_path
91
+ if certificates&.any?
92
+ certificates
93
+ end
94
+ end
95
+
96
+ def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
97
+ if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
98
+ configuration.acceptable_attestation_types.include?(attestation_type) &&
99
+ valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id)
100
+ else
101
+ configuration.acceptable_attestation_types.include?(attestation_type)
102
+ end
103
+ end
104
+
105
+ def valid_certificate_chain?(aaguid: nil, attestation_certificate_key_id: nil)
106
+ attestation_root_certificates_store(
107
+ aaguid: aaguid,
108
+ attestation_certificate_key_id: attestation_certificate_key_id
109
+ ).verify(attestation_certificate, attestation_trust_path)
110
+ end
111
+
112
+ def attestation_root_certificates_store(aaguid: nil, attestation_certificate_key_id: nil)
113
+ OpenSSL::X509::Store.new.tap do |store|
114
+ root_certificates(
115
+ aaguid: aaguid,
116
+ attestation_certificate_key_id: attestation_certificate_key_id
117
+ ).each do |cert|
118
+ store.add_cert(cert)
119
+ end
120
+ end
121
+ end
122
+
123
+ def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
124
+ root_certificates =
125
+ configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder|
126
+ if certs.empty?
127
+ finder.find(
128
+ attestation_format: format,
129
+ aaguid: aaguid,
130
+ attestation_certificate_key_id: attestation_certificate_key_id
131
+ ) || []
132
+ else
133
+ certs
134
+ end
135
+ end
136
+
137
+ if root_certificates.empty? && respond_to?(:default_root_certificates, true)
138
+ default_root_certificates
139
+ else
140
+ root_certificates
141
+ end
142
+ end
143
+
144
+ def raw_subject_key_identifier
145
+ extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
146
+ return unless extension
147
+
148
+ ext_asn1 = OpenSSL::ASN1.decode(extension.to_der)
149
+ ext_value = ext_asn1.value.last
150
+ OpenSSL::ASN1.decode(ext_value.value).value
151
+ end
152
+
153
+ def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
154
+ raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
155
+
156
+ cose_algorithm.verify(
157
+ public_key,
158
+ signature,
159
+ verification_data(authenticator_data, client_data_hash)
160
+ )
161
+ rescue COSE::Error
162
+ false
163
+ end
164
+
165
+ def verification_data(authenticator_data, client_data_hash)
166
+ authenticator_data.data + client_data_hash
167
+ end
168
+
169
+ def cose_algorithm
170
+ @cose_algorithm ||=
171
+ COSE::Algorithm.find(algorithm).tap do |alg|
172
+ alg && configuration.algorithms.include?(alg.name) ||
173
+ raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
174
+ end
175
+ end
176
+
177
+ def configuration
178
+ WebAuthn.configuration
179
+ end
70
180
  end
71
181
  end
72
182
  end