webauthn 2.3.0 → 3.0.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +32 -0
  3. data/.github/workflows/git.yml +21 -0
  4. data/.rubocop.yml +57 -1
  5. data/CHANGELOG.md +79 -0
  6. data/README.md +8 -5
  7. data/SECURITY.md +6 -3
  8. data/docs/advanced_configuration.md +174 -0
  9. data/docs/u2f_migration.md +14 -20
  10. data/lib/cose/rsapkcs1_algorithm.rb +7 -0
  11. data/lib/webauthn/attestation_object.rb +9 -5
  12. data/lib/webauthn/attestation_statement/android_key.rb +0 -4
  13. data/lib/webauthn/attestation_statement/android_safetynet.rb +1 -5
  14. data/lib/webauthn/attestation_statement/apple.rb +65 -0
  15. data/lib/webauthn/attestation_statement/base.rb +18 -32
  16. data/lib/webauthn/attestation_statement/none.rb +7 -1
  17. data/lib/webauthn/attestation_statement/packed.rb +1 -1
  18. data/lib/webauthn/attestation_statement/tpm.rb +2 -2
  19. data/lib/webauthn/attestation_statement.rb +6 -3
  20. data/lib/webauthn/authenticator_assertion_response.rb +4 -3
  21. data/lib/webauthn/authenticator_attestation_response.rb +10 -7
  22. data/lib/webauthn/authenticator_data/attested_credential_data.rb +10 -5
  23. data/lib/webauthn/authenticator_data.rb +10 -2
  24. data/lib/webauthn/authenticator_response.rb +7 -7
  25. data/lib/webauthn/configuration.rb +38 -42
  26. data/lib/webauthn/credential.rb +5 -4
  27. data/lib/webauthn/credential_creation_options.rb +2 -0
  28. data/lib/webauthn/credential_request_options.rb +2 -0
  29. data/lib/webauthn/fake_authenticator/attestation_object.rb +8 -0
  30. data/lib/webauthn/fake_authenticator/authenticator_data.rb +20 -5
  31. data/lib/webauthn/fake_authenticator.rb +19 -3
  32. data/lib/webauthn/fake_client.rb +20 -5
  33. data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
  34. data/lib/webauthn/public_key_credential/entity.rb +3 -4
  35. data/lib/webauthn/public_key_credential/options.rb +9 -8
  36. data/lib/webauthn/public_key_credential/request_options.rb +11 -1
  37. data/lib/webauthn/public_key_credential.rb +24 -5
  38. data/lib/webauthn/public_key_credential_with_assertion.rb +14 -1
  39. data/lib/webauthn/relying_party.rb +120 -0
  40. data/lib/webauthn/u2f_migrator.rb +4 -1
  41. data/lib/webauthn/version.rb +1 -1
  42. data/webauthn.gemspec +7 -8
  43. metadata +40 -59
  44. data/.travis.yml +0 -39
  45. data/Appraisals +0 -21
  46. data/gemfiles/cose_head.gemfile +0 -7
  47. data/gemfiles/openssl_2_0.gemfile +0 -7
  48. data/gemfiles/openssl_2_1.gemfile +0 -7
  49. data/gemfiles/openssl_2_2.gemfile +0 -7
  50. data/gemfiles/openssl_head.gemfile +0 -7
  51. data/lib/webauthn/security_utils.rb +0 -20
  52. data/script/ci/install-openssl +0 -7
  53. data/script/ci/install-ruby +0 -13
@@ -15,11 +15,13 @@ module WebAuthn
15
15
  rp_id_hash:,
16
16
  credential: {
17
17
  id: SecureRandom.random_bytes(16),
18
- public_key: OpenSSL::PKey::EC.new("prime256v1").generate_key.public_key
18
+ public_key: OpenSSL::PKey::EC.generate("prime256v1").public_key
19
19
  },
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, :credential, :user_present, :user_verified, :extensions
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
- reserved_for_future_use_bit,
50
- reserved_for_future_use_bit,
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
- { user_present: user_present, user_verified: user_verified }
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,14 +52,24 @@ 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
- extensions: nil
59
+ extensions: nil,
60
+ allow_credentials: nil
54
61
  )
55
62
  credential_options = credentials[rp_id]
56
63
 
57
64
  if credential_options
