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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/.travis.yml +7 -3
  4. data/Appraisals +8 -0
  5. data/CHANGELOG.md +52 -0
  6. data/README.md +88 -80
  7. data/SECURITY.md +18 -0
  8. data/gemfiles/cose_head.gemfile +7 -0
  9. data/gemfiles/openssl_head.gemfile +7 -0
  10. data/lib/webauthn.rb +9 -1
  11. data/lib/webauthn/attestation_statement/android_safetynet.rb +4 -4
  12. data/lib/webauthn/attestation_statement/base.rb +4 -4
  13. data/lib/webauthn/attestation_statement/fido_u2f.rb +1 -2
  14. data/lib/webauthn/authenticator_assertion_response.rb +33 -35
  15. data/lib/webauthn/authenticator_attestation_response.rb +30 -0
  16. data/lib/webauthn/authenticator_data.rb +3 -1
  17. data/lib/webauthn/authenticator_data/attested_credential_data.rb +1 -0
  18. data/lib/webauthn/authenticator_response.rb +1 -2
  19. data/lib/webauthn/client_data.rb +2 -1
  20. data/lib/webauthn/configuration.rb +9 -0
  21. data/lib/webauthn/credential.rb +26 -0
  22. data/lib/webauthn/credential_creation_options.rb +5 -1
  23. data/lib/webauthn/credential_request_options.rb +5 -0
  24. data/lib/webauthn/encoder.rb +8 -1
  25. data/lib/webauthn/fake_authenticator.rb +1 -0
  26. data/lib/webauthn/fake_client.rb +26 -22
  27. data/lib/webauthn/public_key_credential.rb +10 -50
  28. data/lib/webauthn/public_key_credential/creation_options.rb +92 -0
  29. data/lib/webauthn/public_key_credential/entity.rb +44 -0
  30. data/lib/webauthn/public_key_credential/options.rb +72 -0
  31. data/lib/webauthn/public_key_credential/request_options.rb +36 -0
  32. data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
  33. data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
  34. data/lib/webauthn/public_key_credential_with_assertion.rb +35 -0
  35. data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
  36. data/lib/webauthn/u2f_migrator.rb +1 -1
  37. data/lib/webauthn/version.rb +1 -1
  38. data/webauthn.gemspec +3 -2
  39. metadata +33 -8
  40. 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.from_create(credential, encoding: :base64)
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: WebAuthn::AuthenticatorAssertionResponse.new(
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(*args)
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 == VALID_TYPE
39
+ type == TYPE_PUBLIC_KEY
84
40
  end
85
41
 
86
42
  def valid_id?
87
- raw_id && id && raw_id == Base64.urlsafe_decode64(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