webauthn 2.0.0 → 2.1.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: 2bd2d85eb0ce4769f5c4fe9529dedb4cd8bb25c4322bfb95bd4d9d623ab58b00
4
- data.tar.gz: 6b0a3a7c524d7bd4db88d6c94968a165801374708ceac00f24e981c8599cbfe4
3
+ metadata.gz: 78367d9a528b780d4e53a29667fe47cf95d03698c976714fc7d71d46b253f4fa
4
+ data.tar.gz: ab4e9b0ef6a0bfcb9bd2c33d8220e0f3241f2ba949c57053d52968b6e34a5a61
5
5
  SHA512:
6
- metadata.gz: ebf401f24d784c4beb0adbcb98203de34612eb47bd6af002dc5cb02709bc258078ca92439d5917fef28f8ff869de6e02c41bbe3301f16f2472d76b0cf93662ec
7
- data.tar.gz: 8e1d8f03cad6d75658c4116d7b6117539ba828cbfea07dbb9a584a29387f55519c9a8eed332b4602a75ed0d7691a9bd412e7f9241b9993b0dff123b69cec086c
6
+ metadata.gz: f9a3514276b45c34c5509e7d55a6238d8a705ab19534a5ca3a802be4a00f42784b81c3dbc02f3c991918bb61da55b6f20c268a0573b8099224d9467d13932a60
7
+ data.tar.gz: e337a703e30a984b881d589f5c31a653e312a9ae4395cf2c411408e0f162871622c7f3d212412ea01de4db121a1ddf2431000ae55968358062f513b5feef812c
data/.gitignore CHANGED
@@ -13,3 +13,4 @@
13
13
  /Gemfile.lock
