webauthn 2.1.0 → 3.4.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/build.yml +50 -0
- data/.github/workflows/git.yml +21 -0
- data/.rubocop.yml +121 -13
- data/CHANGELOG.md +169 -0
- data/CONTRIBUTING.md +0 -5
- data/README.md +80 -14
- data/SECURITY.md +7 -4
- data/docs/advanced_configuration.md +174 -0
- data/docs/u2f_migration.md +14 -20
- data/lib/cose/rsapkcs1_algorithm.rb +50 -0
- data/lib/webauthn/attestation_object.rb +47 -0
- data/lib/webauthn/attestation_statement/android_key.rb +27 -33
- data/lib/webauthn/attestation_statement/android_safetynet.rb +27 -11
- data/lib/webauthn/attestation_statement/apple.rb +65 -0
- data/lib/webauthn/attestation_statement/base.rb +114 -21
- data/lib/webauthn/attestation_statement/fido_u2f.rb +8 -6
- data/lib/webauthn/attestation_statement/none.rb +7 -1
- data/lib/webauthn/attestation_statement/packed.rb +14 -42
- data/lib/webauthn/attestation_statement/tpm.rb +38 -75
- data/lib/webauthn/attestation_statement.rb +24 -21
- data/lib/webauthn/authenticator_assertion_response.rb +22 -11
- data/lib/webauthn/authenticator_attestation_response.rb +31 -92
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +33 -49
- data/lib/webauthn/authenticator_data.rb +59 -51
- data/lib/webauthn/authenticator_response.rb +24 -11
- data/lib/webauthn/client_data.rb +4 -6
- data/lib/webauthn/configuration.rb +38 -40
- data/lib/webauthn/credential.rb +4 -4
- data/lib/webauthn/credential_creation_options.rb +2 -0
- data/lib/webauthn/credential_request_options.rb +2 -0
- data/lib/webauthn/encoder.rb +13 -4
- data/lib/webauthn/fake_authenticator/attestation_object.rb +25 -4
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +25 -10
- data/lib/webauthn/fake_authenticator.rb +49 -8
- data/lib/webauthn/fake_client.rb +41 -8
- data/lib/webauthn/json_serializer.rb +45 -0
- data/lib/webauthn/public_key.rb +21 -2
- data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
- data/lib/webauthn/public_key_credential/entity.rb +5 -28
- data/lib/webauthn/public_key_credential/options.rb +11 -32
- data/lib/webauthn/public_key_credential/request_options.rb +11 -1
- data/lib/webauthn/public_key_credential.rb +52 -8
- data/lib/webauthn/public_key_credential_with_assertion.rb +16 -2
- data/lib/webauthn/public_key_credential_with_attestation.rb +2 -2
- data/lib/webauthn/relying_party.rb +137 -0
- data/lib/webauthn/u2f_migrator.rb +8 -4
- data/lib/webauthn/version.rb +1 -1
- data/lib/webauthn.rb +1 -0
- data/webauthn.gemspec +15 -12
- metadata +56 -60
- data/.travis.yml +0 -36
- data/Appraisals +0 -17
- data/gemfiles/cose_head.gemfile +0 -7
- data/gemfiles/openssl_2_0.gemfile +0 -7
- data/gemfiles/openssl_2_1.gemfile +0 -7
- data/gemfiles/openssl_head.gemfile +0 -7
- data/lib/android_safetynet/attestation_response.rb +0 -116
- data/lib/cose/rsassa_algorithm.rb +0 -10
- data/lib/tpm/constants.rb +0 -44
- data/lib/tpm/s_attest/s_certify_info.rb +0 -14
- data/lib/tpm/s_attest.rb +0 -26
- data/lib/tpm/sized_buffer.rb +0 -13
- data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
- data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
- data/lib/tpm/t_public.rb +0 -32
- data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
- data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
- data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
- data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
- data/lib/webauthn/security_utils.rb +0 -20
- data/lib/webauthn/signature_verifier.rb +0 -77
@@ -1,145 +1,84 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "cbor"
|
4
|
+
require "forwardable"
|
4
5
|
require "uri"
|
5
6
|
require "openssl"
|
6
7
|
|
7
|
-
require "webauthn/
|
8
|
+
require "webauthn/attestation_object"
|
8
9
|
require "webauthn/authenticator_response"
|
9
|
-
require "webauthn/attestation_statement"
|
10
10
|
require "webauthn/client_data"
|
11
11
|
require "webauthn/encoder"
|
12
12
|
|
13
13
|
module WebAuthn
|
14
14
|
class AttestationStatementVerificationError < VerificationError; end
|
15
|
-
class AttestationTrustworthinessVerificationError < VerificationError; end
|
16
15
|
class AttestedCredentialVerificationError < VerificationError; end
|
17
16
|
|
18
17
|
class AuthenticatorAttestationResponse < AuthenticatorResponse
|
19
|
-
|
20
|
-
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
|
21
|
+
encoder = relying_party.encoder
|
21
22
|
|
22
23
|
new(
|
23
24
|
attestation_object: encoder.decode(response["attestationObject"]),
|
24
|
-
|
25
|
+
transports: response["transports"],
|
26
|
+
client_data_json: encoder.decode(response["clientDataJSON"]),
|
27
|
+
relying_party: relying_party
|
25
28
|
)
|
26
29
|
end
|
27
30
|
|
28
|
-
attr_reader :attestation_type, :attestation_trust_path
|
31
|
+
attr_reader :attestation_type, :attestation_trust_path, :transports
|
29
32
|
|
30
|
-
def initialize(attestation_object:, **options)
|
33
|
+
def initialize(attestation_object:, transports: [], **options)
|
31
34
|
super(**options)
|
32
35
|
|
33
|
-
@
|
36
|
+
@attestation_object_bytes = attestation_object
|
37
|
+
@transports = transports
|
38
|
+
@relying_party = relying_party
|
34
39
|
end
|
35
40
|
|
36
|
-
def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
|
41
|
+
def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_verification: nil, rp_id: nil)
|
37
42
|
super
|
38
43
|
|
39
44
|
verify_item(:attested_credential)
|
40
|
-
if
|
45
|
+
if relying_party.verify_attestation_statement
|
41
46
|
verify_item(:attestation_statement)
|
42
|
-
verify_item(:attestation_trustworthiness) if WebAuthn.configuration.attestation_root_certificates_finders.any?
|
43
47
|
end
|
44
48
|
|
45
49
|
true
|
46
50
|
end
|
47
51
|
|
48
|
-
def
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
def attestation_statement
|
53
|
-
@attestation_statement ||=
|
54
|
-
WebAuthn::AttestationStatement.from(attestation["fmt"], attestation["attStmt"])
|
52
|
+
def attestation_object
|
53
|
+
@attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes, relying_party)
|
55
54
|
end
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
def_delegators(
|
57
|
+
:attestation_object,
|
58
|
+
:aaguid,
|
59
|
+
:attestation_statement,
|
60
|
+
:attestation_certificate_key_id,
|
61
|
+
:authenticator_data,
|
62
|
+
:credential
|
63
|
+
)
|
60
64
|
|
61
|
-
|
62
|
-
attestation["fmt"]
|
63
|
-
end
|
64
|
-
|
65
|
-
def attestation
|
66
|
-
@attestation ||= CBOR.decode(attestation_object)
|
67
|
-
end
|
68
|
-
|
69
|
-
def aaguid
|
70
|
-
raw_aaguid = authenticator_data.attested_credential_data.raw_aaguid
|
71
|
-
unless raw_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
|
72
|
-
authenticator_data.attested_credential_data.aaguid
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def attestation_certificate_key
|
77
|
-
raw_subject_key_identifier(attestation_statement.attestation_certificate)&.unpack("H*")&.[](0)
|
78
|
-
end
|
65
|
+
alias_method :attestation_certificate_key, :attestation_certificate_key_id
|
79
66
|
|
80
67
|
private
|
81
68
|
|
82
|
-
attr_reader :
|
69
|
+
attr_reader :attestation_object_bytes, :relying_party
|
83
70
|
|
84
71
|
def type
|
85
72
|
WebAuthn::TYPES[:create]
|
86
73
|
end
|
87
74
|
|
88
75
|
def valid_attested_credential?
|
89
|
-
|
90
|
-
authenticator_data.
|
76
|
+
attestation_object.valid_attested_credential? &&
|
77
|
+
relying_party.algorithms.include?(authenticator_data.credential.algorithm)
|
91
78
|
end
|
92
79
|
|
93
80
|
def valid_attestation_statement?
|
94
|
-
@attestation_type, @attestation_trust_path =
|
95
|
-
end
|
96
|
-
|
97
|
-
def valid_attestation_trustworthiness?
|
98
|
-
case @attestation_type
|
99
|
-
when WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE
|
100
|
-
WebAuthn.configuration.acceptable_attestation_types.include?('None')
|
101
|
-
when WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF
|
102
|
-
WebAuthn.configuration.acceptable_attestation_types.include?('Self')
|
103
|
-
else
|
104
|
-
WebAuthn.configuration.acceptable_attestation_types.include?(@attestation_type) &&
|
105
|
-
attestation_root_certificates_store.verify(leaf_certificate, signing_certificates)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def raw_subject_key_identifier(certificate)
|
110
|
-
extension = certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
|
111
|
-
return unless extension
|
112
|
-
|
113
|
-
ext_asn1 = OpenSSL::ASN1.decode(extension.to_der)
|
114
|
-
ext_value = ext_asn1.value.last
|
115
|
-
OpenSSL::ASN1.decode(ext_value.value).value
|
116
|
-
end
|
117
|
-
|
118
|
-
def attestation_root_certificates_store
|
119
|
-
certificates =
|
120
|
-
WebAuthn.configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder|
|
121
|
-
if certs.empty?
|
122
|
-
finder.find(attestation_format: attestation_format,
|
123
|
-
aaguid: aaguid,
|
124
|
-
attestation_certificate_key_id: attestation_certificate_key) || []
|
125
|
-
else
|
126
|
-
certs
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
OpenSSL::X509::Store.new.tap do |store|
|
131
|
-
certificates.each do |cert|
|
132
|
-
store.add_cert(cert)
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def signing_certificates
|
138
|
-
@attestation_trust_path[1..-1]
|
139
|
-
end
|
140
|
-
|
141
|
-
def leaf_certificate
|
142
|
-
@attestation_trust_path.first
|
81
|
+
@attestation_type, @attestation_trust_path = attestation_object.valid_attestation_statement?(client_data.hash)
|
143
82
|
end
|
144
83
|
end
|
145
84
|
end
|
@@ -1,34 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "bindata"
|
3
4
|
require "cose/key"
|
5
|
+
require "webauthn/error"
|
4
6
|
|
5
7
|
module WebAuthn
|
6
|
-
class
|
7
|
-
|
8
|
+
class AttestedCredentialDataFormatError < WebAuthn::Error; end
|
9
|
+
|
10
|
+
class AuthenticatorData < BinData::Record
|
11
|
+
class AttestedCredentialData < BinData::Record
|
8
12
|
AAGUID_LENGTH = 16
|
9
13
|
ZEROED_AAGUID = 0.chr * AAGUID_LENGTH
|
10
14
|
|
11
15
|
ID_LENGTH_LENGTH = 2
|
12
16
|
|
13
|
-
|
17
|
+
endian :big
|
18
|
+
|
19
|
+
string :raw_aaguid, length: AAGUID_LENGTH
|
20
|
+
bit16 :id_length
|
21
|
+
string :id, read_length: :id_length
|
22
|
+
count_bytes_remaining :trailing_bytes_length
|
23
|
+
string :trailing_bytes, length: :trailing_bytes_length
|
14
24
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
25
|
+
Credential =
|
26
|
+
Struct.new(:id, :public_key, :algorithm, keyword_init: true) do
|
27
|
+
def public_key_object
|
28
|
+
COSE::Key.deserialize(public_key).to_pkey
|
29
|
+
end
|
19
30
|
end
|
20
|
-
end
|
21
31
|
|
22
|
-
def
|
23
|
-
|
32
|
+
def self.deserialize(data)
|
33
|
+
read(data)
|
34
|
+
rescue EOFError
|
35
|
+
raise AttestedCredentialDataFormatError
|
24
36
|
end
|
25
37
|
|
26
38
|
def valid?
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
def raw_aaguid
|
31
|
-
data_at(0, AAGUID_LENGTH)
|
39
|
+
valid_credential_public_key?
|
32
40
|
end
|
33
41
|
|
34
42
|
def aaguid
|
@@ -37,62 +45,38 @@ module WebAuthn
|
|
37
45
|
|
38
46
|
def credential
|
39
47
|
@credential ||=
|
40
|
-
if
|
41
|
-
Credential.new(id, public_key)
|
48
|
+
if valid?
|
49
|
+
Credential.new(id: id, public_key: public_key, algorithm: algorithm)
|
42
50
|
end
|
43
51
|
end
|
44
52
|
|
45
53
|
def length
|
46
54
|
if valid?
|
47
|
-
|
55
|
+
AAGUID_LENGTH + ID_LENGTH_LENGTH + id_length + public_key_length
|
48
56
|
end
|
49
57
|
end
|
50
58
|
|
51
59
|
private
|
52
60
|
|
53
|
-
|
61
|
+
def algorithm
|
62
|
+
COSE::Algorithm.find(cose_key.alg).name
|
63
|
+
end
|
54
64
|
|
55
65
|
def valid_credential_public_key?
|
56
|
-
cose_key = COSE::Key.deserialize(public_key)
|
57
|
-
|
58
66
|
!!cose_key.alg
|
59
67
|
end
|
60
68
|
|
61
|
-
def
|
62
|
-
|
63
|
-
data_at(id_position, id_length)
|
64
|
-
end
|
69
|
+
def cose_key
|
70
|
+
@cose_key ||= COSE::Key.deserialize(public_key)
|
65
71
|
end
|
66
72
|
|
67
73
|
def public_key
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
def id_position
|
72
|
-
id_length_position + ID_LENGTH_LENGTH
|
73
|
-
end
|
74
|
-
|
75
|
-
def id_length
|
76
|
-
@id_length ||= data_at(id_length_position, ID_LENGTH_LENGTH).unpack(UINT16_BIG_ENDIAN_FORMAT)[0]
|
77
|
-
end
|
78
|
-
|
79
|
-
def id_length_position
|
80
|
-
AAGUID_LENGTH
|
81
|
-
end
|
82
|
-
|
83
|
-
def public_key_position
|
84
|
-
id_position + id_length
|
74
|
+
trailing_bytes[0..public_key_length - 1]
|
85
75
|
end
|
86
76
|
|
87
77
|
def public_key_length
|
88
78
|
@public_key_length ||=
|
89
|
-
CBOR.encode(CBOR::Unpacker.new(StringIO.new(
|
90
|
-
end
|
91
|
-
|
92
|
-
def data_at(position, length = nil)
|
93
|
-
length ||= data.size - position
|
94
|
-
|
95
|
-
data[position..(position + length - 1)]
|
79
|
+
CBOR.encode(CBOR::Unpacker.new(StringIO.new(trailing_bytes)).each.first).length
|
96
80
|
end
|
97
81
|
end
|
98
82
|
end
|
@@ -1,32 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "bindata"
|
3
4
|
require "webauthn/authenticator_data/attested_credential_data"
|
5
|
+
require "webauthn/error"
|
4
6
|
|
5
7
|
module WebAuthn
|
6
|
-
class
|
7
|
-
RP_ID_HASH_POSITION = 0
|
8
|
+
class AuthenticatorDataFormatError < WebAuthn::Error; end
|
8
9
|
|
10
|
+
class AuthenticatorData < BinData::Record
|
9
11
|
RP_ID_HASH_LENGTH = 32
|
10
12
|
FLAGS_LENGTH = 1
|
11
13
|
SIGN_COUNT_LENGTH = 4
|
12
14
|
|
13
|
-
|
15
|
+
endian :big
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
count_bytes_remaining :data_length
|
18
|
+
string :rp_id_hash, length: RP_ID_HASH_LENGTH
|
19
|
+
struct :flags do
|
20
|
+
bit1 :extension_data_included
|
21
|
+
bit1 :attested_credential_data_included
|
22
|
+
bit1 :reserved_for_future_use_2
|
23
|
+
bit1 :backup_state
|
24
|
+
bit1 :backup_eligibility
|
25
|
+
bit1 :user_verified
|
26
|
+
bit1 :reserved_for_future_use_1
|
27
|
+
bit1 :user_present
|
28
|
+
end
|
29
|
+
bit32 :sign_count
|
30
|
+
count_bytes_remaining :trailing_bytes_length
|
31
|
+
string :trailing_bytes, length: :trailing_bytes_length
|
19
32
|
|
20
|
-
def
|
21
|
-
|
33
|
+
def self.deserialize(data)
|
34
|
+
read(data)
|
35
|
+
rescue EOFError
|
36
|
+
raise AuthenticatorDataFormatError
|
22
37
|
end
|
23
38
|
|
24
|
-
|
39
|
+
def data
|
40
|
+
to_binary_s
|
41
|
+
end
|
25
42
|
|
26
43
|
def valid?
|
27
|
-
|
28
|
-
(!
|
29
|
-
|
44
|
+
(!attested_credential_data_included? || attested_credential_data.valid?) &&
|
45
|
+
(!extension_data_included? || extension_data) &&
|
46
|
+
valid_length?
|
30
47
|
end
|
31
48
|
|
32
49
|
def user_flagged?
|
@@ -34,26 +51,27 @@ module WebAuthn
|
|
34
51
|
end
|
35
52
|
|
36
53
|
def user_present?
|
37
|
-
flags
|
54
|
+
flags.user_present == 1
|
38
55
|
end
|
39
56
|
|
40
57
|
def user_verified?
|
41
|
-
flags
|
58
|
+
flags.user_verified == 1
|
42
59
|
end
|
43
60
|
|
44
|
-
def
|
45
|
-
flags
|
61
|
+
def credential_backup_eligible?
|
62
|
+
flags.backup_eligibility == 1
|
46
63
|
end
|
47
64
|
|
48
|
-
def
|
49
|
-
flags
|
65
|
+
def credential_backed_up?
|
66
|
+
flags.backup_state == 1
|
50
67
|
end
|
51
68
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
69
|
+
def attested_credential_data_included?
|
70
|
+
flags.attested_credential_data_included == 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def extension_data_included?
|
74
|
+
flags.extension_data_included == 1
|
57
75
|
end
|
58
76
|
|
59
77
|
def credential
|
@@ -62,35 +80,39 @@ module WebAuthn
|
|
62
80
|
end
|
63
81
|
end
|
64
82
|
|
65
|
-
def sign_count
|
66
|
-
@sign_count ||= data_at(SIGN_COUNT_POSITION, SIGN_COUNT_LENGTH).unpack('L>')[0]
|
67
|
-
end
|
68
|
-
|
69
83
|
def attested_credential_data
|
70
84
|
@attested_credential_data ||=
|
71
|
-
AttestedCredentialData.
|
85
|
+
AttestedCredentialData.deserialize(trailing_bytes)
|
86
|
+
rescue AttestedCredentialDataFormatError
|
87
|
+
raise AuthenticatorDataFormatError
|
72
88
|
end
|
73
89
|
|
74
90
|
def extension_data
|
75
91
|
@extension_data ||= CBOR.decode(raw_extension_data)
|
76
92
|
end
|
77
93
|
|
78
|
-
def
|
79
|
-
|
94
|
+
def aaguid
|
95
|
+
raw_aaguid = attested_credential_data.raw_aaguid
|
96
|
+
|
97
|
+
unless raw_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
|
98
|
+
attested_credential_data.aaguid
|
99
|
+
end
|
80
100
|
end
|
81
101
|
|
82
102
|
private
|
83
103
|
|
84
104
|
def valid_length?
|
85
|
-
|
105
|
+
data_length == base_length + attested_credential_data_length + extension_data_length
|
86
106
|
end
|
87
107
|
|
88
108
|
def raw_extension_data
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
109
|
+
if extension_data_included?
|
110
|
+
if attested_credential_data_included?
|
111
|
+
trailing_bytes[attested_credential_data.length..-1]
|
112
|
+
else
|
113
|
+
trailing_bytes.snapshot
|
114
|
+
end
|
115
|
+
end
|
94
116
|
end
|
95
117
|
|
96
118
|
def attested_credential_data_length
|
@@ -109,22 +131,8 @@ module WebAuthn
|
|
109
131
|
end
|
110
132
|
end
|
111
133
|
|
112
|
-
def extension_data_position
|
113
|
-
base_length + attested_credential_data_length
|
114
|
-
end
|
115
|
-
|
116
134
|
def base_length
|
117
135
|
RP_ID_HASH_LENGTH + FLAGS_LENGTH + SIGN_COUNT_LENGTH
|
118
136
|
end
|
119
|
-
|
120
|
-
def flags_position
|
121
|
-
RP_ID_HASH_LENGTH
|
122
|
-
end
|
123
|
-
|
124
|
-
def data_at(position, length = nil)
|
125
|
-
length ||= data.size - position
|
126
|
-
|
127
|
-
data[position..(position + length - 1)]
|
128
|
-
end
|
129
137
|
end
|
130
138
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "webauthn/authenticator_data"
|
3
4
|
require "webauthn/client_data"
|
4
5
|
require "webauthn/error"
|
5
|
-
require "webauthn/security_utils"
|
6
6
|
|
7
7
|
module WebAuthn
|
8
8
|
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
|
@@ -19,22 +19,29 @@ module WebAuthn
|
|
19
19
|
class UserVerifiedVerificationError < VerificationError; end
|
20
20
|
|
21
21
|
class AuthenticatorResponse
|
22
|
-
def initialize(client_data_json:)
|
22
|
+
def initialize(client_data_json:, relying_party: WebAuthn.configuration.relying_party)
|
23
23
|
@client_data_json = client_data_json
|
24
|
+
@relying_party = relying_party
|
24
25
|
end
|
25
26
|
|
26
|
-
def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
|
27
|
-
expected_origin ||=
|
28
|
-
|
27
|
+
def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_verification: nil, rp_id: nil)
|
28
|
+
expected_origin ||= relying_party.allowed_origins || raise("Unspecified expected origin")
|
29
|
+
|
30
|
+
rp_id ||= relying_party.id
|
29
31
|
|
30
32
|
verify_item(:type)
|
31
33
|
verify_item(:token_binding)
|
32
34
|
verify_item(:challenge, expected_challenge)
|
33
35
|
verify_item(:origin, expected_origin)
|
34
36
|
verify_item(:authenticator_data)
|
35
|
-
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
|
36
37
|
|
37
|
-
|
38
|
+
verify_item(
|
39
|
+
:rp_id,
|
40
|
+
rp_id || rp_id_from_origin(expected_origin)
|
41
|
+
)
|
42
|
+
|
43
|
+
# Fallback to RP configuration unless user_presence is passed in explicitely
|
44
|
+
if user_presence.nil? && !relying_party.silent_authentication || user_presence
|
38
45
|
verify_item(:user_presence)
|
39
46
|
end
|
40
47
|
|
@@ -57,7 +64,7 @@ module WebAuthn
|
|
57
64
|
|
58
65
|
private
|
59
66
|
|
60
|
-
attr_reader :client_data_json
|
67
|
+
attr_reader :client_data_json, :relying_party
|
61
68
|
|
62
69
|
def verify_item(item, *args)
|
63
70
|
if send("valid_#{item}?", *args)
|
@@ -78,19 +85,25 @@ module WebAuthn
|
|
78
85
|
end
|
79
86
|
|
80
87
|
def valid_challenge?(expected_challenge)
|
81
|
-
|
88
|
+
OpenSSL.secure_compare(client_data.challenge, expected_challenge)
|
82
89
|
end
|
83
90
|
|
84
91
|
def valid_origin?(expected_origin)
|
85
|
-
|
92
|
+
return false unless expected_origin
|
93
|
+
|
94
|
+
expected_origin.include?(client_data.origin)
|
86
95
|
end
|
87
96
|
|
88
97
|
def valid_rp_id?(rp_id)
|
98
|
+
return false unless rp_id
|
99
|
+
|
89
100
|
OpenSSL::Digest::SHA256.digest(rp_id) == authenticator_data.rp_id_hash
|
90
101
|
end
|
91
102
|
|
92
103
|
def valid_authenticator_data?
|
93
104
|
authenticator_data.valid?
|
105
|
+
rescue WebAuthn::AuthenticatorDataFormatError
|
106
|
+
false
|
94
107
|
end
|
95
108
|
|
96
109
|
def valid_user_presence?
|
@@ -102,7 +115,7 @@ module WebAuthn
|
|
102
115
|
end
|
103
116
|
|
104
117
|
def rp_id_from_origin(expected_origin)
|
105
|
-
URI.parse(expected_origin).host
|
118
|
+
URI.parse(expected_origin.first).host if expected_origin.size == 1
|
106
119
|
end
|
107
120
|
|
108
121
|
def type
|
data/lib/webauthn/client_data.rb
CHANGED
@@ -49,12 +49,10 @@ module WebAuthn
|
|
49
49
|
|
50
50
|
def data
|
51
51
|
@data ||=
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
raise ClientDataMissingError, "Client Data JSON is missing"
|
57
|
-
end
|
52
|
+
if client_data_json
|
53
|
+
JSON.parse(client_data_json)
|
54
|
+
else
|
55
|
+
raise ClientDataMissingError, "Client Data JSON is missing"
|
58
56
|
end
|
59
57
|
end
|
60
58
|
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require "webauthn/error"
|
3
|
+
require 'forwardable'
|
4
|
+
require 'webauthn/relying_party'
|
6
5
|
|
7
6
|
module WebAuthn
|
8
7
|
def self.configuration
|
@@ -13,54 +12,53 @@ module WebAuthn
|
|
13
12
|
yield(configuration)
|
14
13
|
end
|
15
14
|
|
16
|
-
class RootCertificateFinderNotSupportedError < Error; end
|
17
|
-
|
18
15
|
class Configuration
|
19
|
-
|
20
|
-
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
|
21
|
-
end
|
16
|
+
extend Forwardable
|
22
17
|
|
23
|
-
|
18
|
+
def_delegators :@relying_party,
|
19
|
+
:algorithms,
|
20
|
+
:algorithms=,
|
21
|
+
:encoding,
|
22
|
+
:encoding=,
|
23
|
+
:origin,
|
24
|
+
:origin=,
|
25
|
+
:allowed_origins,
|
26
|
+
:allowed_origins=,
|
27
|
+
:verify_attestation_statement,
|
28
|
+
:verify_attestation_statement=,
|
29
|
+
:credential_options_timeout,
|
30
|
+
:credential_options_timeout=,
|
31
|
+
:silent_authentication,
|
32
|
+
:silent_authentication=,
|
33
|
+
:acceptable_attestation_types,
|
34
|
+
:acceptable_attestation_types=,
|
35
|
+
:attestation_root_certificates_finders,
|
36
|
+
:attestation_root_certificates_finders=,
|
37
|
+
:encoder,
|
38
|
+
:encoder=,
|
39
|
+
:legacy_u2f_appid,
|
40
|
+
:legacy_u2f_appid=
|
24
41
|
|
25
|
-
|
26
|
-
attr_accessor :encoding
|
27
|
-
attr_accessor :origin
|
28
|
-
attr_accessor :rp_id
|
29
|
-
attr_accessor :rp_name
|
30
|
-
attr_accessor :verify_attestation_statement
|
31
|
-
attr_accessor :credential_options_timeout
|
32
|
-
attr_accessor :silent_authentication
|
33
|
-
attr_accessor :acceptable_attestation_types
|
34
|
-
attr_reader :attestation_root_certificates_finders
|
42
|
+
attr_reader :relying_party
|
35
43
|
|
36
44
|
def initialize
|
37
|
-
@
|
38
|
-
@encoding = WebAuthn::Encoder::STANDARD_ENCODING
|
39
|
-
@verify_attestation_statement = true
|
40
|
-
@credential_options_timeout = 120000
|
41
|
-
@silent_authentication = false
|
42
|
-
@acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']
|
43
|
-
@attestation_root_certificates_finders = []
|
45
|
+
@relying_party = RelyingParty.new
|
44
46
|
end
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
def encoder
|
49
|
-
@encoder ||= WebAuthn::Encoder.new(encoding)
|
48
|
+
def rp_name
|
49
|
+
relying_party.name
|
50
50
|
end
|
51
51
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
end
|
52
|
+
def rp_name=(name)
|
53
|
+
relying_party.name = name
|
54
|
+
end
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
end
|
56
|
+
def rp_id
|
57
|
+
relying_party.id
|
58
|
+
end
|
62
59
|
|
63
|
-
|
60
|
+
def rp_id=(id)
|
61
|
+
relying_party.id = id
|
64
62
|
end
|
65
63
|
end
|
66
64
|
end
|