webauthn 2.1.0 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +113 -13
  3. data/.travis.yml +21 -18
  4. data/Appraisals +4 -0
  5. data/CHANGELOG.md +41 -0
  6. data/CONTRIBUTING.md +0 -5
  7. data/README.md +70 -8
  8. data/SECURITY.md +6 -4
  9. data/gemfiles/openssl_2_2.gemfile +7 -0
  10. data/lib/cose/rsapkcs1_algorithm.rb +50 -0
  11. data/lib/webauthn/attestation_object.rb +43 -0
  12. data/lib/webauthn/attestation_statement.rb +20 -20
  13. data/lib/webauthn/attestation_statement/android_key.rb +28 -30
  14. data/lib/webauthn/attestation_statement/android_safetynet.rb +27 -7
  15. data/lib/webauthn/attestation_statement/base.rb +108 -10
  16. data/lib/webauthn/attestation_statement/fido_u2f.rb +8 -6
  17. data/lib/webauthn/attestation_statement/none.rb +7 -1
  18. data/lib/webauthn/attestation_statement/packed.rb +13 -41
  19. data/lib/webauthn/attestation_statement/tpm.rb +38 -75
  20. data/lib/webauthn/authenticator_assertion_response.rb +3 -7
  21. data/lib/webauthn/authenticator_attestation_response.rb +19 -84
  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 +3 -0
  25. data/lib/webauthn/credential_creation_options.rb +2 -0
  26. data/lib/webauthn/credential_request_options.rb +2 -0
  27. data/lib/webauthn/fake_authenticator.rb +7 -3
  28. data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
  29. data/lib/webauthn/fake_authenticator/authenticator_data.rb +2 -4
  30. data/lib/webauthn/fake_client.rb +19 -5
  31. data/lib/webauthn/public_key.rb +21 -2
  32. data/lib/webauthn/public_key_credential.rb +13 -3
  33. data/lib/webauthn/u2f_migrator.rb +5 -4
  34. data/lib/webauthn/version.rb +1 -1
  35. data/script/ci/install-openssl +7 -0
  36. data/script/ci/install-ruby +13 -0
  37. data/webauthn.gemspec +13 -9
  38. metadata +54 -41
  39. data/lib/android_safetynet/attestation_response.rb +0 -116
  40. data/lib/cose/rsassa_algorithm.rb +0 -10
  41. data/lib/tpm/constants.rb +0 -44
  42. data/lib/tpm/s_attest.rb +0 -26
  43. data/lib/tpm/s_attest/s_certify_info.rb +0 -14
  44. data/lib/tpm/sized_buffer.rb +0 -13
  45. data/lib/tpm/t_public.rb +0 -32
  46. data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
  47. data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
  48. data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
  49. data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
  50. data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
  51. data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
  52. data/lib/webauthn/signature_verifier.rb +0 -77
data/SECURITY.md CHANGED
@@ -4,11 +4,13 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------- | ------------------ |
7
- | 2.1.z | :white_check_mark: |
8
- | 2.0.z | :white_check_mark: |
7
+ | 2.4.z | :white_check_mark: |
8
+ | 2.3.z | :white_check_mark: |
9
+ | 2.2.z | :white_check_mark: |
10
+ | 2.1.z | :x: |
11
+ | 2.0.z | :x: |
9
12
  | 1.18.z | :white_check_mark: |
10
- | 1.17.z | :white_check_mark: |
11
- | < 1.17 | :x: |
13
+ | < 1.18 | :x: |
12
14
 
13
15
  ## Reporting a Vulnerability
14
16
 
@@ -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,50 @@
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
+
44
+ # Patch openssl-signature_algorithm gem to support discouraged/deprecated RSA-PKCS#1 with SHA-1
45
+ # (RS1 in JOSE/COSE terminology) algorithm needed for WebAuthn.
46
+ OpenSSL::SignatureAlgorithm::RSAPKCS1.const_set(
47
+ :ACCEPTED_HASH_FUNCTIONS,
48
+ OpenSSL::SignatureAlgorithm::RSAPKCS1::ACCEPTED_HASH_FUNCTIONS + ["SHA1"]
49
+ )
50
+ 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_trust_path]
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
 
@@ -12,11 +12,12 @@ module WebAuthn
12
12
  valid_response?(authenticator_data, client_data_hash) &&
13
13
  valid_version? &&
14
14
  cts_profile_match? &&
15
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_trust_path]
15
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
16
+ [attestation_type, attestation_trust_path]
16
17
  end
17
18
 
18
19
  def attestation_certificate
19
- attestation_response.leaf_certificate
20
+ attestation_trust_path.first
20
21
  end
21
22
 
22
23
  private
@@ -25,8 +26,9 @@ module WebAuthn
25
26
  nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
26
27
 
27
28
  begin
28
- attestation_response.verify(nonce, trustworthiness: false)
29
- rescue ::AndroidSafetynet::AttestationResponse::VerificationError
29
+ attestation_response
30
+ .verify(nonce, trusted_certificates: root_certificates(aaguid: authenticator_data.aaguid), time: time)
31
+ rescue SafetyNetAttestation::Error
30
32
  false
31
33
  end
32
34
  end
@@ -40,12 +42,30 @@ module WebAuthn
40
42
  attestation_response.cts_profile_match?
41
43
  end
42
44
 
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
43
55
  def attestation_trust_path
44
- attestation_response.certificate_chain
56
+ attestation_response.certificate_chain[0..-2]
45
57
  end
46
58
 
47
59
  def attestation_response
48
- @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
49
69
  end
50
70
  end
51
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)
@@ -26,6 +34,10 @@ module WebAuthn
26
34
  raise NotImplementedError
27
35
  end
28
36
 
37
+ def format
38
+ WebAuthn::AttestationStatement::FORMAT_TO_CLASS.key(self.class)
39
+ end
40
+
29
41
  def attestation_certificate
30
42
  certificates&.first
31
43
  end
@@ -36,6 +48,10 @@ module WebAuthn
36
48
  end
37
49
  end
38
50
 
51
+ def attestation_certificate_key_id
52
+ raw_subject_key_identifier&.unpack("H*")&.[](0)
53
+ end
54
+
39
55
  private
40
56
 
41
57
  attr_reader :statement
@@ -53,9 +69,10 @@ module WebAuthn
53
69
  end
54
70
 
55
71
  def certificates
56
- @certificates ||= raw_certificates&.map do |raw_certificate|
57
- OpenSSL::X509::Certificate.new(raw_certificate)
58
- end
72
+ @certificates ||=
73
+ raw_certificates&.map do |raw_certificate|
74
+ OpenSSL::X509::Certificate.new(raw_certificate)
75
+ end
59
76
  end
60
77
 
61
78
  def algorithm
@@ -66,10 +83,6 @@ module WebAuthn
66
83
  statement["x5c"]
67
84
  end
68
85
 
69
- def raw_ecdaa_key_id
70
- statement["ecdaaKeyId"]
71
- end
72
-
73
86
  def signature
74
87
  statement["sig"]
75
88
  end
@@ -79,6 +92,91 @@ module WebAuthn
79
92
  certificates
80
93
  end
81
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
82
180
  end
83
181
  end
84
182
  end