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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +65 -13
  4. data/.travis.yml +22 -18
  5. data/Appraisals +4 -0
  6. data/CHANGELOG.md +72 -25
  7. data/CONTRIBUTING.md +0 -5
  8. data/README.md +172 -15
  9. data/SECURITY.md +4 -4
  10. data/gemfiles/openssl_2_2.gemfile +7 -0
  11. data/lib/cose/rsapkcs1_algorithm.rb +43 -0
  12. data/lib/webauthn/attestation_object.rb +43 -0
  13. data/lib/webauthn/attestation_statement.rb +20 -20
  14. data/lib/webauthn/attestation_statement/android_key.rb +28 -30
  15. data/lib/webauthn/attestation_statement/android_safetynet.rb +30 -20
  16. data/lib/webauthn/attestation_statement/base.rb +124 -14
  17. data/lib/webauthn/attestation_statement/fido_u2f.rb +13 -9
  18. data/lib/webauthn/attestation_statement/packed.rb +14 -42
  19. data/lib/webauthn/attestation_statement/tpm.rb +38 -54
  20. data/lib/webauthn/authenticator_assertion_response.rb +7 -36
  21. data/lib/webauthn/authenticator_attestation_response.rb +24 -46
  22. data/lib/webauthn/authenticator_data.rb +51 -51
  23. data/lib/webauthn/authenticator_data/attested_credential_data.rb +29 -50
  24. data/lib/webauthn/authenticator_response.rb +15 -10
  25. data/lib/webauthn/configuration.rb +23 -0
  26. data/lib/webauthn/credential.rb +4 -4
  27. data/lib/webauthn/credential_creation_options.rb +1 -1
  28. data/lib/webauthn/fake_authenticator.rb +7 -3
  29. data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
  30. data/lib/webauthn/fake_authenticator/authenticator_data.rb +2 -4
  31. data/lib/webauthn/fake_client.rb +17 -4
  32. data/lib/webauthn/public_key.rb +68 -0
  33. data/lib/webauthn/public_key_credential.rb +13 -3
  34. data/lib/webauthn/public_key_credential/creation_options.rb +2 -2
  35. data/lib/webauthn/u2f_migrator.rb +5 -4
  36. data/lib/webauthn/version.rb +1 -1
  37. data/script/ci/install-openssl +7 -0
  38. data/script/ci/install-ruby +13 -0
  39. data/webauthn.gemspec +14 -9
  40. metadata +70 -42
  41. data/lib/android_safetynet/attestation_response.rb +0 -84
  42. data/lib/cose/algorithm.rb +0 -38
  43. data/lib/tpm/constants.rb +0 -22
  44. data/lib/tpm/s_attest.rb +0 -26
  45. data/lib/tpm/s_attest/s_certify_info.rb +0 -14
  46. data/lib/tpm/sized_buffer.rb +0 -13
  47. data/lib/tpm/t_public.rb +0 -32
  48. data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
  49. data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
  50. data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
  51. data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
  52. data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
  53. data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
  54. 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 AuthenticatorData
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
- SIGN_COUNT_POSITION = RP_ID_HASH_LENGTH + FLAGS_LENGTH
15
+ endian :big
14
16
 
15
- USER_PRESENT_FLAG_POSITION = 0
16
- USER_VERIFIED_FLAG_POSITION = 2
17
- ATTESTED_CREDENTIAL_DATA_INCLUDED_FLAG_POSITION = 6
18
- EXTENSION_DATA_INCLUDED_FLAG_POSITION = 7
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 initialize(data)
21
- @data = data
33
+ def self.deserialize(data)
34
+ read(data)
35
+ rescue EOFError
36
+ raise AuthenticatorDataFormatError
22
37
  end
23
38
 
24
- attr_reader :data
39
+ def data
40
+ to_binary_s
41
+ end
25
42
 
26
43
  def valid?
27
- valid_length? &&
28
- (!attested_credential_data_included? || attested_credential_data.valid?) &&
29
- (!extension_data_included? || extension_data)
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[USER_PRESENT_FLAG_POSITION] == "1"
54
+ flags.user_present == 1
38
55
  end
39
56
 
40
57
  def user_verified?
41
- flags[USER_VERIFIED_FLAG_POSITION] == "1"
58
+ flags.user_verified == 1
42
59
  end
43
60
 
44
61
  def attested_credential_data_included?
45
- flags[ATTESTED_CREDENTIAL_DATA_INCLUDED_FLAG_POSITION] == "1"
62
+ flags.attested_credential_data_included == 1
46
63
  end
47
64
 
48
65
  def extension_data_included?
49
- flags[EXTENSION_DATA_INCLUDED_FLAG_POSITION] == "1"
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.new(data_at(attested_credential_data_position))
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 flags
79
- @flags ||= data_at(flags_position, FLAGS_LENGTH).unpack("b*")[0]
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
- data.length == base_length + attested_credential_data_length + extension_data_length
97
+ data_length == base_length + attested_credential_data_length + extension_data_length
86
98
  end
87
99
 
88
100
  def raw_extension_data
89
- data_at(extension_data_position)
90
- end
91
-
92
- def attested_credential_data_position
93
- base_length
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 AuthenticatorData
7
- class AttestedCredentialData
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
- UINT16_BIG_ENDIAN_FORMAT = "n*"
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
- # FIXME: use keyword_init when we dropped Ruby 2.4 support
16
- Credential = Struct.new(:id, :public_key) do
17
- def public_key_object
18
- COSE::Key.deserialize(public_key).to_pkey
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 initialize(data)
23
- @data = data
33
+ def self.deserialize(data)
34
+ read(data)
35
+ rescue EOFError
36
+ raise AttestedCredentialDataFormatError
24
37
  end
25
38
 
26
39
  def valid?
27
- data.length >= AAGUID_LENGTH + ID_LENGTH_LENGTH && valid_credential_public_key?
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 id
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
- public_key_position + public_key_length
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
- @public_key ||= data_at(public_key_position, public_key_length)
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(data_at(public_key_position))).each.first).length
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
- verify_item(:user_presence)
37
- verify_item(:user_verified, user_verification)
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?(user_verification)
95
- if user_verification
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
@@ -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(*args)
11
- WebAuthn::PublicKeyCredential::CreationOptions.new(*args)
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(*args)
15
- WebAuthn::PublicKeyCredential::RequestOptions.new(*args)
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)
@@ -71,7 +71,7 @@ module WebAuthn
71
71
  end
72
72
 
73
73
  def pub_key_cred_params
74
- WebAuthn.configuration.algorithms.map do |alg_name|
74
+ configuration.algorithms.map do |alg_name|
75
75
  { type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id }
76
76
  end
77
77
  end
@@ -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)