14
14
  /gemfiles/*.gemfile.lock
15
15
  .byebug_history
16
+ /spec/conformance/metadata.zip
@@ -4,6 +4,7 @@ cache: bundler
4
4
 
5
5
  rvm:
6
6
  - ruby-head
7
+ - 2.7.0
7
8
  - 2.6.5
8
9
  - 2.5.7
9
10
  - 2.4.9
@@ -22,12 +23,14 @@ matrix:
22
23
  - gemfile: gemfiles/cose_head.gemfile
23
24
  - gemfile: gemfiles/openssl_head.gemfile
24
25
 
26
+ addons:
27
+ apt:
28
+ packages:
29
+ - libfaketime
30
+
25
31
  before_install:
26
- - wget http://archive.ubuntu.com/ubuntu/pool/universe/f/faketime/libfaketime_0.9.7-3_amd64.deb
27
- - sudo dpkg -i libfaketime_0.9.7-3_amd64.deb
28
32
  - gem install bundler -v "~> 2.0"
29
33
 
30
34
  before_script:
31
35
  - export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
32
- - export DONT_FAKE_MONOTONIC=1
33
36
  - export FAKETIME_NO_CACHE=1
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.1.0] - 2019-12-30
4
+
5
+ ### Added
6
+
7
+ - Ability to convert stored credential public key back to a ruby object with `WebAuthn::PublicKey.deserialize(stored_public_key)`, included the validation during de-serialization ([@ssuttner], [@padulafacundo])
8
+ - Improved TPM attestation validation by checking "Subject Alternative Name" ([@bdewater])
9
+ - Improved SafetyNet attestation validation by checking timestamp ([@padulafacundo])
10
+ - [EXPERIMENTAL] Ability to optionally "Assess the attestation trustworthiness" during registration by setting `acceptable_attestation_types` and `attestation_root_certificates_finders` configuration values ([@padulafacundo])
11
+ - Ruby 2.7 support without warnings
12
+
13
+ Note: Expect possible breaking changes for "EXPERIMENTAL" features.
14
+
3
15
  ## [v2.0.0] - 2019-10-03
4
16
 
5
17
  ### Added
@@ -13,7 +25,7 @@
13
25
  - All the above automatically handle encoding/decoding for necessary values. The specific encoding scheme can
14
26
  be set (or even turned off) in `WebAutnn.configuration.encoding=`. Defaults to `:base64url`.
15
27
  - `WebAuthn::FakeClient#get` better fakes a real client by including `userHandle` in the returned hash.
16
- - Expose AAGUID and attestationCertificateKey for MDS lookup during attestation (@bdwater)
28
+ - Expose AAGUID and attestationCertificateKey for MDS lookup during attestation ([@bdewater])
17
29
 
18
30
  ### Changed
19
31
 
@@ -56,23 +68,23 @@ returned base64url-encoded `id` value.
56
68
 
57
69
  ### Added
58
70
 
59
- - Ability to migrate U2F credentials to WebAuthn ([#211](https://github.com/cedarcode/webauthn-ruby/pull/211)) (@bdewater + @jdongelmans)
60
- - Ability to skip attestation statement verification ([#219](https://github.com/cedarcode/webauthn-ruby/pull/219)) (@MaximeNdutiye)
61
- - Ability to configure default credential options timeout ([#243](https://github.com/cedarcode/webauthn-ruby/pull/243)) (@MaximeNdutiye)
71
+ - Ability to migrate U2F credentials to WebAuthn ([#211](https://github.com/cedarcode/webauthn-ruby/pull/211)) ([@bdewater] + [@jdongelmans])
72
+ - Ability to skip attestation statement verification ([#219](https://github.com/cedarcode/webauthn-ruby/pull/219)) ([@MaximeNdutiye])
73
+ - Ability to configure default credential options timeout ([#243](https://github.com/cedarcode/webauthn-ruby/pull/243)) ([@MaximeNdutiye])
62
74
  - AttestedCredentialData presence verification ([#237](https://github.com/cedarcode/webauthn-ruby/pull/237))
63
75
  - FakeClient learns how to increment sign count ([#225](https://github.com/cedarcode/webauthn-ruby/pull/225))
64
76
 
65
77
  ### Fixed
66
78
 
67
- - Properly verify SafetyNet certificates from input ([#233](https://github.com/cedarcode/webauthn-ruby/pull/233)) (@bdewater)
68
- - FakeClient default origin URL ([#242](https://github.com/cedarcode/webauthn-ruby/pull/242)) (@kalebtesfay)
79
+ - Properly verify SafetyNet certificates from input ([#233](https://github.com/cedarcode/webauthn-ruby/pull/233)) ([@bdewater])
80
+ - FakeClient default origin URL ([#242](https://github.com/cedarcode/webauthn-ruby/pull/242)) ([@kalebtesfay])
69
81
 
70
82
  ## [v1.17.0] - 2019-06-18
71
83
 
72
84
  ### Added
73
85
 
74
- - Support ES384, ES512, PS384, PS512, RS384 and RS512 credentials. Off by default. Enable by adding any of them to `WebAuthn.configuration.algorithms` array. Thank you @bdewater.
75
- - Support [Signature Counter](https://www.w3.org/TR/webauthn/#signature-counter) verification. Thank you @bdewater.
86
+ - Support ES384, ES512, PS384, PS512, RS384 and RS512 credentials. Off by default. Enable by adding any of them to `WebAuthn.configuration.algorithms` array ([@bdewater])
87
+ - Support [Signature Counter](https://www.w3.org/TR/webauthn/#signature-counter) verification ([@bdewater])
76
88
 
77
89
  ## [v1.16.0] - 2019-06-13
78
90
 
@@ -80,7 +92,7 @@ returned base64url-encoded `id` value.
80
92
 
81
93
  - Ability to enforce [user verification](https://www.w3.org/TR/webauthn/#user-verification) with extra argument in the `#verify` method.
82
94
  - Support RS1 (RSA w/ SHA-1) credentials. Off by default. Enable by adding `"RS1"` to `WebAuthn.configuration.algorithms` array.
83
- - Support PS256 (RSA Probabilistic Signature Scheme w/ SHA-256) credentials. On by default. Thank you @bdewater.
95
+ - Support PS256 (RSA Probabilistic Signature Scheme w/ SHA-256) credentials. On by default ([@bdewater])
84
96
 
85
97
  ## [v1.15.0] - 2019-05-16
86
98
 
@@ -102,11 +114,11 @@ returned base64url-encoded `id` value.
102
114
  - Verify 'none' attestation statement is really empty.
103
115
  - Verify 'packed' attestation statement certificates start/end dates.
104
116
  - Verify 'packed' attestation statement signature algorithm.
105
- - Verify 'fiod-u2f attestation statement AAGUID is zeroed out. Thank you @bdewater.
117
+ - Verify 'fiod-u2f attestation statement AAGUID is zeroed out ([@bdewater])
106
118
  - Verify 'android-key' attestation statement signature algorithm.
107
119
  - Verify assertion response signature algorithm.
108
120
  - Verify collectedClientData.tokenBinding format.
109
- - `WebAuthn.credential_creation_options` now accept `rp_name`, `user_id`, `user_name` and `display_name` as keyword arguments. Thank you @bdewater.
121
+ - `WebAuthn.credential_creation_options` now accept `rp_name`, `user_id`, `user_name` and `display_name` as keyword arguments ([@bdewater])
110
122
 
111
123
  ## [v1.12.0] - 2019-04-03
112
124
 
@@ -128,11 +140,11 @@ Note #2: You don't need to do any convesion before passing the public key in `Au
128
140
 
129
141
  ### Added
130
142
 
131
- - `WebAuthn::AuthenticatorAttestationResponse#verify` supports `android-key` attestation statements. Thank you @bdewater!
143
+ - `WebAuthn::AuthenticatorAttestationResponse#verify` supports `android-key` attestation statements ([@bdewater])
132
144
 
133
145
  ### Fixed
134
146
 
135
- - Verify matching AAGUID if needed when verifying `packed` attestation statements. Thank you @bdewater!
147
+ - Verify matching AAGUID if needed when verifying `packed` attestation statements ([@bdewater])
136
148
 
137
149
  ## [v1.10.0] - 2019-03-05
138
150
 
@@ -150,7 +162,7 @@ Note #2: You don't need to do any convesion before passing the public key in `Au
150
162
 
151
163
  ### Added
152
164
 
153
- - Make challenge validation inside `#valid?` method resistant to timing attacks. Thank you @tomek-bt!
165
+ - Make challenge validation inside `#valid?` method resistant to timing attacks (@tomek-bt)
154
166
  - Support for ruby 2.6
155
167
 
156
168
  ### Changed
@@ -162,7 +174,7 @@ Note #2: You don't need to do any convesion before passing the public key in `Au
162
174
  ### Added
163
175
 
164
176
  - _Registration_ ceremony
165
- - `WebAuthn::AuthenticatorAttestationResponse` exposes attestation type and trust path via `#attestation_type` and `#attestation_trust_path` methods. Thank you @bdewater!
177
+ - `WebAuthn::AuthenticatorAttestationResponse` exposes attestation type and trust path via `#attestation_type` and `#attestation_trust_path` methods ([@bdewater])
166
178
 
167
179
  ## [v1.6.0] - 2018-11-01
168
180
 
@@ -174,21 +186,21 @@ Note #2: You don't need to do any convesion before passing the public key in `Au
174
186
 
175
187
  ### Added
176
188
 
177
- - Works with ruby 2.3. Thank you @bdewater!
189
+ - Works with ruby 2.3 ([@bdewater])
178
190
 
179
191
  ## [v1.4.0] - 2018-10-11
180
192
 
181
193
  ### Added
182
194
 
183
195
  - _Registration_ ceremony
184
- - `WebAuthn::AuthenticatorAttestationResponse.valid?` supports `android-safetynet` attestation statements. Thank you @bdewater!
196
+ - `WebAuthn::AuthenticatorAttestationResponse.valid?` supports `android-safetynet` attestation statements ([@bdewater])
185
197
 
186
198
  ## [v1.3.0] - 2018-10-11
187
199
 
188
200
  ### Added
189
201
 
190
202
  - _Registration_ ceremony
191
- - `WebAuthn::AuthenticatorAttestationResponse.valid?` supports `packed` attestation statements. Thank you @sorah!
203
+ - `WebAuthn::AuthenticatorAttestationResponse.valid?` supports `packed` attestation statements ([@sorah])
192
204
 
193
205
  ## [v1.2.0] - 2018-10-08
194
206
 
@@ -206,7 +218,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
206
218
  ### Added
207
219
 
208
220
  - _Registration_ ceremony
209
- - `WebAuthn::AuthenticatorAttestationResponse.valid?` optionally accepts rp_id. Thank you @sorah!
221
+ - `WebAuthn::AuthenticatorAttestationResponse.valid?` optionally accepts rp_id ([@sorah])
210
222
  - _Authentication_ ceremony
211
223
  - `WebAuthn::AuthenticatorAssertionResponse.valid?` optionally accepts rp_id.
212
224
 
@@ -261,6 +273,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
261
273
  - `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
262
274
  - Works with ruby 2.5
263
275
 
276
+ [v2.1.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.0.0...v2.1.0/
264
277
  [v2.0.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.18.0...v2.0.0/
265
278
  [v1.18.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.17.0...v1.18.0/
266
279
  [v1.17.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.16.0...v1.17.0/
@@ -283,3 +296,11 @@ Note: Both additions should help making it compatible with Chrome for Android 70
283
296
  [v1.0.0]: https://github.com/cedarcode/webauthn-ruby/compare/v0.2.0...v1.0.0/
284
297
  [v0.2.0]: https://github.com/cedarcode/webauthn-ruby/compare/v0.1.0...v0.2.0/
285
298
  [v0.1.0]: https://github.com/cedarcode/webauthn-ruby/compare/v0.0.0...v0.1.0/
299
+
300
+ [@bdewater]: https://github.com/bdewater
301
+ [@jdongelmans]: https://github.com/jdongelmans
302
+ [@kalebtesfay]: https://github.com/kalebtesfay
303
+ [@MaximeNdutiye]: https://github.com/MaximeNdutiye
304
+ [@sorah]: https://github.com/sorah
305
+ [@ssuttner]: https://github.com/ssuttner
306
+ [@padulafacundo]: https://github.com/padulafacundo
@@ -14,9 +14,9 @@
14
14
 
15
15
  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.
16
16
 
17
- 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:
17
+ 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 `FAKETIME_NO_CACHE=1` option. E.g. when installed via homebrew on macOS:
18
18
  ```shell
19
- 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
19
+ DYLD_INSERT_LIBRARIES=/usr/local/Cellar/libfaketime/2.9.7_1/lib/faketime/libfaketime.1.dylib DYLD_FORCE_FLAT_NAMESPACE=1 FAKETIME_NO_CACHE=1 bundle exec rspec
20
20
  ```
21
21
 
22
22
  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).
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  __Note__: You are viewing the README for the development version of webauthn-ruby.
2
- For the current release version see https://github.com/cedarcode/webauthn-ruby/blob/1-stable/README.md.
2
+ For the current release version see https://github.com/cedarcode/webauthn-ruby/blob/2-stable/README.md.
3
3
 
4
4
  # webauthn-ruby
5
5
 
@@ -342,7 +342,9 @@ credential_with_assertion.verify(
342
342
  )
343
343
  ```
344
344
 
345
- ## Attestation Statement Formats
345
+ ## Attestation
346
+
347
+ ### Attestation Statement Format
346
348
 
347
349
  | Attestation Statement Format | Supported? |
348
350
  | -------- | :--------: |
@@ -356,7 +358,9 @@ credential_with_assertion.verify(
356
358
  | fido-u2f | Yes |
357
359
  | none | Yes |
358
360
 
359
- NOTE: Be aware that it is up to you to do "trust path validation" (steps 15 and 16 in [Registering a new credential](https://www.w3.org/TR/webauthn/#registering-a-new-credential)) if that's a requirement of your Relying Party policy. The gem doesn't perform that validation for you right now.
361
+ ### Attestation Types
362
+
363
+ You can define what trust policy to enforce by setting `acceptable_attestation_types` config to a subset of `['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']` and `attestation_root_certificates_finders` to an object that responds to `#find` and returns the corresponding root certificate for each registration. The `#find` method will be called passing keyword arguments `attesation_format`, `aaguid` and `attestation_certificate_key_id`.
360
364
 
361
365
  ## Testing Your Integration
362
366
 
@@ -4,11 +4,11 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------- | ------------------ |
7
+ | 2.1.z | :white_check_mark: |
8
+ | 2.0.z | :white_check_mark: |
7
9
  | 1.18.z | :white_check_mark: |
8
10
  | 1.17.z | :white_check_mark: |
9
- | 1.16.z | :white_check_mark: |
10
- | 1.15.z | :white_check_mark: |
11
- | < 1.15 | :x: |
11
+ | < 1.17 | :x: |
12
12
 
13
13
  ## Reporting a Vulnerability
14
14
 
@@ -13,11 +13,22 @@ module AndroidSafetynet
13
13
  class NonceMismatchError < VerificationError; end
14
14
  class SignatureError < VerificationError; end
15
15
  class ResponseMissingError < VerificationError; end
16
+ class TimestampError < VerificationError; end
17
+ class TrustworthinessError < VerificationError; end
16
18
 
17
19
  CERTIRICATE_CHAIN_HEADER = "x5c"
18
20
  VALID_SUBJECT_HOSTNAME = "attest.android.com"
19
21
  HEADERS_POSITION = 1
20
22
  PAYLOAD_POSITION = 0
23
+ LEEWAY = 60
24
+
25
+ # FIXME: Should probably be limited to only roots published by Google
26
+ # See https://github.com/cedarcode/webauthn-ruby/issues/160#issuecomment-487941201
27
+ @trust_store = OpenSSL::X509::Store.new.tap { |trust_store| trust_store.set_default_paths }
28
+
29
+ class << self
30
+ attr_accessor :trust_store
31
+ end
21
32
 
22
33
  attr_reader :response
23
34
 
@@ -25,11 +36,15 @@ module AndroidSafetynet
25
36
  @response = response
26
37
  end
27
38
 
28
- def verify(nonce)
39
+ def verify(nonce, trustworthiness: true)
29
40
  if response
30
41
  valid_nonce?(nonce) || raise(NonceMismatchError)
31
42
  valid_attestation_domain? || raise(LeafCertificateSubjectError)
32
43
  valid_signature? || raise(SignatureError)
44
+ valid_timestamp? || raise(TimestampError)
45
+ trustworthy? || raise(TrustworthinessError) if trustworthiness
46
+
47
+ true
33
48
  else
34
49
  raise(ResponseMissingError)
35
50
  end
@@ -39,12 +54,21 @@ module AndroidSafetynet
39
54
  payload["ctsProfileMatch"]
40
55
  end
41
56
 
57
+ def leaf_certificate
58
+ certificate_chain[0]
59
+ end
60
+
42
61
  def certificate_chain
43
62
  @certificate_chain ||= headers[CERTIRICATE_CHAIN_HEADER].map do |cert|
44
63
  OpenSSL::X509::Certificate.new(Base64.strict_decode64(cert))
45
64
  end
46
65
  end
47
66
 
67
+ def valid_timestamp?
68
+ now = Time.now
69
+ Time.at((payload["timestampMs"] / 1000.0).round).between?(now - LEEWAY, now)
70
+ end
71
+
48
72
  private
49
73
 
50
74
  def valid_nonce?(nonce)
@@ -65,8 +89,16 @@ module AndroidSafetynet
65
89
  false
66
90
  end
67
91
 
68
- def leaf_certificate
69
- certificate_chain[0]
92
+ def trustworthy?
93
+ !trust_store || trust_store.verify(leaf_certificate, signing_certificates)
94
+ end
95
+
96
+ def trust_store
97
+ self.class.trust_store
98
+ end
99
+
100
+ def signing_certificates
101
+ certificate_chain[1..-1]
70
102
  end
71
103
 
72
104
  def headers
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose"
4
+
5
+ RSASSAAlgorithm = Struct.new(:id, :name, :hash_function, :kty)
6
+
7
+ COSE::Algorithm.register(RSASSAAlgorithm.new(-257, "RS256", "SHA256", COSE::Key::RSA::KTY_RSA))
8
+ COSE::Algorithm.register(RSASSAAlgorithm.new(-258, "RS384", "SHA384", COSE::Key::RSA::KTY_RSA))
9
+ COSE::Algorithm.register(RSASSAAlgorithm.new(-259, "RS512", "SHA512", COSE::Key::RSA::KTY_RSA))
10
+ COSE::Algorithm.register(RSASSAAlgorithm.new(-65535, "RS1", "SHA1", COSE::Key::RSA::KTY_RSA))
@@ -19,4 +19,26 @@ module TPM
19
19
 
20
20
  # ECC curves
21
21
  ECC_NIST_P256 = 0x0003
22
+
23
+ # https://trustedcomputinggroup.org/resource/vendor-id-registry/ section 2 "TPM Capabilities Vendor ID (CAP_VID)"
24
+ VENDOR_IDS = {
25
+ "id:414D4400" => "AMD",
26
+ "id:41544D4C" => "Atmel",
27
+ "id:4252434D" => "Broadcom",
28
+ "id:49424D00" => "IBM",
29
+ "id:49465800" => "Infineon",
30
+ "id:494E5443" => "Intel",
31
+ "id:4C454E00" => "Lenovo",
32
+ "id:4E534D20" => "National Semiconductor",
33
+ "id:4E545A00" => "Nationz",
34
+ "id:4E544300" => "Nuvoton Technology",
35
+ "id:51434F4D" => "Qualcomm",
36
+ "id:534D5343" => "SMSC",
37
+ "id:53544D20" => "ST Microelectronics",
38
+ "id:534D534E" => "Samsung",
39
+ "id:534E5300" => "Sinosun",
40
+ "id:54584E00" => "Texas Instruments",
41
+ "id:57454300" => "Winbond",
42
+ "id:524F4343" => "Fuzhou Rockchip",
43
+ }.freeze
22
44
  end
@@ -22,7 +22,7 @@ module WebAuthn
22
22
  all_applications_field_not_present? &&
23
23
  valid_authorization_list_origin? &&
24
24
  valid_authorization_list_purpose? &&
25
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_certificate_chain]
25
+ [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_trust_path]
26
26
  end
27
27
 
28
28
  private
@@ -8,34 +8,24 @@ module WebAuthn
8
8
  module AttestationStatement
9
9
  # Implements https://www.w3.org/TR/webauthn-1/#sctn-android-safetynet-attestation
10
10
  class AndroidSafetynet < Base
11
- def self.default_trust_store
12
- OpenSSL::X509::Store.new.tap { |trust_store| trust_store.set_default_paths }
13
- end
14
-
15
- def valid?(authenticator_data, client_data_hash, trust_store: self.class.default_trust_store)
16
- trusted_attestation_certificate?(trust_store) &&
17
- valid_response?(authenticator_data, client_data_hash) &&
11
+ def valid?(authenticator_data, client_data_hash)
12
+ valid_response?(authenticator_data, client_data_hash) &&
18
13
  valid_version? &&
19
14
  cts_profile_match? &&
20
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_certificate]
15
+ [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_trust_path]
21
16
  end
22
17
 
23
18
  def attestation_certificate
24
- attestation_response.certificate_chain[0]
19
+ attestation_response.leaf_certificate
25
20
  end
26
21
 
27
22
  private
28
23
 
29
- # FIXME: This should be a responsibility of AndroidSafetynet::AttestationResponse#verify
30
- def trusted_attestation_certificate?(trust_store)
31
- trust_store.verify(attestation_certificate, signing_certificates)
32
- end
33
-
34
24
  def valid_response?(authenticator_data, client_data_hash)
35
25
  nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
36
26
 
37
27
  begin
38
- attestation_response.verify(nonce)
28
+ attestation_response.verify(nonce, trustworthiness: false)
39
29
  rescue ::AndroidSafetynet::AttestationResponse::VerificationError
40
30
  false
41
31
  end
@@ -50,8 +40,8 @@ module WebAuthn
50
40
  attestation_response.cts_profile_match?
51
41
  end
52
42
 
53
- def signing_certificates
54
- attestation_response.certificate_chain[1..-1]
43
+ def attestation_trust_path
44
+ attestation_response.certificate_chain
55
45
  end
56
46
 
57
47
  def attestation_response
@@ -27,7 +27,13 @@ module WebAuthn
27
27
  end
28
28
 
29
29
  def attestation_certificate
30
- attestation_certificate_chain&.first
30
+ certificates&.first
31
+ end
32
+
33
+ def certificate_chain
34
+ if certificates
35
+ certificates[1..-1]
36
+ end
31
37
  end
32
38
 
33
39
  private
@@ -46,8 +52,8 @@ module WebAuthn
46
52
  end
47
53
  end
48
54
 
49
- def attestation_certificate_chain
50
- @attestation_certificate_chain ||= raw_attestation_certificates&.map do |raw_certificate|
55
+ def certificates
56
+ @certificates ||= raw_certificates&.map do |raw_certificate|
51
57
  OpenSSL::X509::Certificate.new(raw_certificate)
52
58
  end
53
59
  end
@@ -56,7 +62,7 @@ module WebAuthn
56
62
  statement["alg"]
57
63
  end
58
64
 
59
- def raw_attestation_certificates
65
+ def raw_certificates
60
66
  statement["x5c"]
61
67
  end
62
68
 
@@ -67,6 +73,12 @@ module WebAuthn
67
73
  def signature
68
74
  statement["sig"]
69
75
  end
76
+
77
+ def attestation_trust_path
78
+ if certificates&.any?
79
+ certificates
80
+ end
81
+ end
70
82
  end
71
83
  end
72
84
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cose"
3
4
  require "openssl"
4
5
  require "webauthn/attestation_statement/base"
5
6
  require "webauthn/attestation_statement/fido_u2f/public_key"
@@ -10,6 +11,7 @@ module WebAuthn
10
11
  class FidoU2f < Base
11
12
  VALID_ATTESTATION_CERTIFICATE_COUNT = 1
12
13
  VALID_ATTESTATION_CERTIFICATE_ALGORITHM = COSE::Algorithm.by_name("ES256")
14
+ VALID_ATTESTATION_CERTIFICATE_KEY_CURVE = COSE::Key::Curve.by_name("P-256")
13
15
 
14
16
  def valid?(authenticator_data, client_data_hash)
15
17
  valid_format? &&
@@ -17,19 +19,19 @@ module WebAuthn
17
19
  valid_credential_public_key?(authenticator_data.credential.public_key) &&
18
20
  valid_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
19
21
  valid_signature?(authenticator_data, client_data_hash) &&
20
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, [attestation_certificate]]
22
+ [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, attestation_trust_path]
21
23
  end
22
24
 
23
25
  private
24
26
 
25
27
  def valid_format?
26
- !!(raw_attestation_certificates && signature) &&
27
- raw_attestation_certificates.length == VALID_ATTESTATION_CERTIFICATE_COUNT
28
+ !!(raw_certificates && signature) &&
29
+ raw_certificates.length == VALID_ATTESTATION_CERTIFICATE_COUNT
28
30
  end
29
31
 
30
32
  def valid_certificate_public_key?
31
33
  certificate_public_key.is_a?(OpenSSL::PKey::EC) &&
32
- certificate_public_key.group.curve_name == VALID_ATTESTATION_CERTIFICATE_ALGORITHM.key_curve &&
34
+ certificate_public_key.group.curve_name == VALID_ATTESTATION_CERTIFICATE_KEY_CURVE.pkey_name &&
33
35
  certificate_public_key.check_key
34
36
  end
35
37
 
@@ -30,12 +30,12 @@ module WebAuthn
30
30
  end
31
31
 
32
32
  def self_attestation?
33
- !raw_attestation_certificates && !raw_ecdaa_key_id
33
+ !raw_certificates && !raw_ecdaa_key_id
34
34
  end
35
35
 
36
36
  def valid_format?
37
37
  algorithm && signature && (
38
- [raw_attestation_certificates, raw_ecdaa_key_id].compact.size < 2
38
+ [raw_certificates, raw_ecdaa_key_id].compact.size < 2
39
39
  )
40
40
  end
41
41
 
@@ -46,15 +46,15 @@ module WebAuthn
46
46
  end
47
47
 
48
48
  def valid_certificate_chain?
49
- if attestation_certificate_chain
50
- attestation_certificate_chain[1..-1].all? { |c| certificate_in_use?(c) }
49
+ if certificate_chain
50
+ certificate_chain.all? { |c| certificate_in_use?(c) }
51
51
  else
52
52
  true
53
53
  end
54
54
  end
55
55
 
56
56
  def valid_ec_public_keys?(credential)
57
- (attestation_certificate_chain&.map(&:public_key) || [credential.public_key_object])
57
+ (certificates&.map(&:public_key) || [credential.public_key_object])
58
58
  .select { |pkey| pkey.is_a?(OpenSSL::PKey::EC) }
59
59
  .all? { |pkey| pkey.check_key }
60
60
  end
@@ -89,8 +89,8 @@ module WebAuthn
89
89
  end
90
90
 
91
91
  def attestation_type_and_trust_path
92
- if raw_attestation_certificates&.any?
93
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, attestation_certificate_chain]
92
+ if attestation_trust_path
93
+ [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, attestation_trust_path]
94
94
  else
95
95
  [WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF, nil]
96
96
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "cose/algorithm"
4
4
  require "openssl"
5
+ require "tpm/constants"
5
6
  require "webauthn/attestation_statement/base"
6
7
  require "webauthn/attestation_statement/tpm/cert_info"
7
8
  require "webauthn/attestation_statement/tpm/pub_area"
@@ -12,6 +13,10 @@ module WebAuthn
12
13
  class TPM < Base
13
14
  CERTIFICATE_V3 = 2
14
15
  CERTIFICATE_EMPTY_NAME = OpenSSL::X509::Name.new([]).freeze
16
+ CERTIFICATE_SAN_DIRECTORY_NAME = 4
17
+ OID_TCG_AT_TPM_MANUFACTURER = "2.23.133.2.1"
18
+ OID_TCG_AT_TPM_MODEL = "2.23.133.2.2"
19
+ OID_TCG_AT_TPM_VERSION = "2.23.133.2.3"
15
20
  OID_TCG_KP_AIK_CERTIFICATE = "2.23.133.8.3"
16
21
  TPM_V2 = "2.0"
17
22
 
@@ -24,7 +29,8 @@ module WebAuthn
24
29
  valid_signature? &&
25
30
  valid_attestation_certificate? &&
26
31
  pub_area.valid?(authenticator_data.credential.public_key) &&
27
- cert_info.valid?(statement["pubArea"], OpenSSL::Digest.digest(cose_algorithm.hash, att_to_be_signed)) &&
32
+ cert_info.valid?(statement["pubArea"],
33
+ OpenSSL::Digest.digest(cose_algorithm.hash_function, att_to_be_signed)) &&
28
34
  matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
29
35
  [attestation_type, attestation_trust_path]
30
36
  when ATTESTATION_TYPE_ECDAA
@@ -48,11 +54,30 @@ module WebAuthn
48
54
 
49
55
  attestation_certificate.version == CERTIFICATE_V3 &&
50
56
  attestation_certificate.subject.eql?(CERTIFICATE_EMPTY_NAME) &&
57
+ valid_subject_alternative_name? &&
51
58
  certificate_in_use?(attestation_certificate) &&
52
59
  extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == "CA:FALSE" &&
53
60
  extensions.find { |ext| ext.oid == "extendedKeyUsage" }&.value == OID_TCG_KP_AIK_CERTIFICATE
54
61
  end
55
62
 
63
+ def valid_subject_alternative_name?
64
+ extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectAltName" }
65
+ return unless extension&.critical?
66
+
67
+ san_asn1 = OpenSSL::ASN1.decode(extension).find do |val|
68
+ val.tag_class == :UNIVERSAL && val.tag == OpenSSL::ASN1::OCTET_STRING
69
+ end
70
+ directory_name = OpenSSL::ASN1.decode(san_asn1.value).find do |val|
71
+ val.tag_class == :CONTEXT_SPECIFIC && val.tag == CERTIFICATE_SAN_DIRECTORY_NAME
72
+ end
73
+ name = OpenSSL::X509::Name.new(directory_name.value.first).to_a
74
+ manufacturer = name.assoc(OID_TCG_AT_TPM_MANUFACTURER).at(1)
75
+ model = name.assoc(OID_TCG_AT_TPM_MODEL).at(1)
76
+ version = name.assoc(OID_TCG_AT_TPM_VERSION).at(1)
77
+
78
+ ::TPM::VENDOR_IDS[manufacturer] && !model.empty? && !version.empty?
79
+ end
80
+
56
81
  def certificate_in_use?(certificate)
57
82
  now = Time.now
58
83
 
@@ -80,18 +105,14 @@ module WebAuthn
80
105
  end
81
106
 
82
107
  def attestation_type
83
- if raw_attestation_certificates && !raw_ecdaa_key_id
108
+ if raw_certificates && !raw_ecdaa_key_id
84
109
  ATTESTATION_TYPE_ATTCA
85
- elsif raw_ecdaa_key_id && !raw_attestation_certificates
110
+ elsif raw_ecdaa_key_id && !raw_certificates
86
111
  ATTESTATION_TYPE_ECDAA
87
112
  else
88
113
  raise "Attestation type invalid"
89
114
  end
90
115
  end
91
-
92
- def attestation_trust_path
93
- attestation_certificate_chain
94
- end
95
116
  end
96
117
  end
97
118
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cose/algorithm"
4
- require "cose/key"
3
+ require "cose"
4
+ require "cose/rsassa_algorithm"
5
5
  require "tpm/constants"
6
6
  require "tpm/t_public"
7
7
  require "webauthn/attestation_statement/base"
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cose/algorithm"
4
- require "cose/key"
5
- require "webauthn/attestation_statement/fido_u2f/public_key"
6
3
  require "webauthn/authenticator_data"
7
4
  require "webauthn/authenticator_response"
8
5
  require "webauthn/encoder"
9
6
  require "webauthn/signature_verifier"
7
+ require "webauthn/public_key"
10
8
 
11
9
  module WebAuthn
12
10
  class SignatureVerificationError < VerificationError; end
@@ -32,7 +30,7 @@ module WebAuthn
32
30
  attr_reader :user_handle
33
31
 
34
32
  def initialize(authenticator_data:, signature:, user_handle: nil, **options)
35
- super(options)
33
+ super(**options)
36
34
 
37
35
  @authenticator_data_bytes = authenticator_data
38
36
  @signature = signature
@@ -42,7 +40,7 @@ module WebAuthn
42
40
  def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil,
43
41
  rp_id: nil)
44
42
  super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id)
45
- verify_item(:signature, credential_cose_key(public_key))
43
+ verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key))
46
44
  verify_item(:sign_count, sign_count)
47
45
 
48
46
  true
@@ -56,9 +54,9 @@ module WebAuthn
56
54
 
57
55
  attr_reader :authenticator_data_bytes, :signature
58
56
 
59
- def valid_signature?(credential_cose_key)
57
+ def valid_signature?(webauthn_public_key)
60
58
  WebAuthn::SignatureVerifier
61
- .new(credential_cose_key.alg, credential_cose_key.to_pkey)
59
+ .new(webauthn_public_key.alg, webauthn_public_key.pkey)
62
60
  .verify(signature, authenticator_data_bytes + client_data.hash)
63
61
  end
64
62
 
@@ -71,29 +69,6 @@ module WebAuthn
71
69
  end
72
70
  end
73
71
 
74
- def credential_cose_key(public_key)
75
- if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
76
- # Gem version v1.11.0 and lower, used to behave so that Credential#public_key
77
- # returned an EC P-256 uncompressed point.
78
- #
79
- # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
80
- # and Credential#public_key started returning the unchanged COSE_Key formatted
81
- # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
82
- #
83
- # Given that the credential public key is expected to be stored long-term by the gem
84
- # user and later be passed as the public_key argument in the
85
- # AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
86
- COSE::Key::EC2.new(
87
- alg: COSE::Algorithm.by_name("ES256").id,
88
- crv: 1,
89
- x: public_key[1..32],
90
- y: public_key[33..-1]
91
- )
92
- else
93
- COSE::Key.deserialize(public_key)
94
- end
95
- end
96
-
97
72
  def type
98
73
  WebAuthn::TYPES[:get]
99
74
  end
@@ -12,6 +12,7 @@ require "webauthn/encoder"
12
12
 
13
13
  module WebAuthn
14
14
  class AttestationStatementVerificationError < VerificationError; end
15
+ class AttestationTrustworthinessVerificationError < VerificationError; end
15
16
  class AttestedCredentialVerificationError < VerificationError; end
16
17
 
17
18
  class AuthenticatorAttestationResponse < AuthenticatorResponse
@@ -27,7 +28,7 @@ module WebAuthn
27
28
  attr_reader :attestation_type, :attestation_trust_path
28
29
 
29
30
  def initialize(attestation_object:, **options)
30
- super(options)
31
+ super(**options)
31
32
 
32
33
  @attestation_object = attestation_object
33
34
  end
@@ -36,7 +37,10 @@ module WebAuthn
36
37
  super
37
38
 
38
39
  verify_item(:attested_credential)
39
- verify_item(:attestation_statement) if WebAuthn.configuration.verify_attestation_statement
40
+ if WebAuthn.configuration.verify_attestation_statement
41
+ verify_item(:attestation_statement)
42
+ verify_item(:attestation_trustworthiness) if WebAuthn.configuration.attestation_root_certificates_finders.any?
43
+ end
40
44
 
41
45
  true
42
46
  end
@@ -90,6 +94,18 @@ module WebAuthn
90
94
  @attestation_type, @attestation_trust_path = attestation_statement.valid?(authenticator_data, client_data.hash)
91
95
  end
92
96
 
97
+ def valid_attestation_trustworthiness?
98
+ case @attestation_type
99
+ when WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE
100
+ WebAuthn.configuration.acceptable_attestation_types.include?('None')
101
+ when WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF
102
+ WebAuthn.configuration.acceptable_attestation_types.include?('Self')
103
+ else
104
+ WebAuthn.configuration.acceptable_attestation_types.include?(@attestation_type) &&
105
+ attestation_root_certificates_store.verify(leaf_certificate, signing_certificates)
106
+ end
107
+ end
108
+
93
109
  def raw_subject_key_identifier(certificate)
94
110
  extension = certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
95
111
  return unless extension
@@ -98,5 +114,32 @@ module WebAuthn
98
114
  ext_value = ext_asn1.value.last
99
115
  OpenSSL::ASN1.decode(ext_value.value).value
100
116
  end
117
+
118
+ def attestation_root_certificates_store
119
+ certificates =
120
+ WebAuthn.configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder|
121
+ if certs.empty?
122
+ finder.find(attestation_format: attestation_format,
123
+ aaguid: aaguid,
124
+ attestation_certificate_key_id: attestation_certificate_key) || []
125
+ else
126
+ certs
127
+ end
128
+ end
129
+
130
+ OpenSSL::X509::Store.new.tap do |store|
131
+ certificates.each do |cert|
132
+ store.add_cert(cert)
133
+ end
134
+ end
135
+ end
136
+
137
+ def signing_certificates
138
+ @attestation_trust_path[1..-1]
139
+ end
140
+
141
+ def leaf_certificate
142
+ @attestation_trust_path.first
143
+ end
101
144
  end
102
145
  end
@@ -33,14 +33,20 @@ module WebAuthn
33
33
  verify_item(:origin, expected_origin)
34
34
  verify_item(:authenticator_data)
35
35
  verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
36
- verify_item(:user_presence)
37
- verify_item(:user_verified, user_verification)
36
+
37
+ if !WebAuthn.configuration.silent_authentication
38
+ verify_item(:user_presence)
39
+ end
40
+
41
+ if user_verification
42
+ verify_item(:user_verified)
43
+ end
38
44
 
39
45
  true
40
46
  end
41
47
 
42
- def valid?(*args)
43
- verify(*args)
48
+ def valid?(*args, **keyword_arguments)
49
+ verify(*args, **keyword_arguments)
44
50
  rescue WebAuthn::VerificationError
45
51
  false
46
52
  end
@@ -91,12 +97,8 @@ module WebAuthn
91
97
  authenticator_data.user_flagged?
92
98
  end
93
99
 
94
- def valid_user_verified?(user_verification)
95
- if user_verification
96
- authenticator_data.user_verified?
97
- else
98
- true
99
- end
100
+ def valid_user_verified?
101
+ authenticator_data.user_verified?
100
102
  end
101
103
 
102
104
  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
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/attestation_statement/fido_u2f/public_key"
4
+ require "cose/key"
5
+ require "cose/algorithm"
6
+
7
+ module WebAuthn
8
+ class PublicKey
9
+ def self.deserialize(public_key)
10
+ cose_key =
11
+ if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
12
+ # Gem version v1.11.0 and lower, used to behave so that Credential#public_key
13
+ # returned an EC P-256 uncompressed point.
14
+ #
15
+ # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
16
+ # and Credential#public_key started returning the unchanged COSE_Key formatted
17
+ # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
18
+ #
19
+ # Given that the credential public key is expected to be stored long-term by the gem
20
+ # user and later be passed as the public_key argument in the
21
+ # AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
22
+ COSE::Key::EC2.new(
23
+ alg: COSE::Algorithm.by_name("ES256").id,
24
+ crv: 1,
25
+ x: public_key[1..32],
26
+ y: public_key[33..-1]
27
+ )
28
+ else
29
+ COSE::Key.deserialize(public_key)
30
+ end
31
+
32
+ new(cose_key: cose_key)
33
+ end
34
+
35
+ attr_reader :cose_key
36
+
37
+ def initialize(cose_key:)
38
+ @cose_key = cose_key
39
+ end
40
+
41
+ def pkey
42
+ @cose_key.to_pkey
43
+ end
44
+
45
+ def alg
46
+ @cose_key.alg
47
+ end
48
+ end
49
+ end
@@ -42,14 +42,14 @@ module WebAuthn
42
42
  rp[:name] ||= configuration.rp_name
43
43
  rp[:id] ||= configuration.rp_id
44
44
 
45
- RPEntity.new(rp)
45
+ RPEntity.new(**rp)
46
46
  else
47
47
  rp
48
48
  end
49
49
 
50
50
  @user =
51
51
  if user.is_a?(Hash)
52
- UserEntity.new(user)
52
+ UserEntity.new(**user)
53
53
  else
54
54
  user
55
55
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cose/algorithm"
4
- require "cose/key"
3
+ require "cose"
4
+ require "cose/rsassa_algorithm"
5
5
  require "openssl"
6
6
  require "webauthn/error"
7
7
 
@@ -24,10 +24,10 @@ module WebAuthn
24
24
 
25
25
  def verify(signature, verification_data, rsa_pss_salt_length: :digest)
26
26
  if rsa_pss?
27
- public_key.verify_pss(cose_algorithm.hash, signature, verification_data,
28
- salt_length: rsa_pss_salt_length, mgf1_hash: cose_algorithm.hash)
27
+ public_key.verify_pss(cose_algorithm.hash_function, signature, verification_data,
28
+ salt_length: rsa_pss_salt_length, mgf1_hash: cose_algorithm.hash_function)
29
29
  else
30
- public_key.verify(cose_algorithm.hash, signature, verification_data)
30
+ public_key.verify(cose_algorithm.hash_function, signature, verification_data)
31
31
  end
32
32
  end
33
33
 
@@ -37,13 +37,25 @@ module WebAuthn
37
37
 
38
38
  def cose_algorithm
39
39
  case algorithm
40
- when COSE::Algorithm
40
+ when COSE::Algorithm::Base
41
41
  algorithm
42
42
  else
43
43
  COSE::Algorithm.find(algorithm)
44
44
  end
45
45
  end
46
46
 
47
+ # This logic is a candidate to be moved to cose gem domain
48
+ def cose_key_type
49
+ case cose_algorithm
50
+ when COSE::Algorithm::ECDSA
51
+ COSE::Key::EC2::KTY_EC2
52
+ when COSE::Algorithm::RSAPSS, RSASSAAlgorithm
53
+ COSE::Key::RSA::KTY_RSA
54
+ else
55
+ raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
56
+ end
57
+ end
58
+
47
59
  def rsa_pss?
48
60
  cose_algorithm.name.start_with?("PS")
49
61
  end
@@ -53,7 +65,7 @@ module WebAuthn
53
65
  raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
54
66
  elsif !supported_algorithms.include?(cose_algorithm.name)
55
67
  raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
56
- elsif !KTY_MAP[cose_algorithm.kty].include?(public_key.class)
68
+ elsif !KTY_MAP[cose_key_type].include?(public_key.class)
57
69
  raise("Incompatible algorithm and key")
58
70
  end
59
71
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebAuthn
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.0"
5
5
  end
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "awrence", "~> 1.1"
35
35
  spec.add_dependency "bindata", "~> 2.4"
36
36
  spec.add_dependency "cbor", "~> 0.5.9"
37
- spec.add_dependency "cose", "~> 0.8.0"
37
+ spec.add_dependency "cose", "~> 0.10.0"
38
38
  spec.add_dependency "jwt", [">= 1.5", "< 3.0"]
39
39
  spec.add_dependency "openssl", "~> 2.0"
40
40
  spec.add_dependency "securecompare", "~> 1.0"
@@ -45,4 +45,5 @@ Gem::Specification.new do |spec|
45
45
  spec.add_development_dependency "rake", "~> 13.0"
46
46
  spec.add_development_dependency "rspec", "~> 3.8"
47
47
  spec.add_development_dependency "rubocop", "0.75.0"
48
+ spec.add_development_dependency "timecop", "~> 0.9.1"
48
49
  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: 2.0.0
4
+ version: 2.1.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-10-03 00:00:00.000000000 Z
12
+ date: 2019-12-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: awrence
@@ -59,14 +59,14 @@ dependencies:
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: 0.8.0
62
+ version: 0.10.0
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: 0.8.0
69
+ version: 0.10.0
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: jwt
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -205,6 +205,20 @@ dependencies:
205
205
  - - '='
206
206
  - !ruby/object:Gem::Version
207
207
  version: 0.75.0
208
+ - !ruby/object:Gem::Dependency
209
+ name: timecop
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: 0.9.1
215
+ type: :development
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: 0.9.1
208
222
  description: |-
209
223
  WebAuthn ruby server library ― Make your application a W3C Web Authentication conformant
210
224
  Relying Party and allow your users to authenticate with U2F and FIDO2 authenticators.
@@ -235,7 +249,7 @@ files:
235
249
  - gemfiles/openssl_2_1.gemfile
236
250
  - gemfiles/openssl_head.gemfile
237
251
  - lib/android_safetynet/attestation_response.rb
238
- - lib/cose/algorithm.rb
252
+ - lib/cose/rsassa_algorithm.rb
239
253
  - lib/tpm/constants.rb
240
254
  - lib/tpm/s_attest.rb
241
255
  - lib/tpm/s_attest/s_certify_info.rb
@@ -277,6 +291,7 @@ files:
277
291
  - lib/webauthn/fake_authenticator/attestation_object.rb
278
292
  - lib/webauthn/fake_authenticator/authenticator_data.rb
279
293
  - lib/webauthn/fake_client.rb
294
+ - lib/webauthn/public_key.rb
280
295
  - lib/webauthn/public_key_credential.rb
281
296
  - lib/webauthn/public_key_credential/creation_options.rb
282
297
  - lib/webauthn/public_key_credential/entity.rb
@@ -313,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
328
  - !ruby/object:Gem::Version
314
329
  version: '0'
315
330
  requirements: []
316
- rubygems_version: 3.0.3
331
+ rubygems_version: 3.1.2
317
332
  signing_key:
318
333
  specification_version: 4
319
334
  summary: WebAuthn ruby server library
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "cose/key"
4
-
5
- # TODO: Move this to cose gem
6
- module COSE
7
- # https://tools.ietf.org/html/rfc8152#section-8.1
8
- Algorithm = Struct.new(:id, :name, :hash, :kty, :key_curve) do
9
- @registered = {}
10
-
11
- def self.register(id, name, hash, kty, key_curve = nil)
12
- @registered[id] = COSE::Algorithm.new(id, name, hash, kty, key_curve)
13
- end
14
-
15
- def self.find(id)
16
- @registered[id]
17
- end
18
-
19
- def self.by_name(name)
20
- @registered.values.detect { |algorithm| algorithm.name == name }
21
- end
22
-
23
- def value
24
- id
25
- end
26
- end
27
- end
28
-
29
- COSE::Algorithm.register(-7, "ES256", "SHA256", COSE::Key::EC2::KTY_EC2, "prime256v1")
30
- COSE::Algorithm.register(-35, "ES384", "SHA384", COSE::Key::EC2::KTY_EC2, "prime256v1")
31
- COSE::Algorithm.register(-36, "ES512", "SHA512", COSE::Key::EC2::KTY_EC2, "prime256v1")
32
- COSE::Algorithm.register(-37, "PS256", "SHA256", COSE::Key::RSA::KTY_RSA)
33
- COSE::Algorithm.register(-38, "PS384", "SHA384", COSE::Key::RSA::KTY_RSA)
34
- COSE::Algorithm.register(-39, "PS512", "SHA512", COSE::Key::RSA::KTY_RSA)
35
- COSE::Algorithm.register(-257, "RS256", "SHA256", COSE::Key::RSA::KTY_RSA)
36
- COSE::Algorithm.register(-258, "RS384", "SHA384", COSE::Key::RSA::KTY_RSA)
37
- COSE::Algorithm.register(-259, "RS512", "SHA512", COSE::Key::RSA::KTY_RSA)
38
- COSE::Algorithm.register(-65535, "RS1", "SHA1", COSE::Key::RSA::KTY_RSA)