58
- credential_id, credential = credential_options.first
65
+ allow_credentials ||= credential_options.keys
66
+ credential_id = (credential_options.keys & allow_credentials).first
67
+ unless credential_id
68
+ raise "No matching credentials (allowed=#{allow_credentials}) " \
69
+ "found for RP #{rp_id} among credentials=#{credential_options}"
70
+ end
71
+
72
+ credential = credential_options[credential_id]
59
73
  credential_key = credential[:credential_key]
60
74
  credential_sign_count = credential[:sign_count]
61
75
 
@@ -63,6 +77,8 @@ module WebAuthn
63
77
  rp_id_hash: hashed(rp_id),
64
78
  user_present: user_present,
65
79
  user_verified: user_verified,
80
+ backup_eligibility: backup_eligibility,
81
+ backup_state: backup_state,
66
82
  aaguid: aaguid,
67
83
  credential: nil,
68
84
  sign_count: sign_count || credential_sign_count,
@@ -87,7 +103,7 @@ module WebAuthn
87
103
  attr_reader :credentials
88
104
 
89
105
  def new_credential
90
- [SecureRandom.random_bytes(16), OpenSSL::PKey::EC.new("prime256v1").generate_key, 0]
106
+ [SecureRandom.random_bytes(16), OpenSSL::PKey::EC.generate("prime256v1"), 0]
91
107
  end
92
108
 
93
109
  def hashed(target)
@@ -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
  )
@@ -72,20 +76,31 @@ module WebAuthn
72
76
  rp_id: nil,
73
77
  user_present: true,
74
78
  user_verified: false,
79
+ backup_eligibility: false,
80
+ backup_state: true,
75
81
  sign_count: nil,
76
- extensions: nil)
82
+ extensions: nil,
83
+ user_handle: nil,
84
+ allow_credentials: nil)
77
85
  rp_id ||= URI.parse(origin).host
78
86
 
79
87
  client_data_json = data_json_for(:get, encoder.decode(challenge))
80
88
  client_data_hash = hashed(client_data_json)
81
89
 
90
+ if allow_credentials
91
+ allow_credentials = allow_credentials.map { |credential| encoder.decode(credential) }
92
+ end
93
+
82
94
  assertion = authenticator.get_assertion(
83
95
  rp_id: rp_id,
84
96
  client_data_hash: client_data_hash,
85
97
  user_present: user_present,
86
98
  user_verified: user_verified,
99
+ backup_eligibility: backup_eligibility,
100
+ backup_state: backup_state,
87
101
  sign_count: sign_count,
88
- extensions: extensions
102
+ extensions: extensions,
103
+ allow_credentials: allow_credentials
89
104
  )
90
105
 
91
106
  {
@@ -97,14 +112,14 @@ module WebAuthn
97
112
  "clientDataJSON" => encoder.encode(client_data_json),
98
113
  "authenticatorData" => encoder.encode(assertion[:authenticator_data]),
99
114
  "signature" => encoder.encode(assertion[:signature]),
100
- "userHandle" => nil
115
+ "userHandle" => user_handle ? encoder.encode(user_handle) : nil
101
116
  }
102
117
  }
103
118
  end
104
119
 
105
120
  private
106
121
 
107
- attr_reader :authenticator, :encoding
122
+ attr_reader :authenticator
108
123
 
109
124
  def data_json_for(method, challenge)
110
125
  data = {
@@ -39,8 +39,8 @@ module WebAuthn
39
39
 
40
40
  @rp =
41
41
  if rp.is_a?(Hash)
42
- rp[:name] ||= configuration.rp_name
43
- rp[:id] ||= configuration.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 || configuration.algorithms).map do |alg|
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
@@ -5,11 +5,10 @@ require "awrence"
5
5
  module WebAuthn
6
6
  class PublicKeyCredential
7
7
  class Entity
8
- attr_reader :name, :icon
8
+ attr_reader :name
9
9
 
10
- def initialize(name:, icon: nil)
10
+ def initialize(name:)
11
11
  @name = name
12
- @icon = icon
13
12
  end
14
13
 
15
14
  def as_json
@@ -37,7 +36,7 @@ module WebAuthn
37
36
  end
38
37
 
39
38
  def attributes
40
- [:name, :icon]
39
+ [:name]
41
40
  end
42
41
  end
43
42
  end
@@ -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: default_timeout, extensions: nil)
14
- @timeout = timeout
15
- @extensions = extensions
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
- WebAuthn.configuration.encoder
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
- configuration.credential_options_timeout
61
+ relying_party.credential_options_timeout
61
62
  end
