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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/attestation_statement/base"
4
+
5
+ module WebAuthn
6
+ module AttestationStatement
7
+ class None < Base
8
+ def valid?(*_args)
9
+ if statement == {} && trustworthy?
10
+ [WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE, nil]
11
+ else
12
+ false
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def attestation_type
19
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "webauthn/attestation_statement/base"
5
+
6
+ module WebAuthn
7
+ # Implements https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation
8
+ module AttestationStatement
9
+ class Packed < Base
10
+ # Follows "Verification procedure"
11
+ def valid?(authenticator_data, client_data_hash)
12
+ valid_format? &&
13
+ valid_algorithm?(authenticator_data.credential) &&
14
+ valid_ec_public_keys?(authenticator_data.credential) &&
15
+ meet_certificate_requirement? &&
16
+ matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
17
+ valid_signature?(authenticator_data, client_data_hash) &&
18
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
19
+ [attestation_type, attestation_trust_path]
20
+ end
21
+
22
+ private
23
+
24
+ def valid_algorithm?(credential)
25
+ !self_attestation? || algorithm == COSE::Key.deserialize(credential.public_key).alg
26
+ end
27
+
28
+ def self_attestation?
29
+ !raw_certificates
30
+ end
31
+
32
+ def valid_format?
33
+ algorithm && signature
34
+ end
35
+
36
+ def valid_ec_public_keys?(credential)
37
+ (certificates&.map(&:public_key) || [credential.public_key_object])
38
+ .select { |pkey| pkey.is_a?(OpenSSL::PKey::EC) }
39
+ .all? { |pkey| pkey.check_key }
40
+ end
41
+
42
+ # Check https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation-cert-requirements
43
+ def meet_certificate_requirement?
44
+ if attestation_certificate
45
+ subject = attestation_certificate.subject.to_a
46
+
47
+ attestation_certificate.version == 2 &&
48
+ subject.assoc('OU')&.at(1) == "Authenticator Attestation" &&
49
+ attestation_certificate.find_extension('basicConstraints')&.value == 'CA:FALSE'
50
+ else
51
+ true
52
+ end
53
+ end
54
+
55
+ def attestation_type
56
+ if attestation_trust_path
57
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA # FIXME: use metadata if available
58
+ else
59
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF
60
+ end
61
+ end
62
+
63
+ def valid_signature?(authenticator_data, client_data_hash)
64
+ super(
65
+ authenticator_data,
66
+ client_data_hash,
67
+ attestation_certificate&.public_key || authenticator_data.credential.public_key_object
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/attestation_statement/apple"
4
+ require "webauthn/attestation_statement/fido_u2f"
5
+ require "webauthn/attestation_statement/none"
6
+ require "webauthn/attestation_statement/packed"
7
+ require "webauthn/error"
8
+
9
+ module WebAuthn
10
+ module AttestationStatement
11
+ class FormatNotSupportedError < Error; end
12
+
13
+ ATTESTATION_FORMAT_NONE = "none"
14
+ ATTESTATION_FORMAT_FIDO_U2F = "fido-u2f"
15
+ ATTESTATION_FORMAT_PACKED = 'packed'
16
+ ATTESTATION_FORMAT_APPLE = "apple"
17
+
18
+ FORMAT_TO_CLASS = {
19
+ ATTESTATION_FORMAT_NONE => WebAuthn::AttestationStatement::None,
20
+ ATTESTATION_FORMAT_FIDO_U2F => WebAuthn::AttestationStatement::FidoU2f,
21
+ ATTESTATION_FORMAT_PACKED => WebAuthn::AttestationStatement::Packed,
22
+ ATTESTATION_FORMAT_APPLE => WebAuthn::AttestationStatement::Apple
23
+ }.freeze
24
+
25
+ def self.from(format, statement, relying_party: WebAuthn.configuration.relying_party)
26
+ klass = FORMAT_TO_CLASS[format]
27
+
28
+ if klass
29
+ klass.new(statement, relying_party)
30
+ else
31
+ raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/authenticator_data"
4
+ require "webauthn/authenticator_response"
5
+ require "webauthn/encoder"
6
+ require "webauthn/public_key"
7
+
8
+ module WebAuthn
9
+ class SignatureVerificationError < VerificationError; end
10
+ class SignCountVerificationError < VerificationError; end
11
+
12
+ class AuthenticatorAssertionResponse < AuthenticatorResponse
13
+ def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
14
+ encoder = relying_party.encoder
15
+
16
+ user_handle =
17
+ if response["userHandle"]
18
+ encoder.decode(response["userHandle"])
19
+ end
20
+
21
+ new(
22
+ authenticator_data: encoder.decode(response["authenticatorData"]),
23
+ client_data_json: encoder.decode(response["clientDataJSON"]),
24
+ signature: encoder.decode(response["signature"]),
25
+ user_handle: user_handle,
26
+ relying_party: relying_party
27
+ )
28
+ end
29
+
30
+ attr_reader :user_handle
31
+
32
+ def initialize(authenticator_data:, signature:, user_handle: nil, **options)
33
+ super(**options)
34
+
35
+ @authenticator_data_bytes = authenticator_data
36
+ @signature = signature
37
+ @user_handle = user_handle
38
+ end
39
+
40
+ def verify(
41
+ expected_challenge,
42
+ expected_origin = nil,
43
+ public_key:,
44
+ sign_count:,
45
+ user_presence: nil,
46
+ user_verification: nil,
47
+ rp_id: nil
48
+ )
49
+ super(
50
+ expected_challenge,
51
+ expected_origin,
52
+ user_presence: user_presence,
53
+ user_verification: user_verification,
54
+ rp_id: rp_id
55
+ )
56
+ verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key))
57
+ verify_item(:sign_count, sign_count)
58
+
59
+ true
60
+ end
61
+
62
+ def authenticator_data
63
+ @authenticator_data ||= WebAuthn::AuthenticatorData.deserialize(authenticator_data_bytes)
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :authenticator_data_bytes, :signature
69
+
70
+ def valid_signature?(webauthn_public_key)
71
+ webauthn_public_key.verify(signature, authenticator_data_bytes + client_data.hash)
72
+ end
73
+
74
+ def valid_sign_count?(stored_sign_count)
75
+ normalized_sign_count = stored_sign_count || 0
76
+ if authenticator_data.sign_count.nonzero? || normalized_sign_count.nonzero?
77
+ authenticator_data.sign_count > normalized_sign_count
78
+ else
79
+ true
80
+ end
81
+ end
82
+
83
+ def type
84
+ WebAuthn::TYPES[:get]
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cbor"
4
+ require "forwardable"
5
+ require "uri"
6
+ require "openssl"
7
+
8
+ require "webauthn/attestation_object"
9
+ require "webauthn/authenticator_response"
10
+ require "webauthn/client_data"
11
+ require "webauthn/encoder"
12
+
13
+ module WebAuthn
14
+ class AttestationStatementVerificationError < VerificationError; end
15
+ class AttestedCredentialVerificationError < VerificationError; end
16
+
17
+ class AuthenticatorAttestationResponse < AuthenticatorResponse
18
+ extend Forwardable
19
+
20
+ def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
21
+ encoder = relying_party.encoder
22
+
23
+ new(
24
+ attestation_object: encoder.decode(response["attestationObject"]),
25
+ transports: response["transports"],
26
+ client_data_json: encoder.decode(response["clientDataJSON"]),
27
+ relying_party: relying_party
28
+ )
29
+ end
30
+
31
+ attr_reader :attestation_type, :attestation_trust_path, :transports
32
+
33
+ def initialize(attestation_object:, transports: [], **options)
34
+ super(**options)
35
+
36
+ @attestation_object_bytes = attestation_object
37
+ @transports = transports
38
+ @relying_party = relying_party
39
+ end
40
+
41
+ def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_verification: nil, rp_id: nil)
42
+ super
43
+
44
+ verify_item(:attested_credential)
45
+ if relying_party.verify_attestation_statement
46
+ verify_item(:attestation_statement)
47
+ end
48
+
49
+ true
50
+ end
51
+
52
+ def attestation_object
53
+ @attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes, relying_party)
54
+ end
55
+
56
+ def_delegators(
57
+ :attestation_object,
58
+ :aaguid,
59
+ :attestation_statement,
60
+ :attestation_certificate_key_id,
61
+ :authenticator_data,
62
+ :credential
63
+ )
64
+
65
+ alias_method :attestation_certificate_key, :attestation_certificate_key_id
66
+
67
+ private
68
+
69
+ attr_reader :attestation_object_bytes, :relying_party
70
+
71
+ def type
72
+ WebAuthn::TYPES[:create]
73
+ end
74
+
75
+ def valid_attested_credential?
76
+ attestation_object.valid_attested_credential? &&
77
+ relying_party.algorithms.include?(authenticator_data.credential.algorithm)
78
+ end
79
+
80
+ def valid_attestation_statement?
81
+ @attestation_type, @attestation_trust_path = attestation_object.valid_attestation_statement?(client_data.hash)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cbor"
4
+ require "webauthn/cose"
5
+ require "webauthn/error"
6
+
7
+ module WebAuthn
8
+ class AttestedCredentialDataFormatError < WebAuthn::Error; end
9
+
10
+ class AuthenticatorData
11
+ class AttestedCredentialData
12
+ AAGUID_LENGTH = 16
13
+ ZEROED_AAGUID = 0.chr * AAGUID_LENGTH
14
+
15
+ ID_LENGTH_LENGTH = 2
16
+
17
+ attr_reader :raw_aaguid, :id_length, :id, :trailing_bytes
18
+
19
+ Credential =
20
+ Struct.new(:id, :public_key, :algorithm, keyword_init: true) do
21
+ def public_key_object
22
+ COSE::Key.deserialize(public_key).to_pkey
23
+ end
24
+ end
25
+
26
+ def self.deserialize(data)
27
+ new(data)
28
+ end
29
+
30
+ def initialize(data)
31
+ header_length = AAGUID_LENGTH + ID_LENGTH_LENGTH
32
+ unless data.is_a?(String) && data.bytesize >= header_length
33
+ raise AttestedCredentialDataFormatError
34
+ end
35
+
36
+ data = data.b
37
+ @raw_aaguid = data.byteslice(0, AAGUID_LENGTH)
38
+ @id_length = data.byteslice(AAGUID_LENGTH, ID_LENGTH_LENGTH).unpack1("n")
39
+ if data.bytesize < header_length + id_length
40
+ raise AttestedCredentialDataFormatError
41
+ end
42
+
43
+ @id = data.byteslice(header_length, id_length)
44
+ @trailing_bytes = data.byteslice(header_length + id_length..-1)
45
+ end
46
+
47
+ def valid?
48
+ valid_credential_public_key?
49
+ end
50
+
51
+ def aaguid
52
+ raw_aaguid.unpack("H8H4H4H4H12").join("-")
53
+ end
54
+
55
+ def credential
56
+ @credential ||=
57
+ if valid?
58
+ Credential.new(id: id, public_key: public_key, algorithm: algorithm)
59
+ end
60
+ end
61
+
62
+ def length
63
+ if valid?
64
+ AAGUID_LENGTH + ID_LENGTH_LENGTH + id_length + public_key_length
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def algorithm
71
+ COSE::Algorithm.find(cose_key.alg)&.name
72
+ end
73
+
74
+ def valid_credential_public_key?
75
+ !!cose_key.alg
76
+ end
77
+
78
+ def cose_key
79
+ @cose_key ||= COSE::Key.deserialize(public_key)
80
+ end
81
+
82
+ def public_key
83
+ trailing_bytes[0..public_key_length - 1]
84
+ end
85
+
86
+ def public_key_length
87
+ @public_key_length ||=
88
+ CBOR.decode_first(trailing_bytes).last
89
+ rescue CBOR::MalformedData
90
+ raise AttestedCredentialDataFormatError
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cbor"
4
+ require "webauthn/authenticator_data/attested_credential_data"
5
+ require "webauthn/error"
6
+
7
+ module WebAuthn
8
+ class AuthenticatorDataFormatError < WebAuthn::Error; end
9
+
10
+ class AuthenticatorData
11
+ RP_ID_HASH_LENGTH = 32
12
+ FLAGS_LENGTH = 1
13
+ SIGN_COUNT_LENGTH = 4
14
+
15
+ attr_reader :data, :rp_id_hash, :flags, :sign_count, :trailing_bytes
16
+
17
+ def self.deserialize(data)
18
+ new(data)
19
+ end
20
+
21
+ def initialize(data)
22
+ unless data.is_a?(String) && data.bytesize >= base_length
23
+ raise AuthenticatorDataFormatError
24
+ end
25
+
26
+ @data = data.b.freeze
27
+ @rp_id_hash = @data.byteslice(0, RP_ID_HASH_LENGTH)
28
+ @flags = @data.getbyte(RP_ID_HASH_LENGTH)
29
+ @sign_count = @data.byteslice(RP_ID_HASH_LENGTH + FLAGS_LENGTH, SIGN_COUNT_LENGTH).unpack1("N")
30
+ @trailing_bytes = @data.byteslice(base_length..-1)
31
+ end
32
+
33
+ def valid?
34
+ (!attested_credential_data_included? || attested_credential_data.valid?) &&
35
+ (!extension_data_included? || extension_data) &&
36
+ valid_length?
37
+ end
38
+
39
+ def user_flagged?
40
+ user_present? || user_verified?
41
+ end
42
+
43
+ def user_present?
44
+ (flags & 0x1) != 0
45
+ end
46
+
47
+ def user_verified?
48
+ (flags & 0x4) != 0
49
+ end
50
+
51
+ def credential_backup_eligible?
52
+ (flags & 0x8) != 0
53
+ end
54
+
55
+ def credential_backed_up?
56
+ (flags & 0x10) != 0
57
+ end
58
+
59
+ def attested_credential_data_included?
60
+ (flags & 0x40) != 0
61
+ end
62
+
63
+ def extension_data_included?
64
+ (flags & 0x80) != 0
65
+ end
66
+
67
+ def credential
68
+ if attested_credential_data_included?
69
+ attested_credential_data.credential
70
+ end
71
+ end
72
+
73
+ def attested_credential_data
74
+ @attested_credential_data ||=
75
+ AttestedCredentialData.deserialize(trailing_bytes)
76
+ rescue AttestedCredentialDataFormatError
77
+ raise AuthenticatorDataFormatError
78
+ end
79
+
80
+ def extension_data
81
+ @extension_data ||= CBOR.decode(raw_extension_data)
82
+ rescue CBOR::MalformedData
83
+ raise AuthenticatorDataFormatError
84
+ end
85
+
86
+ def aaguid
87
+ raw_aaguid = attested_credential_data.raw_aaguid
88
+
89
+ unless raw_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
90
+ attested_credential_data.aaguid
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def valid_length?
97
+ data.bytesize == base_length + attested_credential_data_length + extension_data_length
98
+ end
99
+
100
+ def raw_extension_data
101
+ if extension_data_included?
102
+ if attested_credential_data_included?
103
+ trailing_bytes[attested_credential_data.length..-1]
104
+ else
105
+ trailing_bytes
106
+ end
107
+ end
108
+ end
109
+
110
+ def attested_credential_data_length
111
+ if attested_credential_data_included?
112
+ attested_credential_data.length
113
+ else
114
+ 0
115
+ end
116
+ end
117
+
118
+ def extension_data_length
119
+ if extension_data_included?
120
+ raw_extension_data.length
121
+ else
122
+ 0
123
+ end
124
+ end
125
+
126
+ def base_length
127
+ RP_ID_HASH_LENGTH + FLAGS_LENGTH + SIGN_COUNT_LENGTH
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/authenticator_data"
4
+ require "webauthn/client_data"
5
+ require "webauthn/error"
6
+
7
+ module WebAuthn
8
+ TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
9
+
10
+ class VerificationError < Error; end
11
+
12
+ class AuthenticatorDataVerificationError < VerificationError; end
13
+ class ChallengeVerificationError < VerificationError; end
14
+ class OriginVerificationError < VerificationError; end
15
+ class RpIdVerificationError < VerificationError; end
16
+ class TokenBindingVerificationError < VerificationError; end
17
+ class TopOriginVerificationError < VerificationError; end
18
+ class TypeVerificationError < VerificationError; end
19
+ class UserPresenceVerificationError < VerificationError; end
20
+ class UserVerifiedVerificationError < VerificationError; end
21
+
22
+ class AuthenticatorResponse
23
+ def initialize(client_data_json:, relying_party: WebAuthn.configuration.relying_party)
24
+ @client_data_json = client_data_json
25
+ @relying_party = relying_party
26
+ end
27
+
28
+ def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_verification: nil, rp_id: nil)
29
+ expected_origin ||= relying_party.allowed_origins || raise("Unspecified expected origin")
30
+
31
+ rp_id ||= relying_party.id
32
+
33
+ verify_item(:type)
34
+ verify_item(:token_binding)
35
+ verify_item(:challenge, expected_challenge)
36
+ verify_item(:origin, expected_origin)
37
+ verify_item(:top_origin) if needs_top_origin_verification?
38
+ verify_item(:authenticator_data)
39
+
40
+ verify_item(
41
+ :rp_id,
42
+ rp_id || rp_id_from_origin(expected_origin)
43
+ )
44
+
45
+ # Fallback to RP configuration unless user_presence is passed in explicitely
46
+ if user_presence.nil? && !relying_party.silent_authentication || user_presence
47
+ verify_item(:user_presence)
48
+ end
49
+
50
+ if user_verification
51
+ verify_item(:user_verified)
52
+ end
53
+
54
+ true
55
+ end
56
+
57
+ def valid?(*args, **keyword_arguments)
58
+ verify(*args, **keyword_arguments)
59
+ rescue WebAuthn::VerificationError
60
+ false
61
+ end
62
+
63
+ def client_data
64
+ @client_data ||= WebAuthn::ClientData.new(client_data_json)
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :client_data_json, :relying_party
70
+
71
+ def verify_item(item, *args)
72
+ if send("valid_#{item}?", *args)
73
+ true
74
+ else
75
+ camelized_item = item.to_s.split('_').map { |w| w.capitalize }.join
76
+ error_const_name = "WebAuthn::#{camelized_item}VerificationError"
77
+ raise Object.const_get(error_const_name)
78
+ end
79
+ end
80
+
81
+ def valid_type?
82
+ client_data.type == type
83
+ end
84
+
85
+ def valid_token_binding?
86
+ client_data.valid_token_binding_format?
87
+ end
88
+
89
+ def valid_top_origin?
90
+ return false unless client_data.cross_origin
91
+
92
+ relying_party.allowed_top_origins&.include?(client_data.top_origin)
93
+ end
94
+
95
+ def valid_challenge?(expected_challenge)
96
+ OpenSSL.secure_compare(client_data.challenge, expected_challenge)
97
+ end
98
+
99
+ def valid_origin?(expected_origin)
100
+ return false unless expected_origin
101
+
102
+ expected_origin.include?(client_data.origin)
103
+ end
104
+
105
+ def valid_rp_id?(rp_id)
106
+ return false unless rp_id
107
+
108
+ OpenSSL::Digest::SHA256.digest(rp_id) == authenticator_data.rp_id_hash
109
+ end
110
+
111
+ def valid_authenticator_data?
112
+ authenticator_data.valid?
113
+ rescue WebAuthn::AuthenticatorDataFormatError
114
+ false
115
+ end
116
+
117
+ def valid_user_presence?
118
+ authenticator_data.user_flagged?
119
+ end
120
+
121
+ def valid_user_verified?
122
+ authenticator_data.user_verified?
123
+ end
124
+
125
+ def rp_id_from_origin(expected_origin)
126
+ URI.parse(expected_origin.first).host if expected_origin.size == 1
127
+ end
128
+
129
+ def type
130
+ raise NotImplementedError, "Please define #type method in subclass"
131
+ end
132
+
133
+ def needs_top_origin_verification?
134
+ relying_party.verify_cross_origin && (client_data.cross_origin || client_data.top_origin)
135
+ end
136
+ end
137
+ end