webauthn-slim 3.4.3

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/actions/install-openssl/action.yml +35 -0
  4. data/.github/actions/install-ruby/action.yml +76 -0
  5. data/.github/dependabot.yml +6 -0
  6. data/.github/workflows/build.yml +49 -0
  7. data/.github/workflows/git.yml +21 -0
  8. data/.github/workflows/release.yml +28 -0
  9. data/.gitignore +16 -0
  10. data/.rspec +2 -0
  11. data/.rubocop.yml +306 -0
  12. data/CHANGELOG.md +462 -0
  13. data/CONTRIBUTING.md +39 -0
  14. data/Gemfile +8 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +479 -0
  17. data/Rakefile +10 -0
  18. data/SECURITY.md +21 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/docs/advanced_configuration.md +173 -0
  22. data/docs/u2f_migration.md +90 -0
  23. data/lib/webauthn/attestation_object.rb +47 -0
  24. data/lib/webauthn/attestation_statement/apple.rb +65 -0
  25. data/lib/webauthn/attestation_statement/base.rb +175 -0
  26. data/lib/webauthn/attestation_statement/fido_u2f/public_key.rb +44 -0
  27. data/lib/webauthn/attestation_statement/fido_u2f.rb +71 -0
  28. data/lib/webauthn/attestation_statement/none.rb +23 -0
  29. data/lib/webauthn/attestation_statement/packed.rb +72 -0
  30. data/lib/webauthn/attestation_statement.rb +35 -0
  31. data/lib/webauthn/authenticator_assertion_response.rb +87 -0
  32. data/lib/webauthn/authenticator_attestation_response.rb +84 -0
  33. data/lib/webauthn/authenticator_data/attested_credential_data.rb +94 -0
  34. data/lib/webauthn/authenticator_data.rb +130 -0
  35. data/lib/webauthn/authenticator_response.rb +137 -0
  36. data/lib/webauthn/cbor.rb +189 -0
  37. data/lib/webauthn/client_data.rb +67 -0
  38. data/lib/webauthn/configuration.rb +68 -0
  39. data/lib/webauthn/cose.rb +237 -0
  40. data/lib/webauthn/credential.rb +26 -0
  41. data/lib/webauthn/credential_creation_options.rb +97 -0
  42. data/lib/webauthn/credential_entity.rb +11 -0
  43. data/lib/webauthn/credential_options.rb +17 -0
  44. data/lib/webauthn/credential_request_options.rb +44 -0
  45. data/lib/webauthn/credential_rp_entity.rb +8 -0
  46. data/lib/webauthn/credential_user_entity.rb +16 -0
  47. data/lib/webauthn/encoder.rb +18 -0
  48. data/lib/webauthn/encoders.rb +62 -0
  49. data/lib/webauthn/error.rb +5 -0
  50. data/lib/webauthn/fake_authenticator/attestation_object.rb +90 -0
  51. data/lib/webauthn/fake_authenticator/authenticator_data.rb +156 -0
  52. data/lib/webauthn/fake_authenticator.rb +134 -0
  53. data/lib/webauthn/fake_client.rb +179 -0
  54. data/lib/webauthn/json_serializer.rb +45 -0
  55. data/lib/webauthn/public_key.rb +65 -0
  56. data/lib/webauthn/public_key_credential/creation_options.rb +93 -0
  57. data/lib/webauthn/public_key_credential/entity.rb +21 -0
  58. data/lib/webauthn/public_key_credential/options.rb +51 -0
  59. data/lib/webauthn/public_key_credential/request_options.rb +46 -0
  60. data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
  61. data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
  62. data/lib/webauthn/public_key_credential.rb +94 -0
  63. data/lib/webauthn/public_key_credential_with_assertion.rb +49 -0
  64. data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
  65. data/lib/webauthn/relying_party.rb +150 -0
  66. data/lib/webauthn/u2f_migrator.rb +76 -0
  67. data/lib/webauthn/version.rb +5 -0
  68. data/lib/webauthn.rb +19 -0
  69. data/webauthn.gemspec +43 -0
  70. metadata +218 -0
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+ require "webauthn/authenticator_data"
6
+ require "webauthn/encoder"
7
+ require "webauthn/fake_authenticator"
8
+
9
+ module WebAuthn
10
+ class FakeClient
11
+ TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
12
+
13
+ attr_reader :origin, :cross_origin, :top_origin, :token_binding, :encoding
14
+
15
+ def initialize(
16
+ origin = fake_origin,
17
+ cross_origin: nil,
18
+ top_origin: nil,
19
+ token_binding: nil,
20
+ authenticator: WebAuthn::FakeAuthenticator.new,
21
+ encoding: WebAuthn.configuration.encoding
22
+ )
23
+ @origin = origin
24
+ @cross_origin = cross_origin
25
+ @top_origin = top_origin
26
+ @token_binding = token_binding
27
+ @authenticator = authenticator
28
+ @encoding = encoding
29
+ end
30
+
31
+ def create(
32
+ challenge: fake_challenge,
33
+ rp_id: nil,
34
+ user_present: true,
35
+ user_verified: false,
36
+ backup_eligibility: false,
37
+ backup_state: false,
38
+ attested_credential_data: true,
39
+ credential_algorithm: nil,
40
+ extensions: nil
41
+ )
42
+ rp_id ||= URI.parse(origin).host
43
+
44
+ client_data_json = data_json_for(:create, encoder.decode(challenge))
45
+ client_data_hash = hashed(client_data_json)
46
+
47
+ attestation_object = authenticator.make_credential(
48
+ rp_id: rp_id,
49
+ client_data_hash: client_data_hash,
50
+ user_present: user_present,
51
+ user_verified: user_verified,
52
+ backup_eligibility: backup_eligibility,
53
+ backup_state: backup_state,
54
+ attested_credential_data: attested_credential_data,
55
+ algorithm: credential_algorithm,
56
+ extensions: extensions
57
+ )
58
+
59
+ id =
60
+ if attested_credential_data
61
+ WebAuthn::AuthenticatorData
62
+ .deserialize(CBOR.decode(attestation_object)["authData"])
63
+ .attested_credential_data
64
+ .id
65
+ else
66
+ "id-for-pk-without-attested-credential-data"
67
+ end
68
+
69
+ {
70
+ "type" => "public-key",
71
+ "id" => internal_encoder.encode(id),
72
+ "rawId" => encoder.encode(id),
73
+ "authenticatorAttachment" => 'platform',
74
+ "clientExtensionResults" => extensions,
75
+ "response" => {
76
+ "attestationObject" => encoder.encode(attestation_object),
77
+ "clientDataJSON" => encoder.encode(client_data_json),
78
+ "transports" => ["internal"],
79
+ }
80
+ }
81
+ end
82
+
83
+ def get(challenge: fake_challenge,
84
+ rp_id: nil,
85
+ user_present: true,
86
+ user_verified: false,
87
+ backup_eligibility: false,
88
+ backup_state: true,
89
+ sign_count: nil,
90
+ extensions: nil,
91
+ user_handle: nil,
92
+ allow_credentials: nil)
93
+ rp_id ||= URI.parse(origin).host
94
+
95
+ client_data_json = data_json_for(:get, encoder.decode(challenge))
96
+ client_data_hash = hashed(client_data_json)
97
+
98
+ if allow_credentials
99
+ allow_credentials = allow_credentials.map { |credential| encoder.decode(credential) }
100
+ end
101
+
102
+ assertion = authenticator.get_assertion(
103
+ rp_id: rp_id,
104
+ client_data_hash: client_data_hash,
105
+ user_present: user_present,
106
+ user_verified: user_verified,
107
+ backup_eligibility: backup_eligibility,
108
+ backup_state: backup_state,
109
+ sign_count: sign_count,
110
+ extensions: extensions,
111
+ allow_credentials: allow_credentials
112
+ )
113
+
114
+ {
115
+ "type" => "public-key",
116
+ "id" => internal_encoder.encode(assertion[:credential_id]),
117
+ "rawId" => encoder.encode(assertion[:credential_id]),
118
+ "clientExtensionResults" => extensions,
119
+ "authenticatorAttachment" => 'platform',
120
+ "response" => {
121
+ "clientDataJSON" => encoder.encode(client_data_json),
122
+ "authenticatorData" => encoder.encode(assertion[:authenticator_data]),
123
+ "signature" => encoder.encode(assertion[:signature]),
124
+ "userHandle" => user_handle ? encoder.encode(user_handle) : nil
125
+ }
126
+ }
127
+ end
128
+
129
+ private
130
+
131
+ attr_reader :authenticator
132
+
133
+ def data_json_for(method, challenge)
134
+ data = {
135
+ type: type_for(method),
136
+ challenge: internal_encoder.encode(challenge),
137
+ origin: origin
138
+ }
139
+
140
+ if token_binding
141
+ data[:tokenBinding] = token_binding
142
+ end
143
+
144
+ if cross_origin
145
+ data[:crossOrigin] = cross_origin
146
+ end
147
+
148
+ if top_origin
149
+ data[:topOrigin] = top_origin
150
+ end
151
+
152
+ data.to_json
153
+ end
154
+
155
+ def encoder
156
+ @encoder ||= WebAuthn::Encoder.new(encoding)
157
+ end
158
+
159
+ def internal_encoder
160
+ WebAuthn.standard_encoder
161
+ end
162
+
163
+ def hashed(data)
164
+ OpenSSL::Digest::SHA256.digest(data)
165
+ end
166
+
167
+ def fake_challenge
168
+ encoder.encode(SecureRandom.random_bytes(32))
169
+ end
170
+
171
+ def fake_origin
172
+ "http://localhost#{rand(1000)}.test"
173
+ end
174
+
175
+ def type_for(method)
176
+ TYPES[method]
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebAuthn
4
+ module JSONSerializer
5
+ # Argument wildcard for Ruby on Rails controller automatic object JSON serialization
6
+ def as_json(*)
7
+ deep_camelize_keys(to_hash)
8
+ end
9
+
10
+ private
11
+
12
+ def to_hash
13
+ attributes.each_with_object({}) do |attribute_name, hash|
14
+ value = send(attribute_name)
15
+
16
+ if value.respond_to?(:as_json)
17
+ value = value.as_json
18
+ end
19
+
20
+ if value
21
+ hash[attribute_name] = value
22
+ end
23
+ end
24
+ end
25
+
26
+ def deep_camelize_keys(object)
27
+ case object
28
+ when Hash
29
+ object.each_with_object({}) do |(key, value), result|
30
+ result[camelize(key)] = deep_camelize_keys(value)
31
+ end
32
+ when Array
33
+ object.map { |element| deep_camelize_keys(element) }
34
+ else
35
+ object
36
+ end
37
+ end
38
+
39
+ def camelize(term)
40
+ first_term, *rest = term.to_s.split('_')
41
+
42
+ [first_term, *rest.map(&:capitalize)].join.to_sym
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "webauthn/attestation_statement/fido_u2f/public_key"
5
+
6
+ module WebAuthn
7
+ class PublicKey
8
+ class UnsupportedAlgorithm < Error; end
9
+
10
+ def self.deserialize(public_key)
11
+ cose_key =
12
+ if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
13
+ # Gem version v1.11.0 and lower, used to behave so that Credential#public_key
14
+ # returned an EC P-256 uncompressed point.
15
+ #
16
+ # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
17
+ # and Credential#public_key started returning the unchanged COSE_Key formatted
18
+ # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
19
+ #
20
+ # Given that the credential public key is expected to be stored long-term by the gem
21
+ # user and later be passed as the public_key argument in the
22
+ # AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
23
+ COSE::Key::EC2.new(
24
+ alg: COSE::Algorithm.by_name("ES256").id,
25
+ crv: 1,
26
+ x: public_key[1..32],
27
+ y: public_key[33..-1]
28
+ )
29
+ else
30
+ COSE::Key.deserialize(public_key)
31
+ end
32
+
33
+ new(cose_key: cose_key)
34
+ end
35
+
36
+ attr_reader :cose_key
37
+
38
+ def initialize(cose_key:)
39
+ @cose_key = cose_key
40
+ end
41
+
42
+ def pkey
43
+ @cose_key.to_pkey
44
+ end
45
+
46
+ def alg
47
+ @cose_key.alg
48
+ end
49
+
50
+ def verify(signature, verification_data)
51
+ cose_algorithm.verify(pkey, signature, verification_data)
52
+ rescue COSE::Error
53
+ false
54
+ end
55
+
56
+ private
57
+
58
+ def cose_algorithm
59
+ @cose_algorithm ||= COSE::Algorithm.find(alg) || raise(
60
+ UnsupportedAlgorithm,
61
+ "The public key algorithm #{alg} is not among the available COSE algorithms"
62
+ )
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "webauthn/public_key_credential/options"
5
+ require "webauthn/public_key_credential/rp_entity"
6
+ require "webauthn/public_key_credential/user_entity"
7
+
8
+ module WebAuthn
9
+ class PublicKeyCredential
10
+ class CreationOptions < Options
11
+ attr_accessor(
12
+ :attestation,
13
+ :authenticator_selection,
14
+ :exclude,
15
+ :algs,
16
+ :rp,
17
+ :user
18
+ )
19
+
20
+ def initialize(
21
+ attestation: nil,
22
+ authenticator_selection: nil,
23
+ exclude_credentials: nil,
24
+ exclude: nil,
25
+ pub_key_cred_params: nil,
26
+ algs: nil,
27
+ rp: {},
28
+ user:,
29
+ **keyword_arguments
30
+ )
31
+ super(**keyword_arguments)
32
+
33
+ @attestation = attestation
34
+ @authenticator_selection = authenticator_selection
35
+ @exclude_credentials = exclude_credentials
36
+ @exclude = exclude
37
+ @pub_key_cred_params = pub_key_cred_params
38
+ @algs = algs
39
+
40
+ @rp =
41
+ if rp.is_a?(Hash)
42
+ rp = rp.dup
43
+ rp[:name] ||= relying_party.name
44
+ rp[:id] ||= relying_party.id
45
+
46
+ RPEntity.new(**rp)
47
+ else
48
+ rp
49
+ end
50
+
51
+ @user =
52
+ if user.is_a?(Hash)
53
+ UserEntity.new(**user)
54
+ else
55
+ user
56
+ end
57
+ end
58
+
59
+ def exclude_credentials
60
+ @exclude_credentials || exclude_credentials_from_exclude
61
+ end
62
+
63
+ def pub_key_cred_params
64
+ @pub_key_cred_params || pub_key_cred_params_from_algs
65
+ end
66
+
67
+ private
68
+
69
+ def attributes
70
+ super.concat([:rp, :user, :pub_key_cred_params, :attestation, :authenticator_selection, :exclude_credentials])
71
+ end
72
+
73
+ def exclude_credentials_from_exclude
74
+ if exclude
75
+ as_public_key_descriptors(exclude)
76
+ end
77
+ end
78
+
79
+ def pub_key_cred_params_from_algs
80
+ Array(algs || relying_party.algorithms).map do |alg|
81
+ alg_id =
82
+ if alg.is_a?(String) || alg.is_a?(Symbol)
83
+ COSE::Algorithm.by_name(alg.to_s).id
84
+ else
85
+ alg
86
+ end
87
+
88
+ { type: TYPE_PUBLIC_KEY, alg: alg_id }
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebAuthn
4
+ class PublicKeyCredential
5
+ class Entity
6
+ include JSONSerializer
7
+
8
+ attr_reader :name
9
+
10
+ def initialize(name:)
11
+ @name = name
12
+ end
13
+
14
+ private
15
+
16
+ def attributes
17
+ [:name]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module WebAuthn
6
+ class PublicKeyCredential
7
+ class Options
8
+ include JSONSerializer
9
+
10
+ CHALLENGE_LENGTH = 32
11
+
12
+ attr_reader :timeout, :extensions, :relying_party
13
+
14
+ def initialize(timeout: nil, extensions: nil, relying_party: WebAuthn.configuration.relying_party)
15
+ @relying_party = relying_party
16
+ @timeout = timeout || default_timeout
17
+ @extensions = default_extensions.merge(extensions || {})
18
+ end
19
+
20
+ def challenge
21
+ encoder.encode(raw_challenge)
22
+ end
23
+
24
+ private
25
+
26
+ def attributes
27
+ [:challenge, :timeout, :extensions]
28
+ end
29
+
30
+ def encoder
31
+ relying_party.encoder
32
+ end
33
+
34
+ def raw_challenge
35
+ @raw_challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH)
36
+ end
37
+
38
+ def default_timeout
39
+ relying_party.credential_options_timeout
40
+ end
41
+
42
+ def default_extensions
43
+ {}
44
+ end
45
+
46
+ def as_public_key_descriptors(ids)
47
+ Array(ids).map { |id| { type: TYPE_PUBLIC_KEY, id: id } }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/public_key_credential/options"
4
+
5
+ module WebAuthn
6
+ class PublicKeyCredential
7
+ class RequestOptions < Options
8
+ attr_accessor :rp_id, :allow, :user_verification
9
+
10
+ def initialize(rp_id: nil, allow_credentials: nil, allow: nil, user_verification: nil, **keyword_arguments)
11
+ super(**keyword_arguments)
12
+
13
+ @rp_id = rp_id || relying_party.id
14
+ @allow_credentials = allow_credentials
15
+ @allow = allow
16
+ @user_verification = user_verification
17
+ end
18
+
19
+ def allow_credentials
20
+ @allow_credentials || allow_credentials_from_allow || []
21
+ end
22
+
23
+ private
24
+
25
+ def attributes
26
+ super.concat([:allow_credentials, :rp_id, :user_verification])
27
+ end
28
+
29
+ def default_extensions
30
+ extensions = super || {}
31
+
32
+ if relying_party.legacy_u2f_appid
33
+ extensions.merge!(appid: relying_party.legacy_u2f_appid)
34
+ end
35
+
36
+ extensions
37
+ end
38
+
39
+ def allow_credentials_from_allow
40
+ if allow
41
+ as_public_key_descriptors(allow)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/public_key_credential/entity"
4
+
5
+ module WebAuthn
6
+ class PublicKeyCredential
7
+ class RPEntity < Entity
8
+ attr_reader :id
9
+
10
+ def initialize(id: nil, **keyword_arguments)
11
+ super(**keyword_arguments)
12
+
13
+ @id = id
14
+ end
15
+
16
+ private
17
+
18
+ def attributes
19
+ super.concat([:id])
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/public_key_credential/entity"
4
+
5
+ module WebAuthn
6
+ class PublicKeyCredential
7
+ class UserEntity < Entity
8
+ attr_reader :id, :display_name
9
+
10
+ def initialize(id:, display_name: nil, **keyword_arguments)
11
+ super(**keyword_arguments)
12
+
13
+ @id = id
14
+ @display_name = display_name || name
15
+ end
16
+
17
+ private
18
+
19
+ def attributes
20
+ super.concat([:id, :display_name])
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/encoder"
4
+
5
+ module WebAuthn
6
+ class PublicKeyCredential
7
+ class InvalidChallengeError < Error; end
8
+
9
+ attr_reader :type, :id, :raw_id, :client_extension_outputs, :authenticator_attachment, :response
10
+
11
+ def self.from_client(credential, relying_party: WebAuthn.configuration.relying_party)
12
+ new(
13
+ type: credential["type"],
14
+ id: credential["id"],
15
+ raw_id: relying_party.encoder.decode(credential["rawId"]),
16
+ client_extension_outputs: credential["clientExtensionResults"],
17
+ authenticator_attachment: credential["authenticatorAttachment"],
18
+ response: response_class.from_client(credential["response"], relying_party: relying_party),
19
+ relying_party: relying_party
20
+ )
21
+ end
22
+
23
+ def initialize(
24
+ type:,
25
+ id:,
26
+ raw_id:,
27
+ response:,
28
+ authenticator_attachment: nil,
29
+ client_extension_outputs: {},
30
+ relying_party: WebAuthn.configuration.relying_party
31
+ )
32
+ @type = type
33
+ @id = id
34
+ @raw_id = raw_id
35
+ @client_extension_outputs = client_extension_outputs
36
+ @authenticator_attachment = authenticator_attachment
37
+ @response = response
38
+ @relying_party = relying_party
39
+ end
40
+
41
+ def verify(challenge, *_args)
42
+ unless valid_class?(challenge)
43
+ msg = "challenge must be a String. input challenge class: #{challenge.class}"
44
+
45
+ raise(InvalidChallengeError, msg)
46
+ end
47
+
48
+ valid_type? || raise("invalid type")
49
+ valid_id? || raise("invalid id")
50
+
51
+ true
52
+ end
53
+
54
+ def sign_count
55
+ authenticator_data&.sign_count
56
+ end
57
+
58
+ def authenticator_extension_outputs
59
+ authenticator_data.extension_data if authenticator_data&.extension_data_included?
60
+ end
61
+
62
+ def backup_eligible?
63
+ authenticator_data&.credential_backup_eligible?
64
+ end
65
+
66
+ def backed_up?
67
+ authenticator_data&.credential_backed_up?
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :relying_party
73
+
74
+ def valid_type?
75
+ type == TYPE_PUBLIC_KEY
76
+ end
77
+
78
+ def valid_id?
79
+ raw_id && id && raw_id == WebAuthn.standard_encoder.decode(id)
80
+ end
81
+
82
+ def valid_class?(challenge)
83
+ challenge.is_a?(String)
84
+ end
85
+
86
+ def authenticator_data
87
+ response&.authenticator_data
88
+ end
89
+
90
+ def encoder
91
+ relying_party.encoder
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/authenticator_assertion_response"
4
+ require "webauthn/public_key_credential"
5
+
6
+ module WebAuthn
7
+ class PublicKeyCredentialWithAssertion < PublicKeyCredential
8
+ def self.response_class
9
+ WebAuthn::AuthenticatorAssertionResponse
10
+ end
11
+
12
+ def verify(challenge, public_key:, sign_count:, user_presence: nil, user_verification: nil)
13
+ super
14
+
15
+ response.verify(
16
+ encoder.decode(challenge),
17
+ public_key: encoder.decode(public_key),
18
+ sign_count: sign_count,
19
+ user_presence: user_presence,
20
+ user_verification: user_verification,
21
+ rp_id: appid_extension_output ? appid : nil
22
+ )
23
+
24
+ true
25
+ end
26
+
27
+ def user_handle
28
+ if raw_user_handle
29
+ encoder.encode(raw_user_handle)
30
+ end
31
+ end
32
+
33
+ def raw_user_handle
34
+ response.user_handle
35
+ end
36
+
37
+ private
38
+
39
+ def appid_extension_output
40
+ return if client_extension_outputs.nil?
41
+
42
+ client_extension_outputs['appid']
43
+ end
44
+
45
+ def appid
46
+ URI.parse(relying_party.legacy_u2f_appid || raise("Unspecified legacy U2F AppID")).to_s
47
+ end
48
+ end
49
+ end