62
63
 
63
- def configuration
64
- WebAuthn.configuration
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 || configuration.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)
@@ -6,22 +6,31 @@ module WebAuthn
6
6
  class PublicKeyCredential
7
7
  attr_reader :type, :id, :raw_id, :client_extension_outputs, :response
8
8
 
9
- def self.from_client(credential)
9
+ def self.from_client(credential, relying_party: WebAuthn.configuration.relying_party)
10
10
  new(
11
11
  type: credential["type"],
12
12
  id: credential["id"],
13
- raw_id: WebAuthn.configuration.encoder.decode(credential["rawId"]),
13
+ raw_id: relying_party.encoder.decode(credential["rawId"]),
14
14
  client_extension_outputs: credential["clientExtensionResults"],
15
- response: response_class.from_client(credential["response"])
15
+ response: response_class.from_client(credential["response"], relying_party: relying_party),
16
+ relying_party: relying_party
16
17
  )
17
18
  end
18
19
 
19
- def initialize(type:, id:, raw_id:, client_extension_outputs: {}, response:)
20
+ def initialize(
21
+ type:,
22
+ id:,
23
+ raw_id:,
24
+ response:,
25
+ client_extension_outputs: {},
26
+ relying_party: WebAuthn.configuration.relying_party
27
+ )
20
28
  @type = type
21
29
  @id = id
22
30
  @raw_id = raw_id
23
31
  @client_extension_outputs = client_extension_outputs
24
32
  @response = response
33
+ @relying_party = relying_party
25
34
  end
26
35
 
27
36
  def verify(*_args)
@@ -39,8 +48,18 @@ module WebAuthn
39
48
  authenticator_data.extension_data if authenticator_data&.extension_data_included?
40
49
  end
41
50
 
51
+ def backup_eligible?
52
+ authenticator_data&.credential_backup_eligible?
53
+ end
54
+
55
+ def backed_up?
56
+ authenticator_data&.credential_backed_up?
57
+ end
58
+
42
59
  private
43
60
 
61
+ attr_reader :relying_party
62
+
44
63
  def valid_type?
45
64
  type == TYPE_PUBLIC_KEY
46
65
  end
@@ -54,7 +73,7 @@ module WebAuthn
54
73
  end
55
74
 
56
75
  def encoder
57
- WebAuthn.configuration.encoder
76
+ relying_party.encoder
58
77
  end
59
78
  end
60
79
  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(hash[:id], hash[:public_key].serialize)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebAuthn
4
- VERSION = "2.3.0"
4
+ VERSION = "3.0.0"
5
5
  end
data/webauthn.gemspec CHANGED
@@ -31,23 +31,22 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- spec.required_ruby_version = ">= 2.4"
34
+ spec.required_ruby_version = ">= 2.5"
35
35
 
36
36
  spec.add_dependency "android_key_attestation", "~> 0.3.0"
37
37
  spec.add_dependency "awrence", "~> 1.1"
38
38
  spec.add_dependency "bindata", "~> 2.4"
39
39
  spec.add_dependency "cbor", "~> 0.5.9"
40
- spec.add_dependency "cose", "~> 1.0"
41
- spec.add_dependency "openssl", "~> 2.0"
40
+ spec.add_dependency "cose", "~> 1.1"
41
+ spec.add_dependency "openssl", ">= 2.2"
42
42
  spec.add_dependency "safety_net_attestation", "~> 0.4.0"
43
- spec.add_dependency "securecompare", "~> 1.0"
44
- spec.add_dependency "tpm-key_attestation", "~> 0.9.0"
43
+ spec.add_dependency "tpm-key_attestation", "~> 0.12.0"
45
44
 
46
- spec.add_development_dependency "appraisal", "~> 2.3.0"
47
45
  spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
48
46
  spec.add_development_dependency "byebug", "~> 11.0"
49
47
  spec.add_development_dependency "rake", "~> 13.0"
50
48
  spec.add_development_dependency "rspec", "~> 3.8"
51
- spec.add_development_dependency "rubocop", "0.80.1"
52
- spec.add_development_dependency "rubocop-rspec", "~> 1.38.1"
49
+ spec.add_development_dependency "rubocop", "~> 1.9.1"
50
+ spec.add_development_dependency "rubocop-rake", "~> 0.5.1"
51
+ spec.add_development_dependency "rubocop-rspec", "~> 2.2.0"
53
52
  end