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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +113 -13
  3. data/.travis.yml +21 -18
  4. data/Appraisals +4 -0
  5. data/CHANGELOG.md +41 -0
  6. data/CONTRIBUTING.md +0 -5
  7. data/README.md +70 -8
  8. data/SECURITY.md +6 -4
  9. data/gemfiles/openssl_2_2.gemfile +7 -0
  10. data/lib/cose/rsapkcs1_algorithm.rb +50 -0
  11. data/lib/webauthn/attestation_object.rb +43 -0
  12. data/lib/webauthn/attestation_statement.rb +20 -20
  13. data/lib/webauthn/attestation_statement/android_key.rb +28 -30
  14. data/lib/webauthn/attestation_statement/android_safetynet.rb +27 -7
  15. data/lib/webauthn/attestation_statement/base.rb +108 -10
  16. data/lib/webauthn/attestation_statement/fido_u2f.rb +8 -6
  17. data/lib/webauthn/attestation_statement/none.rb +7 -1
  18. data/lib/webauthn/attestation_statement/packed.rb +13 -41
  19. data/lib/webauthn/attestation_statement/tpm.rb +38 -75
  20. data/lib/webauthn/authenticator_assertion_response.rb +3 -7
  21. data/lib/webauthn/authenticator_attestation_response.rb +19 -84
  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 +3 -0
  25. data/lib/webauthn/credential_creation_options.rb +2 -0
  26. data/lib/webauthn/credential_request_options.rb +2 -0
  27. data/lib/webauthn/fake_authenticator.rb +7 -3
  28. data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
  29. data/lib/webauthn/fake_authenticator/authenticator_data.rb +2 -4
  30. data/lib/webauthn/fake_client.rb +19 -5
  31. data/lib/webauthn/public_key.rb +21 -2
  32. data/lib/webauthn/public_key_credential.rb +13 -3
  33. data/lib/webauthn/u2f_migrator.rb +5 -4
  34. data/lib/webauthn/version.rb +1 -1
  35. data/script/ci/install-openssl +7 -0
  36. data/script/ci/install-ruby +13 -0
  37. data/webauthn.gemspec +13 -9
  38. metadata +54 -41
  39. data/lib/android_safetynet/attestation_response.rb +0 -116
  40. data/lib/cose/rsassa_algorithm.rb +0 -10
  41. data/lib/tpm/constants.rb +0 -44
  42. data/lib/tpm/s_attest.rb +0 -26
  43. data/lib/tpm/s_attest/s_certify_info.rb +0 -14
  44. data/lib/tpm/sized_buffer.rb +0 -13
  45. data/lib/tpm/t_public.rb +0 -32
  46. data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
  47. data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
  48. data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
  49. data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
  50. data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
  51. data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
  52. 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 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"
@@ -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?
@@ -32,6 +32,8 @@ module WebAuthn
32
32
  user_display_name: nil,
33
33
  rp_name: nil
34
34
  )
35
+ super()
36
+
35
37
  @attestation = attestation
36
38
  @authenticator_selection = authenticator_selection
37
39
  @exclude_credentials = exclude_credentials
@@ -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
- # FIXME: Remove once writer in cose
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
- # FIXME: Remove once writer in cose
129
- key.instance_variable_set(:@alg, alg[key.crv])
127
+ key.alg = alg[key.crv]
130
128
 
131
129
  end
132
130
 
@@ -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.new(CBOR.decode(attestation_object)["authData"]).credential.id
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, rp_id: nil, user_present: true, user_verified: false, sign_count: nil)
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