webauthn 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e9c0949f662e12a84d1810c564633cd6feec64b1f4ed9352a19260a1840d6fc
4
- data.tar.gz: 30725d9ebd21ce4b77b548807a9c57014983e245183bc2c1179e7eb1580d0583
3
+ metadata.gz: 690e8753529b42c4acc5fa15f621c077ee9822c850aa19e8a322ea5d937dbb8d
4
+ data.tar.gz: c0aa00203643d32d5bcdc673eced0dbc4185b05ab262fef5aa9fcdbf073de5c4
5
5
  SHA512:
6
- metadata.gz: f0499fa6c77cc863dfd325ad84e7bf746377b18e6260cbad692add3ac3a1e1508b5652e1536ef9f72a2614026c95ab71a026a73b96212bd54ab4e088e3ad9dd5
7
- data.tar.gz: 122fd1cd5689b051a9c39744ee3d02dce099ff9f226d4b2278ae3baf2fe2aff0d1e6dc218bed9e19f3703eafb7a0e0d142055c01c5219449ac5f118a21b94813
6
+ metadata.gz: 66470f20b0365194b77753a664bdd759a204f62cf60c1dcabbf7181c472696c665e8946cf2c1073aad190cbeb3b6b557120aed8386525911cf65e25f5fb82762
7
+ data.tar.gz: baea59ad89ba252e6da4ec1ddce5394d3ea484d186e7d48740bae1d136e973e4a34114b91c34d64ea65bb4bee4d2a6468300057a5877dfab269296b16ae1a4b0
@@ -1,10 +1,9 @@
1
1
  dist: xenial
2
- sudo: false
3
2
  language: ruby
4
3
  cache: bundler
5
4
  rvm:
6
5
  - ruby-head
7
- - 2.6.0
6
+ - 2.6.1
8
7
  - 2.5.3
9
8
  - 2.4.5
10
9
  - 2.3.8
@@ -14,4 +13,11 @@ matrix:
14
13
  - rvm: ruby-head
15
14
 
16
15
  before_install:
16
+ - wget http://archive.ubuntu.com/ubuntu/pool/universe/f/faketime/libfaketime_0.9.7-3_amd64.deb
17
+ - sudo dpkg -i libfaketime_0.9.7-3_amd64.deb
17
18
  - gem install bundler -v "~> 2.0"
19
+
20
+ before_script:
21
+ - export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
22
+ - export DONT_FAKE_MONOTONIC=1
23
+ - export FAKETIME_NO_CACHE=1
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.9.0] - 2019-02-22
4
+
5
+ ### Added
6
+
7
+ - Added `#verify`, which can be used for getting a meaningful error raised in case of a verification error, as opposed to `#valid?` which returns `false`
8
+
3
9
  ## [v1.8.0] - 2019-01-17
4
10
 
5
11
  ### Added
@@ -115,6 +121,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
115
121
  - `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
116
122
  - Works with ruby 2.5
117
123
 
124
+ [v1.9.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.8.0...v1.9.0/
118
125
  [v1.8.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.7.0...v1.8.0/
119
126
  [v1.7.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.6.0...v1.7.0/
120
127
  [v1.6.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.5.0...v1.6.0/
data/README.md CHANGED
@@ -63,7 +63,7 @@ NOTE: You can find a working example on how to use this gem in a __Rails__ app i
63
63
  credential_creation_options = WebAuthn.credential_creation_options
64
64
 
65
65
  # Store the newly generated challenge somewhere so you can have it
66
- # for the validation phase.
66
+ # for the verification phase.
67
67
  #
68
68
  # You can read it from the resulting options:
69
69
  credential_creation_options[:challenge]
@@ -72,7 +72,7 @@ credential_creation_options[:challenge]
72
72
  # to call `navigator.credentials.create({ "publicKey": credentialCreationOptions })`
73
73
  ```
74
74
 
75
- #### Validation phase
75
+ #### Verification phase
76
76
 
77
77
  ```ruby
78
78
  # These should be ruby `String`s encoded as binary data, e.g. `Encoding:ASCII-8BIT`.
@@ -80,10 +80,10 @@ credential_creation_options[:challenge]
80
80
  # If the user-agent is a web browser, you would use some encoding algorithm to send what
81
81
  # `navigator.credentials.create` returned through the wire.
82
82
  #
83
- # Then you need to decode that data before passing it to the `#valid?` method.
83
+ # Then you need to decode that data before passing it to the `#verify` method.
84
84
  #
85
85
  # E.g. in https://github.com/cedarcode/webauthn-rails-demo-app we use `Base64.strict_decode64`
86
- # on the user-agent encoded data before calling `#valid`
86
+ # on the user-agent encoded data before calling `#verify`
87
87
  attestation_object = "..."
88
88
  client_data_json = "..."
89
89
 
