webauthn 2.1.0 → 2.4.1
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/.rubocop.yml +113 -13
- data/.travis.yml +21 -18
- data/Appraisals +4 -0
- data/CHANGELOG.md +41 -0
- data/CONTRIBUTING.md +0 -5
- data/README.md +70 -8
- data/SECURITY.md +6 -4
- data/gemfiles/openssl_2_2.gemfile +7 -0
- data/lib/cose/rsapkcs1_algorithm.rb +50 -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 +27 -7
- data/lib/webauthn/attestation_statement/base.rb +108 -10
- 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 +13 -41
- data/lib/webauthn/attestation_statement/tpm.rb +38 -75
- data/lib/webauthn/authenticator_assertion_response.rb +3 -7
- data/lib/webauthn/authenticator_attestation_response.rb +19 -84
- 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 +3 -0
- data/lib/webauthn/credential_creation_options.rb +2 -0
- data/lib/webauthn/credential_request_options.rb +2 -0
- 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 +19 -5
- data/lib/webauthn/public_key.rb +21 -2
- data/lib/webauthn/public_key_credential.rb +13 -3
- 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 +13 -9
- metadata +54 -41
- 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.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 -77
@@ -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"
|
@@ -91,6 +92,8 @@ module WebAuthn
|
|
91
92
|
|
92
93
|
def valid_authenticator_data?
|
93
94
|
authenticator_data.valid?
|
95
|
+
rescue WebAuthn::AuthenticatorDataFormatError
|
96
|
+
false
|
94
97
|
end
|
95
98
|
|
96
99
|
def valid_user_presence?
|
@@ -16,6 +16,8 @@ module WebAuthn
|
|
16
16
|
attr_accessor :allow_credentials, :extensions, :user_verification
|
17
17
|
|
18
18
|
def initialize(allow_credentials: [], extensions: nil, user_verification: nil)
|
19
|
+
super()
|
20
|
+
|
19
21
|
@allow_credentials = allow_credentials
|
20
22
|
@extensions = extensions
|
21
23
|
@user_verification = user_verification
|
@@ -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)
|
@@ -14,7 +14,8 @@ module WebAuthn
|
|
14
14
|
user_present: true,
|
15
15
|
user_verified: false,
|
16
16
|
attested_credential_data: true,
|
17
|
-
sign_count: 0
|
17
|
+
sign_count: 0,
|
18
|
+
extensions: nil
|
18
19
|
)
|
19
20
|
@client_data_hash = client_data_hash
|
20
21
|
@rp_id_hash = rp_id_hash
|
@@ -24,6 +25,7 @@ module WebAuthn
|
|
24
25
|
@user_verified = user_verified
|
25
26
|
@attested_credential_data = attested_credential_data
|
26
27
|
@sign_count = sign_count
|
28
|
+
@extensions = extensions
|
27
29
|
end
|
28
30
|
|
29
31
|
def serialize
|
@@ -44,7 +46,8 @@ module WebAuthn
|
|
44
46
|
:user_present,
|
45
47
|
:user_verified,
|
46
48
|
:attested_credential_data,
|
47
|
-
:sign_count
|
49
|
+
:sign_count,
|
50
|
+
:extensions
|
48
51
|
)
|
49
52
|
|
50
53
|
def authenticator_data
|
@@ -60,7 +63,8 @@ module WebAuthn
|
|
60
63
|
credential: credential_data,
|
61
64
|
user_present: user_present,
|
62
65
|
user_verified: user_verified,
|
63
|
-
sign_count: 0
|
66
|
+
sign_count: 0,
|
67
|
+
extensions: extensions
|
64
68
|
)
|
65
69
|
end
|
66
70
|
end
|
@@ -115,8 +115,7 @@ module WebAuthn
|
|
115
115
|
case credential[:public_key]
|
116
116
|
when OpenSSL::PKey::RSA
|
117
117
|
key = COSE::Key::RSA.from_pkey(credential[:public_key])
|
118
|
-
|
119
|
-
key.instance_variable_set(:@alg, -257)
|
118
|
+
key.alg = -257
|
120
119
|
when OpenSSL::PKey::EC::Point
|
121
120
|
alg = {
|
122
121
|
COSE::Key::Curve.by_name("P-256").id => -7,
|
@@ -125,8 +124,7 @@ module WebAuthn
|
|
125
124
|
}
|
126
125
|
|
127
126
|
key = COSE::Key::EC2.from_pkey(credential[:public_key])
|
128
|
-
|
129
|
-
key.instance_variable_set(:@alg, alg[key.crv])
|
127
|
+
key.alg = alg[key.crv]
|
130
128
|
|
131
129
|
end
|
132
130
|
|
data/lib/webauthn/fake_client.rb
CHANGED
@@ -29,7 +29,8 @@ module WebAuthn
|
|
29
29
|
rp_id: nil,
|
30
30
|
user_present: true,
|
31
31
|
user_verified: false,
|
32
|
-
attested_credential_data: true
|
32
|
+
attested_credential_data: true,
|
33
|
+
extensions: nil
|
33
34
|
)
|
34
35
|
rp_id ||= URI.parse(origin).host
|
35
36
|
|
@@ -41,12 +42,16 @@ module WebAuthn
|
|
41
42
|
client_data_hash: client_data_hash,
|
42
43
|
user_present: user_present,
|
43
44
|
user_verified: user_verified,
|
44
|
-
attested_credential_data: attested_credential_data
|
45
|
+
attested_credential_data: attested_credential_data,
|
46
|
+
extensions: extensions
|
45
47
|
)
|
46
48
|
|
47
49
|
id =
|
48
50
|
if attested_credential_data
|
49
|
-
WebAuthn::AuthenticatorData
|
51
|
+
WebAuthn::AuthenticatorData
|
52
|
+
.deserialize(CBOR.decode(attestation_object)["authData"])
|
53
|
+
.attested_credential_data
|
54
|
+
.id
|
50
55
|
else
|
51
56
|
"id-for-pk-without-attested-credential-data"
|
52
57
|
end
|
@@ -55,6 +60,7 @@ module WebAuthn
|
|
55
60
|
"type" => "public-key",
|
56
61
|
"id" => internal_encoder.encode(id),
|
57
62
|
"rawId" => encoder.encode(id),
|
63
|
+
"clientExtensionResults" => extensions,
|
58
64
|
"response" => {
|
59
65
|
"attestationObject" => encoder.encode(attestation_object),
|
60
66
|
"clientDataJSON" => encoder.encode(client_data_json)
|
@@ -62,7 +68,13 @@ module WebAuthn
|
|
62
68
|
}
|
63
69
|
end
|
64
70
|
|
65
|
-
def get(challenge: fake_challenge,
|
71
|
+
def get(challenge: fake_challenge,
|
72
|
+
rp_id: nil,
|
73
|
+
user_present: true,
|
74
|
+
user_verified: false,
|
75
|
+
sign_count: nil,
|
76
|
+
extensions: nil,
|
77
|
+
user_handle: nil)
|
66
78
|
rp_id ||= URI.parse(origin).host
|
67
79
|
|
68
80
|
client_data_json = data_json_for(:get, encoder.decode(challenge))
|
@@ -74,17 +86,19 @@ module WebAuthn
|
|
74
86
|
user_present: user_present,
|
75
87
|
user_verified: user_verified,
|
76
88
|
sign_count: sign_count,
|
89
|
+
extensions: extensions
|
77
90
|
)
|
78
91
|
|
79
92
|
{
|
80
93
|
"type" => "public-key",
|
81
94
|
"id" => internal_encoder.encode(assertion[:credential_id]),
|
82
95
|
"rawId" => encoder.encode(assertion[:credential_id]),
|
96
|
+
"clientExtensionResults" => extensions,
|
83
97
|
"response" => {
|
84
98
|
"clientDataJSON" => encoder.encode(client_data_json),
|
85
99
|
"authenticatorData" => encoder.encode(assertion[:authenticator_data]),
|
86
100
|
"signature" => encoder.encode(assertion[:signature]),
|
87
|
-
"userHandle" => nil
|
101
|
+
"userHandle" => user_handle ? encoder.encode(user_handle) : nil
|
88
102
|
}
|
89
103
|
}
|
90
104
|
end
|