webauthn 1.18.0 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -0
- data/.travis.yml +7 -3
- data/Appraisals +8 -0
- data/CHANGELOG.md +52 -0
- data/README.md +88 -80
- data/SECURITY.md +18 -0
- data/gemfiles/cose_head.gemfile +7 -0
- data/gemfiles/openssl_head.gemfile +7 -0
- data/lib/webauthn.rb +9 -1
- data/lib/webauthn/attestation_statement/android_safetynet.rb +4 -4
- data/lib/webauthn/attestation_statement/base.rb +4 -4
- data/lib/webauthn/attestation_statement/fido_u2f.rb +1 -2
- data/lib/webauthn/authenticator_assertion_response.rb +33 -35
- data/lib/webauthn/authenticator_attestation_response.rb +30 -0
- data/lib/webauthn/authenticator_data.rb +3 -1
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +1 -0
- data/lib/webauthn/authenticator_response.rb +1 -2
- data/lib/webauthn/client_data.rb +2 -1
- data/lib/webauthn/configuration.rb +9 -0
- data/lib/webauthn/credential.rb +26 -0
- data/lib/webauthn/credential_creation_options.rb +5 -1
- data/lib/webauthn/credential_request_options.rb +5 -0
- data/lib/webauthn/encoder.rb +8 -1
- data/lib/webauthn/fake_authenticator.rb +1 -0
- data/lib/webauthn/fake_client.rb +26 -22
- data/lib/webauthn/public_key_credential.rb +10 -50
- data/lib/webauthn/public_key_credential/creation_options.rb +92 -0
- data/lib/webauthn/public_key_credential/entity.rb +44 -0
- data/lib/webauthn/public_key_credential/options.rb +72 -0
- data/lib/webauthn/public_key_credential/request_options.rb +36 -0
- data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
- data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
- data/lib/webauthn/public_key_credential_with_assertion.rb +35 -0
- data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
- data/lib/webauthn/u2f_migrator.rb +1 -1
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +3 -2
- metadata +33 -8
- data/webauthn-ruby.png +0 -0
@@ -1,50 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "base64"
|
4
|
-
require "webauthn/authenticator_assertion_response"
|
5
|
-
require "webauthn/authenticator_attestation_response"
|
6
3
|
require "webauthn/encoder"
|
7
4
|
|
8
5
|
module WebAuthn
|
9
6
|
class PublicKeyCredential
|
10
|
-
VALID_TYPE = "public-key"
|
11
|
-
|
12
7
|
attr_reader :type, :id, :raw_id, :response
|
13
8
|
|
14
|
-
def self.
|
15
|
-
encoder = WebAuthn::Encoder.new(encoding)
|
16
|
-
|
17
|
-
new(
|
18
|
-
type: credential["type"],
|
19
|
-
id: credential["id"],
|
20
|
-
raw_id: encoder.decode(credential["rawId"]),
|
21
|
-
response: WebAuthn::AuthenticatorAttestationResponse.new(
|
22
|
-
attestation_object: encoder.decode(credential["response"]["attestationObject"]),
|
23
|
-
client_data_json: encoder.decode(credential["response"]["clientDataJSON"])
|
24
|
-
)
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.from_get(credential, encoding: :base64)
|
29
|
-
encoder = WebAuthn::Encoder.new(encoding)
|
30
|
-
|
31
|
-
user_handle =
|
32
|
-
if credential["response"]["userHandle"]
|
33
|
-
encoder.decode(credential["response"]["userHandle"])
|
34
|
-
end
|
35
|
-
|
9
|
+
def self.from_client(credential)
|
36
10
|
new(
|
37
11
|
type: credential["type"],
|
38
12
|
id: credential["id"],
|
39
|
-
raw_id: encoder.decode(credential["rawId"]),
|
40
|
-
response:
|
41
|
-
# FIXME: credential_id doesn't belong inside AuthenticatorAssertionResponse
|
42
|
-
credential_id: Base64.urlsafe_decode64(credential["id"]),
|
43
|
-
authenticator_data: encoder.decode(credential["response"]["authenticatorData"]),
|
44
|
-
client_data_json: encoder.decode(credential["response"]["clientDataJSON"]),
|
45
|
-
signature: encoder.decode(credential["response"]["signature"]),
|
46
|
-
user_handle: user_handle
|
47
|
-
)
|
13
|
+
raw_id: WebAuthn.configuration.encoder.decode(credential["rawId"]),
|
14
|
+
response: response_class.from_client(credential["response"])
|
48
15
|
)
|
49
16
|
end
|
50
17
|
|
@@ -55,24 +22,13 @@ module WebAuthn
|
|
55
22
|
@response = response
|
56
23
|
end
|
57
24
|
|
58
|
-
def verify(*
|
25
|
+
def verify(*_args)
|
59
26
|
valid_type? || raise("invalid type")
|
60
27
|
valid_id? || raise("invalid id")
|
61
|
-
response.verify(*args)
|
62
28
|
|
63
29
|
true
|
64
30
|
end
|
65
31
|
|
66
|
-
def public_key
|
67
|
-
response&.authenticator_data&.credential&.public_key
|
68
|
-
end
|
69
|
-
|
70
|
-
def user_handle
|
71
|
-
if response.is_a?(WebAuthn::AuthenticatorAssertionResponse)
|
72
|
-
response.user_handle
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
32
|
def sign_count
|
77
33
|
response&.authenticator_data&.sign_count
|
78
34
|
end
|
@@ -80,11 +36,15 @@ module WebAuthn
|
|
80
36
|
private
|
81
37
|
|
82
38
|
def valid_type?
|
83
|
-
type ==
|
39
|
+
type == TYPE_PUBLIC_KEY
|
84
40
|
end
|
85
41
|
|
86
42
|
def valid_id?
|
87
|
-
raw_id && id && raw_id ==
|
43
|
+
raw_id && id && raw_id == WebAuthn.standard_encoder.decode(id)
|
44
|
+
end
|
45
|
+
|
46
|
+
def encoder
|
47
|
+
WebAuthn.configuration.encoder
|
88
48
|
end
|
89
49
|
end
|
90
50
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cose/algorithm"
|
4
|
+
require "webauthn/public_key_credential/options"
|
5
|
+
require "webauthn/public_key_credential/rp_entity"
|
6
|
+
require "webauthn/public_key_credential/user_entity"
|
7
|
+
|
8
|
+
module WebAuthn
|
9
|
+
class PublicKeyCredential
|
10
|
+
class CreationOptions < Options
|
11
|
+
attr_accessor(
|
12
|
+
:attestation,
|
13
|
+
:authenticator_selection,
|
14
|
+
:exclude,
|
15
|
+
:algs,
|
16
|
+
:rp,
|
17
|
+
:user
|
18
|
+
)
|
19
|
+
|
20
|
+
def initialize(
|
21
|
+
attestation: nil,
|
22
|
+
authenticator_selection: nil,
|
23
|
+
exclude_credentials: nil,
|
24
|
+
exclude: nil,
|
25
|
+
pub_key_cred_params: nil,
|
26
|
+
algs: nil,
|
27
|
+
rp: {},
|
28
|
+
user:,
|
29
|
+
**keyword_arguments
|
30
|
+
)
|
31
|
+
super(**keyword_arguments)
|
32
|
+
|
33
|
+
@attestation = attestation
|
34
|
+
@authenticator_selection = authenticator_selection
|
35
|
+
@exclude_credentials = exclude_credentials
|
36
|
+
@exclude = exclude
|
37
|
+
@pub_key_cred_params = pub_key_cred_params
|
38
|
+
@algs = algs
|
39
|
+
|
40
|
+
@rp =
|
41
|
+
if rp.is_a?(Hash)
|
42
|
+
rp[:name] ||= configuration.rp_name
|
43
|
+
rp[:id] ||= configuration.rp_id
|
44
|
+
|
45
|
+
RPEntity.new(rp)
|
46
|
+
else
|
47
|
+
rp
|
48
|
+
end
|
49
|
+
|
50
|
+
@user =
|
51
|
+
if user.is_a?(Hash)
|
52
|
+
UserEntity.new(user)
|
53
|
+
else
|
54
|
+
user
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def exclude_credentials
|
59
|
+
@exclude_credentials || exclude_credentials_from_exclude
|
60
|
+
end
|
61
|
+
|
62
|
+
def pub_key_cred_params
|
63
|
+
@pub_key_cred_params || pub_key_cred_params_from_algs
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def attributes
|
69
|
+
super.concat([:rp, :user, :pub_key_cred_params, :attestation, :authenticator_selection, :exclude_credentials])
|
70
|
+
end
|
71
|
+
|
72
|
+
def exclude_credentials_from_exclude
|
73
|
+
if exclude
|
74
|
+
as_public_key_descriptors(exclude)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def pub_key_cred_params_from_algs
|
79
|
+
Array(algs || configuration.algorithms).map do |alg|
|
80
|
+
alg_id =
|
81
|
+
if alg.is_a?(String) || alg.is_a?(Symbol)
|
82
|
+
COSE::Algorithm.by_name(alg.to_s).id
|
83
|
+
else
|
84
|
+
alg
|
85
|
+
end
|
86
|
+
|
87
|
+
{ type: TYPE_PUBLIC_KEY, alg: alg_id }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "awrence"
|
4
|
+
|
5
|
+
module WebAuthn
|
6
|
+
class PublicKeyCredential
|
7
|
+
class Entity
|
8
|
+
attr_reader :name, :icon
|
9
|
+
|
10
|
+
def initialize(name:, icon: nil)
|
11
|
+
@name = name
|
12
|
+
@icon = icon
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json
|
16
|
+
to_hash.to_camelback_keys
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
hash = {}
|
23
|
+
|
24
|
+
attributes.each do |attribute_name|
|
25
|
+
value = send(attribute_name)
|
26
|
+
|
27
|
+
if value.respond_to?(:as_json)
|
28
|
+
value = value.as_json
|
29
|
+
end
|
30
|
+
|
31
|
+
if value
|
32
|
+
hash[attribute_name] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def attributes
|
40
|
+
[:name, :icon]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "awrence"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
module WebAuthn
|
7
|
+
class PublicKeyCredential
|
8
|
+
class Options
|
9
|
+
CHALLENGE_LENGTH = 32
|
10
|
+
|
11
|
+
attr_reader :timeout, :extensions
|
12
|
+
|
13
|
+
def initialize(timeout: default_timeout, extensions: nil)
|
14
|
+
@timeout = timeout
|
15
|
+
@extensions = extensions
|
16
|
+
end
|
17
|
+
|
18
|
+
def challenge
|
19
|
+
encoder.encode(raw_challenge)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Argument wildcard for Ruby on Rails controller automatic object JSON serialization
|
23
|
+
def as_json(*)
|
24
|
+
to_hash.to_camelback_keys
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
hash = {}
|
31
|
+
|
32
|
+
attributes.each do |attribute_name|
|
33
|
+
value = send(attribute_name)
|
34
|
+
|
35
|
+
if value.respond_to?(:as_json)
|
36
|
+
value = value.as_json
|
37
|
+
end
|
38
|
+
|
39
|
+
if value
|
40
|
+
hash[attribute_name] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def attributes
|
48
|
+
[:challenge, :timeout, :extensions]
|
49
|
+
end
|
50
|
+
|
51
|
+
def encoder
|
52
|
+
WebAuthn.configuration.encoder
|
53
|
+
end
|
54
|
+
|
55
|
+
def raw_challenge
|
56
|
+
@raw_challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH)
|
57
|
+
end
|
58
|
+
|
59
|
+
def default_timeout
|
60
|
+
configuration.credential_options_timeout
|
61
|
+
end
|
62
|
+
|
63
|
+
def configuration
|
64
|
+
WebAuthn.configuration
|
65
|
+
end
|
66
|
+
|
67
|
+
def as_public_key_descriptors(ids)
|
68
|
+
Array(ids).map { |id| { type: TYPE_PUBLIC_KEY, id: id } }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/public_key_credential/options"
|
4
|
+
|
5
|
+
module WebAuthn
|
6
|
+
class PublicKeyCredential
|
7
|
+
class RequestOptions < Options
|
8
|
+
attr_accessor :rp_id, :allow, :user_verification
|
9
|
+
|
10
|
+
def initialize(rp_id: nil, allow_credentials: nil, allow: nil, user_verification: nil, **keyword_arguments)
|
11
|
+
super(**keyword_arguments)
|
12
|
+
|
13
|
+
@rp_id = rp_id || configuration.rp_id
|
14
|
+
@allow_credentials = allow_credentials
|
15
|
+
@allow = allow
|
16
|
+
@user_verification = user_verification
|
17
|
+
end
|
18
|
+
|
19
|
+
def allow_credentials
|
20
|
+
@allow_credentials || allow_credentials_from_allow || []
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def attributes
|
26
|
+
super.concat([:allow_credentials, :rp_id, :user_verification])
|
27
|
+
end
|
28
|
+
|
29
|
+
def allow_credentials_from_allow
|
30
|
+
if allow
|
31
|
+
as_public_key_descriptors(allow)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/public_key_credential/entity"
|
4
|
+
|
5
|
+
module WebAuthn
|
6
|
+
class PublicKeyCredential
|
7
|
+
class RPEntity < Entity
|
8
|
+
attr_reader :id
|
9
|
+
|
10
|
+
def initialize(id: nil, **keyword_arguments)
|
11
|
+
super(**keyword_arguments)
|
12
|
+
|
13
|
+
@id = id
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
super.concat([:id])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/public_key_credential/entity"
|
4
|
+
|
5
|
+
module WebAuthn
|
6
|
+
class PublicKeyCredential
|
7
|
+
class UserEntity < Entity
|
8
|
+
attr_reader :id, :display_name
|
9
|
+
|
10
|
+
def initialize(id:, display_name: nil, **keyword_arguments)
|
11
|
+
super(**keyword_arguments)
|
12
|
+
|
13
|
+
@id = id
|
14
|
+
@display_name = display_name || name
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def attributes
|
20
|
+
super.concat([:id, :display_name])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/authenticator_assertion_response"
|
4
|
+
require "webauthn/public_key_credential"
|
5
|
+
|
6
|
+
module WebAuthn
|
7
|
+
class PublicKeyCredentialWithAssertion < PublicKeyCredential
|
8
|
+
def self.response_class
|
9
|
+
WebAuthn::AuthenticatorAssertionResponse
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify(challenge, public_key:, sign_count:, user_verification: nil)
|
13
|
+
super
|
14
|
+
|
15
|
+
response.verify(
|
16
|
+
encoder.decode(challenge),
|
17
|
+
public_key: encoder.decode(public_key),
|
18
|
+
sign_count: sign_count,
|
19
|
+
user_verification: user_verification
|
20
|
+
)
|
21
|
+
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_handle
|
26
|
+
if raw_user_handle
|
27
|
+
encoder.encode(raw_user_handle)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def raw_user_handle
|
32
|
+
response.user_handle
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/authenticator_attestation_response"
|
4
|
+
require "webauthn/public_key_credential"
|
5
|
+
|
6
|
+
module WebAuthn
|
7
|
+
class PublicKeyCredentialWithAttestation < PublicKeyCredential
|
8
|
+
def self.response_class
|
9
|
+
WebAuthn::AuthenticatorAttestationResponse
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify(challenge, user_verification: nil)
|
13
|
+
super
|
14
|
+
|
15
|
+
response.verify(encoder.decode(challenge), user_verification: user_verification)
|
16
|
+
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def public_key
|
21
|
+
if raw_public_key
|
22
|
+
encoder.encode(raw_public_key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def raw_public_key
|
27
|
+
response&.authenticator_data&.credential&.public_key
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|