@@ -93,17 +93,19 @@ attestation_response = WebAuthn::AuthenticatorAttestationResponse.new(
93
93
  )
94
94
 
95
95
  # This value needs to match `window.location.origin` evaluated by
96
- # the User Agent as part of the validation phase.
96
+ # the User Agent as part of the verification phase.
97
97
  original_origin = "https://www.example.com"
98
98
 
99
- if attestation_response.valid?(original_challenge, original_origin)
99
+ begin
100
+ attestation_response.verify(original_challenge, original_origin)
101
+
100
102
  # 1. Register the new user and
101
103
  # 2. Keep Credential ID and Credential Public Key under storage
102
104
  # for future authentications
103
105
  # Access by invoking:
104
106
  # `attestation_response.credential.id`
105
107
  # `attestation_response.credential.public_key`
106
- else
108
+ rescue WebAuthn::VerificationError => e
107
109
  # Handle error
108
110
  end
109
111
  ```
@@ -119,7 +121,7 @@ credential_request_options = WebAuthn.credential_request_options
119
121
  credential_request_options[:allowCredentials] << { id: credential_id, type: "public-key" }
120
122
 
121
123
  # Store the newly generated challenge somewhere so you can have it
122
- # for the validation phase.
124
+ # for the verification phase.
123
125
  #
124
126
  # You can read it from the resulting options:
125
127
  credential_request_options[:challenge]
@@ -128,7 +130,7 @@ credential_request_options[:challenge]
128
130
  # to call `navigator.credentials.get({ "publicKey": credentialRequestOptions })`
129
131
  ```
130
132
 
131
- #### Validation phase
133
+ #### Verification phase
132
134
 
133
135
  Assuming you have the previously stored Credential Public Key, now in variable `credential_public_key`
134
136
 
@@ -138,10 +140,10 @@ Assuming you have the previously stored Credential Public Key, now in variable `
138
140
  # If the user-agent is a web browser, you would use some encoding algorithm to send what
139
141
  # `navigator.credentials.get` returned through the wire.
140
142
  #
141
- # Then you need to decode that data before passing it to the `#valid?` method.
143
+ # Then you need to decode that data before passing it to the `#verify` method.
142
144
  #
143
145
  # E.g. in https://github.com/cedarcode/webauthn-rails-demo-app we use `Base64.strict_decode64`
144
- # on the user-agent encoded data before calling `#valid`
146
+ # on the user-agent encoded data before calling `#verify`
145
147
  authenticator_data = "..."
146
148
  client_data_json = "..."
147
149
  signature = "..."
@@ -153,7 +155,7 @@ assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
153
155
  )
154
156
 
155
157
  # This value needs to match `window.location.origin` evaluated by
156
- # the User Agent as part of the validation phase.
158
+ # the User Agent as part of the verification phase.
157
159
  original_origin = "https://www.example.com"
158
160
 
159
161
  # This hash must have the id and its corresponding public key of the
@@ -163,17 +165,28 @@ allowed_credential = {
163
165
  public_key: credential_public_key
164
166
  }
165
167
 
166
- if assertion_response.valid?(original_challenge, original_origin, allowed_credential: allowed_credential)
168
+ begin
169
+ assertion_response.verify(original_challenge, original_origin, allowed_credentials: [allowed_credential])
170
+
167
171
  # Sign in the user
168
- else
172
+ rescue WebAuthn::VerificationError => e
169
173
  # Handle error
170
174
  end
