webauthn 2.5.2 → 3.1.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 +7 -3
- data/.github/workflows/git.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +40 -3
- data/README.md +4 -4
- data/docs/advanced_configuration.md +10 -10
- data/docs/u2f_migration.md +14 -21
- data/lib/webauthn/attestation_object.rb +9 -5
- data/lib/webauthn/attestation_statement/base.rb +16 -10
- data/lib/webauthn/attestation_statement.rb +2 -2
- data/lib/webauthn/authenticator_assertion_response.rb +4 -3
- data/lib/webauthn/authenticator_attestation_response.rb +10 -7
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +10 -5
- data/lib/webauthn/authenticator_data.rb +10 -2
- data/lib/webauthn/authenticator_response.rb +6 -5
- data/lib/webauthn/configuration.rb +38 -38
- data/lib/webauthn/credential.rb +4 -4
- data/lib/webauthn/encoder.rb +13 -4
- data/lib/webauthn/fake_authenticator/attestation_object.rb +8 -0
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +19 -4
- data/lib/webauthn/fake_authenticator.rb +8 -0
- data/lib/webauthn/fake_client.rb +12 -2
- data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
- data/lib/webauthn/public_key_credential/options.rb +9 -8
- data/lib/webauthn/public_key_credential/request_options.rb +11 -1
- data/lib/webauthn/public_key_credential.rb +41 -7
- data/lib/webauthn/public_key_credential_with_assertion.rb +14 -1
- data/lib/webauthn/relying_party.rb +120 -0
- data/lib/webauthn/u2f_migrator.rb +4 -1
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +4 -3
- metadata +22 -12
|
@@ -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,50 +12,51 @@ module WebAuthn
|
|
|
13
12
|
yield(configuration)
|
|
14
13
|
end
|
|
15
14
|
|
|
16
|
-
class RootCertificateFinderNotSupportedError < Error; end
|
|
17
|
-
|
|
18
15
|
class Configuration
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
extend Forwardable
|
|
17
|
+
|
|
18
|
+
def_delegators :@relying_party,
|
|
19
|
+
:algorithms,
|
|
20
|
+
:algorithms=,
|
|
21
|
+
:encoding,
|
|
22
|
+
:encoding=,
|
|
23
|
+
:origin,
|
|
24
|
+
:origin=,
|
|
25
|
+
:verify_attestation_statement,
|
|
26
|
+
:verify_attestation_statement=,
|
|
27
|
+
:credential_options_timeout,
|
|
28
|
+
:credential_options_timeout=,
|
|
29
|
+
:silent_authentication,
|
|
30
|
+
:silent_authentication=,
|
|
31
|
+
:acceptable_attestation_types,
|
|
32
|
+
:acceptable_attestation_types=,
|
|
33
|
+
:attestation_root_certificates_finders,
|
|
34
|
+
:attestation_root_certificates_finders=,
|
|
35
|
+
:encoder,
|
|
36
|
+
:encoder=,
|
|
37
|
+
:legacy_u2f_appid,
|
|
38
|
+
:legacy_u2f_appid=
|
|
39
|
+
|
|
40
|
+
attr_reader :relying_party
|
|
31
41
|
|
|
32
42
|
def initialize
|
|
33
|
-
@
|
|
34
|
-
@encoding = WebAuthn::Encoder::STANDARD_ENCODING
|
|
35
|
-
@verify_attestation_statement = true
|
|
36
|
-
@credential_options_timeout = 120000
|
|
37
|
-
@silent_authentication = false
|
|
38
|
-
@acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA', 'AnonCA']
|
|
39
|
-
@attestation_root_certificates_finders = []
|
|
43
|
+
@relying_party = RelyingParty.new
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def encoder
|
|
45
|
-
@encoder ||= WebAuthn::Encoder.new(encoding)
|
|
46
|
+
def rp_name
|
|
47
|
+
relying_party.name
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
end
|
|
50
|
+
def rp_name=(name)
|
|
51
|
+
relying_party.name = name
|
|
52
|
+
end
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
57
|
-
end
|
|
54
|
+
def rp_id
|
|
55
|
+
relying_party.id
|
|
56
|
+
end
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
def rp_id=(id)
|
|
59
|
+
relying_party.id = id
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
end
|
data/lib/webauthn/credential.rb
CHANGED
|
@@ -15,12 +15,12 @@ module WebAuthn
|
|
|
15
15
|
WebAuthn::PublicKeyCredential::RequestOptions.new(**keyword_arguments)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def self.from_create(credential)
|
|
19
|
-
WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential)
|
|
18
|
+
def self.from_create(credential, relying_party: WebAuthn.configuration.relying_party)
|
|
19
|
+
WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential, relying_party: relying_party)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def self.from_get(credential)
|
|
23
|
-
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential)
|
|
22
|
+
def self.from_get(credential, relying_party: WebAuthn.configuration.relying_party)
|
|
23
|
+
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential, relying_party: relying_party)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
data/lib/webauthn/encoder.rb
CHANGED
|
@@ -20,9 +20,12 @@ module WebAuthn
|
|
|
20
20
|
def encode(data)
|
|
21
21
|
case encoding
|
|
22
22
|
when :base64
|
|
23
|
-
Base64.strict_encode64(data)
|
|
23
|
+
[data].pack("m0") # Base64.strict_encode64(data)
|
|
24
24
|
when :base64url
|
|
25
|
-
Base64.urlsafe_encode64(data, padding: false)
|
|
25
|
+
data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false)
|
|
26
|
+
data.chomp!("==") or data.chomp!("=")
|
|
27
|
+
data.tr!("+/", "-_")
|
|
28
|
+
data
|
|
26
29
|
when nil, false
|
|
27
30
|
data
|
|
28
31
|
else
|
|
@@ -33,9 +36,15 @@ module WebAuthn
|
|
|
33
36
|
def decode(data)
|
|
34
37
|
case encoding
|
|
35
38
|
when :base64
|
|
36
|
-
Base64.strict_decode64(data)
|
|
39
|
+
data.unpack1("m0") # Base64.strict_decode64(data)
|
|
37
40
|
when :base64url
|
|
38
|
-
Base64.urlsafe_decode64(data)
|
|
41
|
+
if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data)
|
|
42
|
+
data = data.ljust((data.length + 3) & ~3, "=")
|
|
43
|
+
data.tr!("-_", "+/")
|
|
44
|
+
else
|
|
45
|
+
data = data.tr("-_", "+/")
|
|
46
|
+
end
|
|
47
|
+
data.unpack1("m0")
|
|
39
48
|
when nil, false
|
|
40
49
|
data
|
|
41
50
|
else
|
|
@@ -13,6 +13,8 @@ module WebAuthn
|
|
|
13
13
|
credential_key:,
|
|
14
14
|
user_present: true,
|
|
15
15
|
user_verified: false,
|
|
16
|
+
backup_eligibility: false,
|
|
17
|
+
backup_state: false,
|
|
16
18
|
attested_credential_data: true,
|
|
17
19
|
sign_count: 0,
|
|
18
20
|
extensions: nil
|
|
@@ -23,6 +25,8 @@ module WebAuthn
|
|
|
23
25
|
@credential_key = credential_key
|
|
24
26
|
@user_present = user_present
|
|
25
27
|
@user_verified = user_verified
|
|
28
|
+
@backup_eligibility = backup_eligibility
|
|
29
|
+
@backup_state = backup_state
|
|
26
30
|
@attested_credential_data = attested_credential_data
|
|
27
31
|
@sign_count = sign_count
|
|
28
32
|
@extensions = extensions
|
|
@@ -45,6 +49,8 @@ module WebAuthn
|
|
|
45
49
|
:credential_key,
|
|
46
50
|
:user_present,
|
|
47
51
|
:user_verified,
|
|
52
|
+
:backup_eligibility,
|
|
53
|
+
:backup_state,
|
|
48
54
|
:attested_credential_data,
|
|
49
55
|
:sign_count,
|
|
50
56
|
:extensions
|
|
@@ -63,6 +69,8 @@ module WebAuthn
|
|
|
63
69
|
credential: credential_data,
|
|
64
70
|
user_present: user_present,
|
|
65
71
|
user_verified: user_verified,
|
|
72
|
+
backup_eligibility: backup_eligibility,
|
|
73
|
+
backup_state: backup_state,
|
|
66
74
|
sign_count: 0,
|
|
67
75
|
extensions: extensions
|
|
68
76
|
)
|
|
@@ -20,6 +20,8 @@ module WebAuthn
|
|
|
20
20
|
sign_count: 0,
|
|
21
21
|
user_present: true,
|
|
22
22
|
user_verified: !user_present,
|
|
23
|
+
backup_eligibility: false,
|
|
24
|
+
backup_state: false,
|
|
23
25
|
aaguid: AAGUID,
|
|
24
26
|
extensions: { "fakeExtension" => "fakeExtensionValue" }
|
|
25
27
|
)
|
|
@@ -28,6 +30,8 @@ module WebAuthn
|
|
|
28
30
|
@sign_count = sign_count
|
|
29
31
|
@user_present = user_present
|
|
30
32
|
@user_verified = user_verified
|
|
33
|
+
@backup_eligibility = backup_eligibility
|
|
34
|
+
@backup_state = backup_state
|
|
31
35
|
@aaguid = aaguid
|
|
32
36
|
@extensions = extensions
|
|
33
37
|
end
|
|
@@ -38,7 +42,13 @@ module WebAuthn
|
|
|
38
42
|
|
|
39
43
|
private
|
|
40
44
|
|
|
41
|
-
attr_reader :rp_id_hash,
|
|
45
|
+
attr_reader :rp_id_hash,
|
|
46
|
+
:credential,
|
|
47
|
+
:user_present,
|
|
48
|
+
:user_verified,
|
|
49
|
+
:extensions,
|
|
50
|
+
:backup_eligibility,
|
|
51
|
+
:backup_state
|
|
42
52
|
|
|
43
53
|
def flags
|
|
44
54
|
[
|
|
@@ -46,8 +56,8 @@ module WebAuthn
|
|
|
46
56
|
bit(:user_present),
|
|
47
57
|
reserved_for_future_use_bit,
|
|
48
58
|
bit(:user_verified),
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
bit(:backup_eligibility),
|
|
60
|
+
bit(:backup_state),
|
|
51
61
|
reserved_for_future_use_bit,
|
|
52
62
|
attested_credential_data_included_bit,
|
|
53
63
|
extension_data_included_bit
|
|
@@ -108,7 +118,12 @@ module WebAuthn
|
|
|
108
118
|
end
|
|
109
119
|
|
|
110
120
|
def context
|
|
111
|
-
{
|
|
121
|
+
{
|
|
122
|
+
user_present: user_present,
|
|
123
|
+
user_verified: user_verified,
|
|
124
|
+
backup_eligibility: backup_eligibility,
|
|
125
|
+
backup_state: backup_state
|
|
126
|
+
}
|
|
112
127
|
end
|
|
113
128
|
|
|
114
129
|
def cose_credential_public_key
|
|
@@ -17,6 +17,8 @@ module WebAuthn
|
|
|
17
17
|
client_data_hash:,
|
|
18
18
|
user_present: true,
|
|
19
19
|
user_verified: false,
|
|
20
|
+
backup_eligibility: false,
|
|
21
|
+
backup_state: false,
|
|
20
22
|
attested_credential_data: true,
|
|
21
23
|
sign_count: nil,
|
|
22
24
|
extensions: nil
|
|
@@ -37,6 +39,8 @@ module WebAuthn
|
|
|
37
39
|
credential_key: credential_key,
|
|
38
40
|
user_present: user_present,
|
|
39
41
|
user_verified: user_verified,
|
|
42
|
+
backup_eligibility: backup_eligibility,
|
|
43
|
+
backup_state: backup_state,
|
|
40
44
|
attested_credential_data: attested_credential_data,
|
|
41
45
|
sign_count: sign_count,
|
|
42
46
|
extensions: extensions
|
|
@@ -48,6 +52,8 @@ module WebAuthn
|
|
|
48
52
|
client_data_hash:,
|
|
49
53
|
user_present: true,
|
|
50
54
|
user_verified: false,
|
|
55
|
+
backup_eligibility: false,
|
|
56
|
+
backup_state: false,
|
|
51
57
|
aaguid: AuthenticatorData::AAGUID,
|
|
52
58
|
sign_count: nil,
|
|
53
59
|
extensions: nil,
|
|
@@ -71,6 +77,8 @@ module WebAuthn
|
|
|
71
77
|
rp_id_hash: hashed(rp_id),
|
|
72
78
|
user_present: user_present,
|
|
73
79
|
user_verified: user_verified,
|
|
80
|
+
backup_eligibility: backup_eligibility,
|
|
81
|
+
backup_state: backup_state,
|
|
74
82
|
aaguid: aaguid,
|
|
75
83
|
credential: nil,
|
|
76
84
|
sign_count: sign_count || credential_sign_count,
|
data/lib/webauthn/fake_client.rb
CHANGED
|
@@ -10,7 +10,7 @@ module WebAuthn
|
|
|
10
10
|
class FakeClient
|
|
11
11
|
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
|
|
12
12
|
|
|
13
|
-
attr_reader :origin, :token_binding
|
|
13
|
+
attr_reader :origin, :token_binding, :encoding
|
|
14
14
|
|
|
15
15
|
def initialize(
|
|
16
16
|
origin = fake_origin,
|
|
@@ -29,6 +29,8 @@ module WebAuthn
|
|
|
29
29
|
rp_id: nil,
|
|
30
30
|
user_present: true,
|
|
31
31
|
user_verified: false,
|
|
32
|
+
backup_eligibility: false,
|
|
33
|
+
backup_state: false,
|
|
32
34
|
attested_credential_data: true,
|
|
33
35
|
extensions: nil
|
|
34
36
|
)
|
|
@@ -42,6 +44,8 @@ module WebAuthn
|
|
|
42
44
|
client_data_hash: client_data_hash,
|
|
43
45
|
user_present: user_present,
|
|
44
46
|
user_verified: user_verified,
|
|
47
|
+
backup_eligibility: backup_eligibility,
|
|
48
|
+
backup_state: backup_state,
|
|
45
49
|
attested_credential_data: attested_credential_data,
|
|
46
50
|
extensions: extensions
|
|
47
51
|
)
|
|
@@ -60,6 +64,7 @@ module WebAuthn
|
|
|
60
64
|
"type" => "public-key",
|
|
61
65
|
"id" => internal_encoder.encode(id),
|
|
62
66
|
"rawId" => encoder.encode(id),
|
|
67
|
+
"authenticatorAttachment" => 'platform',
|
|
63
68
|
"clientExtensionResults" => extensions,
|
|
64
69
|
"response" => {
|
|
65
70
|
"attestationObject" => encoder.encode(attestation_object),
|
|
@@ -72,6 +77,8 @@ module WebAuthn
|
|
|
72
77
|
rp_id: nil,
|
|
73
78
|
user_present: true,
|
|
74
79
|
user_verified: false,
|
|
80
|
+
backup_eligibility: false,
|
|
81
|
+
backup_state: true,
|
|
75
82
|
sign_count: nil,
|
|
76
83
|
extensions: nil,
|
|
77
84
|
user_handle: nil,
|
|
@@ -90,6 +97,8 @@ module WebAuthn
|
|
|
90
97
|
client_data_hash: client_data_hash,
|
|
91
98
|
user_present: user_present,
|
|
92
99
|
user_verified: user_verified,
|
|
100
|
+
backup_eligibility: backup_eligibility,
|
|
101
|
+
backup_state: backup_state,
|
|
93
102
|
sign_count: sign_count,
|
|
94
103
|
extensions: extensions,
|
|
95
104
|
allow_credentials: allow_credentials
|
|
@@ -100,6 +109,7 @@ module WebAuthn
|
|
|
100
109
|
"id" => internal_encoder.encode(assertion[:credential_id]),
|
|
101
110
|
"rawId" => encoder.encode(assertion[:credential_id]),
|
|
102
111
|
"clientExtensionResults" => extensions,
|
|
112
|
+
"authenticatorAttachment" => 'platform',
|
|
103
113
|
"response" => {
|
|
104
114
|
"clientDataJSON" => encoder.encode(client_data_json),
|
|
105
115
|
"authenticatorData" => encoder.encode(assertion[:authenticator_data]),
|
|
@@ -111,7 +121,7 @@ module WebAuthn
|
|
|
111
121
|
|
|
112
122
|
private
|
|
113
123
|
|
|
114
|
-
attr_reader :authenticator
|
|
124
|
+
attr_reader :authenticator
|
|
115
125
|
|
|
116
126
|
def data_json_for(method, challenge)
|
|
117
127
|
data = {
|
|
@@ -39,8 +39,8 @@ module WebAuthn
|
|
|
39
39
|
|
|
40
40
|
@rp =
|
|
41
41
|
if rp.is_a?(Hash)
|
|
42
|
-
rp[:name] ||=
|
|
43
|
-
rp[:id] ||=
|
|
42
|
+
rp[:name] ||= relying_party.name
|
|
43
|
+
rp[:id] ||= relying_party.id
|
|
44
44
|
|
|
45
45
|
RPEntity.new(**rp)
|
|
46
46
|
else
|
|
@@ -76,7 +76,7 @@ module WebAuthn
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def pub_key_cred_params_from_algs
|
|
79
|
-
Array(algs ||
|
|
79
|
+
Array(algs || relying_party.algorithms).map do |alg|
|
|
80
80
|
alg_id =
|
|
81
81
|
if alg.is_a?(String) || alg.is_a?(Symbol)
|
|
82
82
|
COSE::Algorithm.by_name(alg.to_s).id
|
|
@@ -8,11 +8,12 @@ module WebAuthn
|
|
|
8
8
|
class Options
|
|
9
9
|
CHALLENGE_LENGTH = 32
|
|
10
10
|
|
|
11
|
-
attr_reader :timeout, :extensions
|
|
11
|
+
attr_reader :timeout, :extensions, :relying_party
|
|
12
12
|
|
|
13
|
-
def initialize(timeout:
|
|
14
|
-
@
|
|
15
|
-
@
|
|
13
|
+
def initialize(timeout: nil, extensions: nil, relying_party: WebAuthn.configuration.relying_party)
|
|
14
|
+
@relying_party = relying_party
|
|
15
|
+
@timeout = timeout || default_timeout
|
|
16
|
+
@extensions = default_extensions.merge(extensions || {})
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def challenge
|
|
@@ -49,7 +50,7 @@ module WebAuthn
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def encoder
|
|
52
|
-
|
|
53
|
+
relying_party.encoder
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def raw_challenge
|
|
@@ -57,11 +58,11 @@ module WebAuthn
|
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def default_timeout
|
|
60
|
-
|
|
61
|
+
relying_party.credential_options_timeout
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
def
|
|
64
|
-
|
|
64
|
+
def default_extensions
|
|
65
|
+
{}
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def as_public_key_descriptors(ids)
|
|
@@ -10,7 +10,7 @@ module WebAuthn
|
|
|
10
10
|
def initialize(rp_id: nil, allow_credentials: nil, allow: nil, user_verification: nil, **keyword_arguments)
|
|
11
11
|
super(**keyword_arguments)
|
|
12
12
|
|
|
13
|
-
@rp_id = rp_id ||
|
|
13
|
+
@rp_id = rp_id || relying_party.id
|
|
14
14
|
@allow_credentials = allow_credentials
|
|
15
15
|
@allow = allow
|
|
16
16
|
@user_verification = user_verification
|
|
@@ -26,6 +26,16 @@ module WebAuthn
|
|
|
26
26
|
super.concat([:allow_credentials, :rp_id, :user_verification])
|
|
27
27
|
end
|
|
28
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
|
+
|
|
29
39
|
def allow_credentials_from_allow
|
|
30
40
|
if allow
|
|
31
41
|
as_public_key_descriptors(allow)
|
|
@@ -4,27 +4,47 @@ require "webauthn/encoder"
|
|
|
4
4
|
|
|
5
5
|
module WebAuthn
|
|
6
6
|
class PublicKeyCredential
|
|
7
|
-
|
|
7
|
+
class InvalidChallengeError < Error; end
|
|
8
8
|
|
|
9
|
-
|
|
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)
|
|
10
12
|
new(
|
|
11
13
|
type: credential["type"],
|
|
12
14
|
id: credential["id"],
|
|
13
|
-
raw_id:
|
|
15
|
+
raw_id: relying_party.encoder.decode(credential["rawId"]),
|
|
14
16
|
client_extension_outputs: credential["clientExtensionResults"],
|
|
15
|
-
|
|
17
|
+
authenticator_attachment: credential["authenticatorAttachment"],
|
|
18
|
+
response: response_class.from_client(credential["response"], relying_party: relying_party),
|
|
19
|
+
relying_party: relying_party
|
|
16
20
|
)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
def initialize(
|
|
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
|
+
)
|
|
20
32
|
@type = type
|
|
21
33
|
@id = id
|
|
22
34
|
@raw_id = raw_id
|
|
23
35
|
@client_extension_outputs = client_extension_outputs
|
|
36
|
+
@authenticator_attachment = authenticator_attachment
|
|
24
37
|
@response = response
|
|
38
|
+
@relying_party = relying_party
|
|
25
39
|
end
|
|
26
40
|
|
|
27
|
-
def verify(*_args)
|
|
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
|
+
|
|
28
48
|
valid_type? || raise("invalid type")
|
|
29
49
|
valid_id? || raise("invalid id")
|
|
30
50
|
|
|
@@ -39,8 +59,18 @@ module WebAuthn
|
|
|
39
59
|
authenticator_data.extension_data if authenticator_data&.extension_data_included?
|
|
40
60
|
end
|
|
41
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
|
+
|
|
42
70
|
private
|
|
43
71
|
|
|
72
|
+
attr_reader :relying_party
|
|
73
|
+
|
|
44
74
|
def valid_type?
|
|
45
75
|
type == TYPE_PUBLIC_KEY
|
|
46
76
|
end
|
|
@@ -49,12 +79,16 @@ module WebAuthn
|
|
|
49
79
|
raw_id && id && raw_id == WebAuthn.standard_encoder.decode(id)
|
|
50
80
|
end
|
|
51
81
|
|
|
82
|
+
def valid_class?(challenge)
|
|
83
|
+
challenge.is_a?(String)
|
|
84
|
+
end
|
|
85
|
+
|
|
52
86
|
def authenticator_data
|
|
53
87
|
response&.authenticator_data
|
|
54
88
|
end
|
|
55
89
|
|
|
56
90
|
def encoder
|
|
57
|
-
|
|
91
|
+
relying_party.encoder
|
|
58
92
|
end
|
|
59
93
|
end
|
|
60
94
|
end
|
|
@@ -16,7 +16,8 @@ module WebAuthn
|
|
|
16
16
|
encoder.decode(challenge),
|
|
17
17
|
public_key: encoder.decode(public_key),
|
|
18
18
|
sign_count: sign_count,
|
|
19
|
-
user_verification: user_verification
|
|
19
|
+
user_verification: user_verification,
|
|
20
|
+
rp_id: appid_extension_output ? appid : nil
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
true
|
|
@@ -31,5 +32,17 @@ module WebAuthn
|
|
|
31
32
|
def raw_user_handle
|
|
32
33
|
response.user_handle
|
|
33
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def appid_extension_output
|
|
39
|
+
return if client_extension_outputs.nil?
|
|
40
|
+
|
|
41
|
+
client_extension_outputs['appid']
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def appid
|
|
45
|
+
URI.parse(relying_party.legacy_u2f_appid || raise("Unspecified legacy U2F AppID")).to_s
|
|
46
|
+
end
|
|
34
47
|
end
|
|
35
48
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "webauthn/credential"
|
|
5
|
+
require "webauthn/encoder"
|
|
6
|
+
require "webauthn/error"
|
|
7
|
+
|
|
8
|
+
module WebAuthn
|
|
9
|
+
class RootCertificateFinderNotSupportedError < Error; end
|
|
10
|
+
|
|
11
|
+
class RelyingParty
|
|
12
|
+
def self.if_pss_supported(algorithm)
|
|
13
|
+
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(
|
|
19
|
+
algorithms: DEFAULT_ALGORITHMS.dup,
|
|
20
|
+
encoding: WebAuthn::Encoder::STANDARD_ENCODING,
|
|
21
|
+
origin: nil,
|
|
22
|
+
id: nil,
|
|
23
|
+
name: nil,
|
|
24
|
+
verify_attestation_statement: true,
|
|
25
|
+
credential_options_timeout: 120000,
|
|
26
|
+
silent_authentication: false,
|
|
27
|
+
acceptable_attestation_types: ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA', 'AnonCA'],
|
|
28
|
+
attestation_root_certificates_finders: [],
|
|
29
|
+
legacy_u2f_appid: nil
|
|
30
|
+
)
|
|
31
|
+
@algorithms = algorithms
|
|
32
|
+
@encoding = encoding
|
|
33
|
+
@origin = origin
|
|
34
|
+
@id = id
|
|
35
|
+
@name = name
|
|
36
|
+
@verify_attestation_statement = verify_attestation_statement
|
|
37
|
+
@credential_options_timeout = credential_options_timeout
|
|
38
|
+
@silent_authentication = silent_authentication
|
|
39
|
+
@acceptable_attestation_types = acceptable_attestation_types
|
|
40
|
+
@legacy_u2f_appid = legacy_u2f_appid
|
|
41
|
+
self.attestation_root_certificates_finders = attestation_root_certificates_finders
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_accessor :algorithms,
|
|
45
|
+
:encoding,
|
|
46
|
+
:origin,
|
|
47
|
+
:id,
|
|
48
|
+
:name,
|
|
49
|
+
:verify_attestation_statement,
|
|
50
|
+
:credential_options_timeout,
|
|
51
|
+
:silent_authentication,
|
|
52
|
+
:acceptable_attestation_types,
|
|
53
|
+
:legacy_u2f_appid
|
|
54
|
+
|
|
55
|
+
attr_reader :attestation_root_certificates_finders
|
|
56
|
+
|
|
57
|
+
# This is the user-data encoder.
|
|
58
|
+
# Used to decode user input and to encode data provided to the user.
|
|
59
|
+
def encoder
|
|
60
|
+
@encoder ||= WebAuthn::Encoder.new(encoding)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def attestation_root_certificates_finders=(finders)
|
|
64
|
+
if !finders.respond_to?(:each)
|
|
65
|
+
finders = [finders]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
finders.each do |finder|
|
|
69
|
+
unless finder.respond_to?(:find)
|
|
70
|
+
raise RootCertificateFinderNotSupportedError, "Finder must implement `find` method"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@attestation_root_certificates_finders = finders
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def options_for_registration(**keyword_arguments)
|
|
78
|
+
WebAuthn::Credential.options_for_create(
|
|
79
|
+
**keyword_arguments,
|
|
80
|
+
relying_party: self
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def verify_registration(raw_credential, challenge, user_verification: nil)
|
|
85
|
+
webauthn_credential = WebAuthn::Credential.from_create(raw_credential, relying_party: self)
|
|
86
|
+
|
|
87
|
+
if webauthn_credential.verify(challenge, user_verification: user_verification)
|
|
88
|
+
webauthn_credential
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def options_for_authentication(**keyword_arguments)
|
|
93
|
+
WebAuthn::Credential.options_for_get(
|
|
94
|
+
**keyword_arguments,
|
|
95
|
+
relying_party: self
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def verify_authentication(
|
|
100
|
+
raw_credential,
|
|
101
|
+
challenge,
|
|
102
|
+
user_verification: nil,
|
|
103
|
+
public_key: nil,
|
|
104
|
+
sign_count: nil
|
|
105
|
+
)
|
|
106
|
+
webauthn_credential = WebAuthn::Credential.from_get(raw_credential, relying_party: self)
|
|
107
|
+
|
|
108
|
+
stored_credential = yield(webauthn_credential) if block_given?
|
|
109
|
+
|
|
110
|
+
if webauthn_credential.verify(
|
|
111
|
+
challenge,
|
|
112
|
+
public_key: public_key || stored_credential.public_key,
|
|
113
|
+
sign_count: sign_count || stored_credential.sign_count,
|
|
114
|
+
user_verification: user_verification
|
|
115
|
+
)
|
|
116
|
+
block_given? ? [webauthn_credential, stored_credential] : webauthn_credential
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -31,7 +31,10 @@ module WebAuthn
|
|
|
31
31
|
@credential ||=
|
|
32
32
|
begin
|
|
33
33
|
hash = authenticator_data.send(:credential)
|
|
34
|
-
WebAuthn::AuthenticatorData::AttestedCredentialData::Credential.new(
|
|
34
|
+
WebAuthn::AuthenticatorData::AttestedCredentialData::Credential.new(
|
|
35
|
+
id: hash[:id],
|
|
36
|
+
public_key: hash[:public_key].serialize
|
|
37
|
+
)
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
|