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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +65 -13
- data/.travis.yml +22 -18
- data/Appraisals +4 -0
- data/CHANGELOG.md +72 -25
- data/CONTRIBUTING.md +0 -5
- data/README.md +172 -15
- data/SECURITY.md +4 -4
- data/gemfiles/openssl_2_2.gemfile +7 -0
- data/lib/cose/rsapkcs1_algorithm.rb +43 -0
- data/lib/webauthn/attestation_object.rb +43 -0
- data/lib/webauthn/attestation_statement.rb +20 -20
- data/lib/webauthn/attestation_statement/android_key.rb +28 -30
- data/lib/webauthn/attestation_statement/android_safetynet.rb +30 -20
- data/lib/webauthn/attestation_statement/base.rb +124 -14
- data/lib/webauthn/attestation_statement/fido_u2f.rb +13 -9
- data/lib/webauthn/attestation_statement/packed.rb +14 -42
- data/lib/webauthn/attestation_statement/tpm.rb +38 -54
- data/lib/webauthn/authenticator_assertion_response.rb +7 -36
- data/lib/webauthn/authenticator_attestation_response.rb +24 -46
- data/lib/webauthn/authenticator_data.rb +51 -51
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +29 -50
- data/lib/webauthn/authenticator_response.rb +15 -10
- data/lib/webauthn/configuration.rb +23 -0
- data/lib/webauthn/credential.rb +4 -4
- data/lib/webauthn/credential_creation_options.rb +1 -1
- data/lib/webauthn/fake_authenticator.rb +7 -3
- data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +2 -4
- data/lib/webauthn/fake_client.rb +17 -4
- data/lib/webauthn/public_key.rb +68 -0
- data/lib/webauthn/public_key_credential.rb +13 -3
- data/lib/webauthn/public_key_credential/creation_options.rb +2 -2
- data/lib/webauthn/u2f_migrator.rb +5 -4
- data/lib/webauthn/version.rb +1 -1
- data/script/ci/install-openssl +7 -0
- data/script/ci/install-ruby +13 -0
- data/webauthn.gemspec +14 -9
- metadata +70 -42
- data/lib/android_safetynet/attestation_response.rb +0 -84
- data/lib/cose/algorithm.rb +0 -38
- data/lib/tpm/constants.rb +0 -22
- data/lib/tpm/s_attest.rb +0 -26
- data/lib/tpm/s_attest/s_certify_info.rb +0 -14
- data/lib/tpm/sized_buffer.rb +0 -13
- data/lib/tpm/t_public.rb +0 -32
- data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
- data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
- 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/signature_verifier.rb +0 -65
@@ -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_4
|
23
|
+
bit1 :reserved_for_future_use_3
|
24
|
+
bit1 :reserved_for_future_use_2
|
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,19 @@ 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
61
|
def attested_credential_data_included?
|
45
|
-
flags
|
62
|
+
flags.attested_credential_data_included == 1
|
46
63
|
end
|
47
64
|
|
48
65
|
def extension_data_included?
|
49
|
-
flags
|
50
|
-
end
|
51
|
-
|
52
|
-
def rp_id_hash
|
53
|
-
@rp_id_hash ||=
|
54
|
-
if valid?
|
55
|
-
data_at(RP_ID_HASH_POSITION, RP_ID_HASH_LENGTH)
|
56
|
-
end
|
66
|
+
flags.extension_data_included == 1
|
57
67
|
end
|
58
68
|
|
59
69
|
def credential
|
@@ -62,35 +72,39 @@ module WebAuthn
|
|
62
72
|
end
|
63
73
|
end
|
64
74
|
|
65
|
-
def sign_count
|
66
|
-
@sign_count ||= data_at(SIGN_COUNT_POSITION, SIGN_COUNT_LENGTH).unpack('L>')[0]
|
67
|
-
end
|
68
|
-
|
69
75
|
def attested_credential_data
|
70
76
|
@attested_credential_data ||=
|
71
|
-
AttestedCredentialData.
|
77
|
+
AttestedCredentialData.deserialize(trailing_bytes)
|
78
|
+
rescue AttestedCredentialDataFormatError
|
79
|
+
raise AuthenticatorDataFormatError
|
72
80
|
end
|
73
81
|
|
74
82
|
def extension_data
|
75
83
|
@extension_data ||= CBOR.decode(raw_extension_data)
|
76
84
|
end
|
77
85
|
|
78
|
-
def
|
79
|
-
|
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
|
80
92
|
end
|
81
93
|
|
82
94
|
private
|
83
95
|
|
84
96
|
def valid_length?
|
85
|
-
|
97
|
+
data_length == base_length + attested_credential_data_length + extension_data_length
|
86
98
|
end
|
87
99
|
|
88
100
|
def raw_extension_data
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
101
|
+
if extension_data_included?
|
102
|
+
if attested_credential_data_included?
|
103
|
+
trailing_bytes[attested_credential_data.length..-1]
|
104
|
+
else
|
105
|
+
trailing_bytes.snapshot
|
106
|
+
end
|
107
|
+
end
|
94
108
|
end
|
95
109
|
|
96
110
|
def attested_credential_data_length
|
@@ -109,22 +123,8 @@ module WebAuthn
|
|
109
123
|
end
|
110
124
|
end
|
111
125
|
|
112
|
-
def extension_data_position
|
113
|
-
base_length + attested_credential_data_length
|
114
|
-
end
|
115
|
-
|
116
126
|
def base_length
|
117
127
|
RP_ID_HASH_LENGTH + FLAGS_LENGTH + SIGN_COUNT_LENGTH
|
118
128
|
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
129
|
end
|
130
130
|
end
|
@@ -1,34 +1,43 @@
|
|
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
|
-
Credential =
|
17
|
-
|
18
|
-
|
25
|
+
# TODO: use keyword_init when we dropped Ruby 2.4 support
|
26
|
+
Credential =
|
27
|
+
Struct.new(:id, :public_key) do
|
28
|
+
def public_key_object
|
29
|
+
COSE::Key.deserialize(public_key).to_pkey
|
30
|
+
end
|
19
31
|
end
|
20
|
-
end
|
21
32
|
|
22
|
-
def
|
23
|
-
|
33
|
+
def self.deserialize(data)
|
34
|
+
read(data)
|
35
|
+
rescue EOFError
|
36
|
+
raise AttestedCredentialDataFormatError
|
24
37
|
end
|
25
38
|
|
26
39
|
def valid?
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
def raw_aaguid
|
31
|
-
data_at(0, AAGUID_LENGTH)
|
40
|
+
valid_credential_public_key?
|
32
41
|
end
|
33
42
|
|
34
43
|
def aaguid
|
@@ -37,62 +46,32 @@ module WebAuthn
|
|
37
46
|
|
38
47
|
def credential
|
39
48
|
@credential ||=
|
40
|
-
if
|
49
|
+
if valid?
|
41
50
|
Credential.new(id, public_key)
|
42
51
|
end
|
43
52
|
end
|
44
53
|
|
45
54
|
def length
|
46
55
|
if valid?
|
47
|
-
|
56
|
+
AAGUID_LENGTH + ID_LENGTH_LENGTH + id_length + public_key_length
|
48
57
|
end
|
49
58
|
end
|
50
59
|
|
51
60
|
private
|
52
61
|
|
53
|
-
attr_reader :data
|
54
|
-
|
55
62
|
def valid_credential_public_key?
|
56
63
|
cose_key = COSE::Key.deserialize(public_key)
|
57
64
|
|
58
|
-
!!cose_key.alg
|
59
|
-
end
|
60
|
-
|
61
|
-
def id
|
62
|
-
if valid?
|
63
|
-
data_at(id_position, id_length)
|
64
|
-
end
|
65
|
+
!!cose_key.alg && WebAuthn.configuration.algorithms.include?(COSE::Algorithm.find(cose_key.alg).name)
|
65
66
|
end
|
66
67
|
|
67
68
|
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
|
69
|
+
trailing_bytes[0..public_key_length - 1]
|
85
70
|
end
|
86
71
|
|
87
72
|
def public_key_length
|
88
73
|
@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)]
|
74
|
+
CBOR.encode(CBOR::Unpacker.new(StringIO.new(trailing_bytes)).each.first).length
|
96
75
|
end
|
97
76
|
end
|
98
77
|
end
|
@@ -1,5 +1,6 @@
|
|
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
6
|
require "webauthn/security_utils"
|
@@ -33,14 +34,20 @@ module WebAuthn
|
|
33
34
|
verify_item(:origin, expected_origin)
|
34
35
|
verify_item(:authenticator_data)
|
35
36
|
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
|
36
|
-
|
37
|
-
|
37
|
+
|
38
|
+
if !WebAuthn.configuration.silent_authentication
|
39
|
+
verify_item(:user_presence)
|
40
|
+
end
|
41
|
+
|
42
|
+
if user_verification
|
43
|
+
verify_item(:user_verified)
|
44
|
+
end
|
38
45
|
|
39
46
|
true
|
40
47
|
end
|
41
48
|
|
42
|
-
def valid?(*args)
|
43
|
-
verify(*args)
|
49
|
+
def valid?(*args, **keyword_arguments)
|
50
|
+
verify(*args, **keyword_arguments)
|
44
51
|
rescue WebAuthn::VerificationError
|
45
52
|
false
|
46
53
|
end
|
@@ -85,18 +92,16 @@ module WebAuthn
|
|
85
92
|
|
86
93
|
def valid_authenticator_data?
|
87
94
|
authenticator_data.valid?
|
95
|
+
rescue WebAuthn::AuthenticatorDataFormatError
|
96
|
+
false
|
88
97
|
end
|
89
98
|
|
90
99
|
def valid_user_presence?
|
91
100
|
authenticator_data.user_flagged?
|
92
101
|
end
|
93
102
|
|
94
|
-
def valid_user_verified?
|
95
|
-
|
96
|
-
authenticator_data.user_verified?
|
97
|
-
else
|
98
|
-
true
|
99
|
-
end
|
103
|
+
def valid_user_verified?
|
104
|
+
authenticator_data.user_verified?
|
100
105
|
end
|
101
106
|
|
102
107
|
def rp_id_from_origin(expected_origin)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "openssl"
|
4
4
|
require "webauthn/encoder"
|
5
|
+
require "webauthn/error"
|
5
6
|
|
6
7
|
module WebAuthn
|
7
8
|
def self.configuration
|
@@ -12,6 +13,8 @@ module WebAuthn
|
|
12
13
|
yield(configuration)
|
13
14
|
end
|
14
15
|
|
16
|
+
class RootCertificateFinderNotSupportedError < Error; end
|
17
|
+
|
15
18
|
class Configuration
|
16
19
|
def self.if_pss_supported(algorithm)
|
17
20
|
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
|
@@ -26,12 +29,18 @@ module WebAuthn
|
|
26
29
|
attr_accessor :rp_name
|
27
30
|
attr_accessor :verify_attestation_statement
|
28
31
|
attr_accessor :credential_options_timeout
|
32
|
+
attr_accessor :silent_authentication
|
33
|
+
attr_accessor :acceptable_attestation_types
|
34
|
+
attr_reader :attestation_root_certificates_finders
|
29
35
|
|
30
36
|
def initialize
|
31
37
|
@algorithms = DEFAULT_ALGORITHMS.dup
|
32
38
|
@encoding = WebAuthn::Encoder::STANDARD_ENCODING
|
33
39
|
@verify_attestation_statement = true
|
34
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 = []
|
35
44
|
end
|
36
45
|
|
37
46
|
# This is the user-data encoder.
|
@@ -39,5 +48,19 @@ module WebAuthn
|
|
39
48
|
def encoder
|
40
49
|
@encoder ||= WebAuthn::Encoder.new(encoding)
|
41
50
|
end
|
51
|
+
|
52
|
+
def attestation_root_certificates_finders=(finders)
|
53
|
+
if !finders.respond_to?(:each)
|
54
|
+
finders = [finders]
|
55
|
+
end
|
56
|
+
|
57
|
+
finders.each do |finder|
|
58
|
+
unless finder.respond_to?(:find)
|
59
|
+
raise RootCertificateFinderNotSupportedError, "Finder must implement `find` method"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@attestation_root_certificates_finders = finders
|
64
|
+
end
|
42
65
|
end
|
43
66
|
end
|
data/lib/webauthn/credential.rb
CHANGED
@@ -7,12 +7,12 @@ require "webauthn/public_key_credential_with_attestation"
|
|
7
7
|
|
8
8
|
module WebAuthn
|
9
9
|
module Credential
|
10
|
-
def self.options_for_create(
|
11
|
-
WebAuthn::PublicKeyCredential::CreationOptions.new(
|
10
|
+
def self.options_for_create(**keyword_arguments)
|
11
|
+
WebAuthn::PublicKeyCredential::CreationOptions.new(**keyword_arguments)
|
12
12
|
end
|
13
13
|
|
14
|
-
def self.options_for_get(
|
15
|
-
WebAuthn::PublicKeyCredential::RequestOptions.new(
|
14
|
+
def self.options_for_get(**keyword_arguments)
|
15
|
+
WebAuthn::PublicKeyCredential::RequestOptions.new(**keyword_arguments)
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.from_create(credential)
|
@@ -18,7 +18,8 @@ module WebAuthn
|
|
18
18
|
user_present: true,
|
19
19
|
user_verified: false,
|
20
20
|
attested_credential_data: true,
|
21
|
-
sign_count: nil
|
21
|
+
sign_count: nil,
|
22
|
+
extensions: nil
|
22
23
|
)
|
23
24
|
credential_id, credential_key, credential_sign_count = new_credential
|
24
25
|
sign_count ||= credential_sign_count
|
@@ -37,7 +38,8 @@ module WebAuthn
|
|
37
38
|
user_present: user_present,
|
38
39
|
user_verified: user_verified,
|
39
40
|
attested_credential_data: attested_credential_data,
|
40
|
-
sign_count: sign_count
|
41
|
+
sign_count: sign_count,
|
42
|
+
extensions: extensions
|
41
43
|
).serialize
|
42
44
|
end
|
43
45
|
|
@@ -47,7 +49,8 @@ module WebAuthn
|
|
47
49
|
user_present: true,
|
48
50
|
user_verified: false,
|
49
51
|
aaguid: AuthenticatorData::AAGUID,
|
50
|
-
sign_count: nil
|
52
|
+
sign_count: nil,
|
53
|
+
extensions: nil
|
51
54
|
)
|
52
55
|
credential_options = credentials[rp_id]
|
53
56
|
|
@@ -63,6 +66,7 @@ module WebAuthn
|
|
63
66
|
aaguid: aaguid,
|
64
67
|
credential: nil,
|
65
68
|
sign_count: sign_count || credential_sign_count,
|
69
|
+
extensions: extensions
|
66
70
|
).serialize
|
67
71
|
|
68
72
|
signature = credential_key.sign("SHA256", authenticator_data + client_data_hash)
|