171
175
  ```
172
176
 
177
+ ## Testing Your Integration
178
+
179
+ The Webauthn spec requires for data that is signed and authenticated. As a result, it can be difficult to create valid test authenticator data when testing your integration. Webauthn-ruby exposes [WebAuthn::FakeAuthenticator](https://github.com/cedarcode/webauthn-ruby/blob/master/lib/webauthn/fake_authenticator.rb) for you to use in your tests. Example usage can be found in [webauthn-ruby/spec/webauthn/authenticator_assertion_response_spec.rb](https://github.com/cedarcode/webauthn-ruby/blob/master/spec/webauthn/authenticator_assertion_response_spec.rb).
180
+
173
181
  ## Development
174
182
 
175
183
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests and code-style checks. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
176
184
 
185
+ Some tests require stubbing time with [libfaketime](https://github.com/wolfcw/libfaketime) in order to pass, otherwise they're skipped. You can install this library with your package manager. Follow libfaketime's instructions for your OS to preload the library before running the tests, and use the `DONT_FAKE_MONOTONIC=1 FAKETIME_NO_CACHE=1` options. E.g. when installed via homebrew on macOS:
186
+ ```shell
187
+ DYLD_INSERT_LIBRARIES=/usr/local/Cellar/libfaketime/2.9.7_1/lib/faketime/libfaketime.1.dylib DYLD_FORCE_FLAT_NAMESPACE=1 DONT_FAKE_MONOTONIC=1 FAKETIME_NO_CACHE=1 bundle exec rspec
188
+ ```
189
+
177
190
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
178
191
 
179
192
  ### Commit message format
@@ -3,6 +3,9 @@
3
3
  require "webauthn/authenticator_response"
4
4
 
5
5
  module WebAuthn
6
+ class CredentialVerificationError < VerificationError; end
7
+ class SignatureVerificationError < VerificationError; end
8
+
6
9
  class AuthenticatorAssertionResponse < AuthenticatorResponse
7
10
  def initialize(credential_id:, authenticator_data:, signature:, **options)
8
11
  super(options)
@@ -12,10 +15,13 @@ module WebAuthn
12
15
  @signature = signature
13
16
  end
14
17
 
15
- def valid?(original_challenge, original_origin, allowed_credentials:, rp_id: nil)
16
- super(original_challenge, original_origin, rp_id: rp_id) &&
17
- valid_credential?(allowed_credentials) &&
18
- valid_signature?(credential_public_key(allowed_credentials))
18
+ def verify(original_challenge, original_origin, allowed_credentials:, rp_id: nil)
19
+ super(original_challenge, original_origin, rp_id: rp_id)
20
+
21
+ verify_item(:credential, allowed_credentials)
22
+ verify_item(:signature, credential_public_key(allowed_credentials))
23
+
24
+ true
19
25
  end
20
26
 
21
27
  def authenticator_data
@@ -10,6 +10,8 @@ require "webauthn/attestation_statement"
10
10
  require "webauthn/client_data"
11
11
 
12
12
  module WebAuthn
13
+ class AttestationStatementVerificationError < VerificationError; end
14
+
13
15
  class AuthenticatorAttestationResponse < AuthenticatorResponse
14
16
  attr_reader :attestation_type, :attestation_trust_path
15
17
 
@@ -19,14 +21,11 @@ module WebAuthn
19
21
  @attestation_object = attestation_object
20
22
  end
21
23
 
22
- def valid?(original_challenge, original_origin, rp_id: nil)
23
- valid_response = super
24
- return false unless valid_response
24
+ def verify(original_challenge, original_origin, rp_id: nil)
25
+ super
25
26
 
26
- valid_attestation = attestation_statement.valid?(authenticator_data, client_data.hash)
27
- return false unless valid_attestation
27
+ verify_item(:attestation_statement)
28
28
 
29
- @attestation_type, @attestation_trust_path = valid_attestation
30
29
  true
31
30
  end
32
31
 
@@ -58,5 +57,9 @@ module WebAuthn
58
57
  def type
59
58
  WebAuthn::TYPES[:create]
60
59
  end
60
+
61
+ def valid_attestation_statement?
62
+ @attestation_type, @attestation_trust_path = attestation_statement.valid?(authenticator_data, client_data.hash)
63
+ end
61
64
  end
62
65
  end
@@ -1,18 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "webauthn/error"
4
+
3
5
  module WebAuthn
6
+ class VerificationError < Error; end
7
+
8
+ class AuthenticatorDataVerificationError < VerificationError; end
9
+ class ChallengeVerificationError < VerificationError; end
10
+ class OriginVerificationError < VerificationError; end
11
+ class RpIdVerificationError < VerificationError; end
12
+ class TypeVerificationError < VerificationError; end
13
+ class UserPresenceVerificationError < VerificationError; end
14
+
4
15
  class AuthenticatorResponse
5
16
  def initialize(client_data_json:)
6
17
  @client_data_json = client_data_json
7
18
  end
8
19
 
9
- def valid?(original_challenge, original_origin, rp_id: nil)
10
- valid_type? &&
11
- valid_challenge?(original_challenge) &&
12
- valid_origin?(original_origin) &&
13
- valid_rp_id?(rp_id || rp_id_from_origin(original_origin)) &&
14
- authenticator_data.valid? &&
15
- authenticator_data.user_flagged?
20
+ def verify(original_challenge, original_origin, rp_id: nil)
21
+ verify_item(:type)
22
+ verify_item(:challenge, original_challenge)
23
+ verify_item(:origin, original_origin)
24
+ verify_item(:authenticator_data)
25
+ verify_item(:rp_id, rp_id || rp_id_from_origin(original_origin))
26
+ verify_item(:user_presence)
27
+
28
+ true
29
+ end
30
+
31
+ def valid?(*args)
32
+ verify(*args)
33
+ rescue WebAuthn::VerificationError
34
+ false
16
35
  end
17
36
 
18
37
  def client_data
@@ -23,6 +42,16 @@ module WebAuthn
23
42
 
24
43
  attr_reader :client_data_json
25
44
 
45
+ def verify_item(item, *args)
46
+ if send("valid_#{item}?", *args)
47
+ true
48
+ else
49
+ camelized_item = item.to_s.split('_').map { |w| w.capitalize }.join
50
+ error_const_name = "WebAuthn::#{camelized_item}VerificationError"
51
+ raise Object.const_get(error_const_name)
52
+ end
53
+ end
54
+
26
55
  def valid_type?
27
56
  client_data.type == type
28
57
  end
@@ -39,6 +68,14 @@ module WebAuthn
39
68
  OpenSSL::Digest::SHA256.digest(rp_id) == authenticator_data.rp_id_hash
40
69
  end
41
70
 
71
+ def valid_authenticator_data?
72
+ authenticator_data.valid?
73
+ end
74
+
75
+ def valid_user_presence?
76
+ authenticator_data.user_flagged?
77
+ end
78
+
42
79
  def rp_id_from_origin(original_origin)
43
80
  URI.parse(original_origin).host
44
81
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securecompare"
4
+
3
5
  module WebAuthn
4
6
  module SecurityUtils
5
7
  # Constant time string comparison, for variable length strings.
@@ -10,26 +12,9 @@ module WebAuthn
10
12
  def secure_compare(first_string, second_string)
11
13
  first_string_sha256 = ::Digest::SHA256.hexdigest(first_string)
12
14
  second_string_sha256 = ::Digest::SHA256.hexdigest(second_string)
13
- fixed_length_secure_compare(first_string_sha256, second_string_sha256) && first_string == second_string
14
- end
15
- module_function :secure_compare
16
15
 
17
- private
18
-
19
- # Constant time string comparison, for fixed length strings.
20
- # This code was adapted from Rails ActiveSupport::SecurityUtils
21
- #
22
- # The values compared should be of fixed length, such as strings
23
- # that have already been processed by HMAC. Raises in case of length mismatch.
24
- def fixed_length_secure_compare(first_string, second_string)
25
- raise ArgumentError, "string length mismatch." unless first_string.bytesize == second_string.bytesize
26
-
27
- l = first_string.unpack "C#{first_string.bytesize}"
28
-
29
- res = 0
30
- second_string.each_byte { |byte| res |= byte ^ l.shift }
31
- res == 0
16
+ SecureCompare.compare(first_string_sha256, second_string_sha256) && first_string == second_string
32
17
  end
33
- module_function :fixed_length_secure_compare
18
+ module_function :secure_compare
34
19
  end
35
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebAuthn
4
- VERSION = "1.8.0"
4
+ VERSION = "1.9.0"
5
5
  end
@@ -33,10 +33,11 @@ Gem::Specification.new do |spec|
33
33
  spec.add_dependency "cose", "~> 0.1.0"
34
34
  spec.add_dependency "jwt", [">= 1.5", "< 3.0"]
35
35
  spec.add_dependency "openssl", "~> 2.0"
36
+ spec.add_dependency "securecompare", "~> 1.0"
36
37
 
37
38
  spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
38
- spec.add_development_dependency "byebug", "~> 10.0"
39
+ spec.add_development_dependency "byebug", "~> 11.0"
39
40
  spec.add_development_dependency "rake", "~> 12.3"
40
41
  spec.add_development_dependency "rspec", "~> 3.8"
41
- spec.add_development_dependency "rubocop", "0.61.1"
42
+ spec.add_development_dependency "rubocop", "0.65.0"
42
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webauthn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gonzalo Rodriguez
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-01-17 00:00:00.000000000 Z
12
+ date: 2019-02-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cbor
@@ -73,6 +73,20 @@ dependencies:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: '2.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: securecompare
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
76
90
  - !ruby/object:Gem::Dependency
77
91
  name: bundler
78
92
  requirement: !ruby/object:Gem::Requirement
@@ -99,14 +113,14 @@ dependencies:
99
113
  requirements:
100
114
  - - "~>"
101
115
  - !ruby/object:Gem::Version
102
- version: '10.0'
116
+ version: '11.0'
103
117
  type: :development
104
118
  prerelease: false
105
119
  version_requirements: !ruby/object:Gem::Requirement
106
120
  requirements:
107
121
  - - "~>"
108
122
  - !ruby/object:Gem::Version
109
- version: '10.0'
123
+ version: '11.0'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: rake
112
126
  requirement: !ruby/object:Gem::Requirement
@@ -141,14 +155,14 @@ dependencies:
141
155
  requirements:
142
156
  - - '='
143
157
  - !ruby/object:Gem::Version
144
- version: 0.61.1
158
+ version: 0.65.0
145
159
  type: :development
146
160
  prerelease: false
147
161
  version_requirements: !ruby/object:Gem::Requirement
148
162
  requirements:
149
163
  - - '='
150
164
  - !ruby/object:Gem::Version
151
- version: 0.61.1
165
+ version: 0.65.0
152
166
  description:
153
167
  email:
154
168
  - gonzalo@cedarcode.com