webauthn 2.5.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +3 -7
  3. data/.github/workflows/git.yml +21 -0
  4. data/.rubocop.yml +1 -1
  5. data/CHANGELOG.md +42 -1
  6. data/README.md +5 -3
  7. data/docs/advanced_configuration.md +174 -0
  8. data/docs/u2f_migration.md +14 -20
  9. data/lib/webauthn/attestation_object.rb +9 -5
  10. data/lib/webauthn/attestation_statement/apple.rb +2 -2
  11. data/lib/webauthn/attestation_statement/base.rb +11 -25
  12. data/lib/webauthn/attestation_statement/packed.rb +1 -1
  13. data/lib/webauthn/attestation_statement/tpm.rb +2 -2
  14. data/lib/webauthn/attestation_statement.rb +2 -2
  15. data/lib/webauthn/authenticator_assertion_response.rb +4 -3
  16. data/lib/webauthn/authenticator_attestation_response.rb +10 -7
  17. data/lib/webauthn/authenticator_data/attested_credential_data.rb +10 -5
  18. data/lib/webauthn/authenticator_data.rb +10 -2
  19. data/lib/webauthn/authenticator_response.rb +7 -7
  20. data/lib/webauthn/configuration.rb +38 -38
  21. data/lib/webauthn/credential.rb +5 -4
  22. data/lib/webauthn/fake_authenticator/attestation_object.rb +8 -0
  23. data/lib/webauthn/fake_authenticator/authenticator_data.rb +20 -5
  24. data/lib/webauthn/fake_authenticator.rb +9 -1
  25. data/lib/webauthn/fake_client.rb +10 -2
  26. data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
  27. data/lib/webauthn/public_key_credential/options.rb +9 -8
  28. data/lib/webauthn/public_key_credential/request_options.rb +11 -1
  29. data/lib/webauthn/public_key_credential.rb +24 -5
  30. data/lib/webauthn/public_key_credential_with_assertion.rb +14 -1
  31. data/lib/webauthn/relying_party.rb +120 -0
  32. data/lib/webauthn/u2f_migrator.rb +4 -1
  33. data/lib/webauthn/version.rb +1 -1
  34. data/webauthn.gemspec +3 -5
  35. metadata +16 -45
  36. data/Appraisals +0 -9
  37. data/gemfiles/openssl_2_1.gemfile +0 -7
  38. data/gemfiles/openssl_2_2.gemfile +0 -7
  39. data/lib/webauthn/security_utils.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e6487b19f172c0c7e96af23d04e47f91bebd2ef7d20f144f99f85e761a2db86
4
- data.tar.gz: 7623405e7cd01708f29897a0d4183fbc8c9b2a3dfb06b9c182646ddaf9c6cb0d
3
+ metadata.gz: b133f46bd003c9bfb9a8557bb8caeca7239fcd6c185f78e74e0ae578444d576e
4
+ data.tar.gz: 14a32debc72ddcfe7e752118ebd4db832b8a46a5628ee54830f30eada4159bcf
5
5
  SHA512:
6
- metadata.gz: d2f8d2137b2ee140a3258fbbff8d62e49264b2eafa80f0726dacc16a742addf75625b9da51696db6f3862a85e63f44ca5fc2b73320b1c256dd1c57f96121de24
7
- data.tar.gz: dcb2ea914a14944b4bf7c4682394df12e00ddd4a4b0cc1076a03a7368bf4d563d08b61fbbe27ece3ddcbc05a9ed542d8236c1bfce833669c9b60c5d3387b35b4
6
+ metadata.gz: 9b4ff5cee18575b517473fef7d8000d7e41f113b178378e64a4d2b9036d248f4295edd846a519ab76036a270c3066020fbe44847019812c2a5758f2b1b428f4c
7
+ data.tar.gz: f3ab22709cbf537982e163acc60dbe114217f223b18cb4cf681a3d932e2f31276f8286091e49da392356c1958ada7e2c07a5b86b48d57b597f965a57c1bcf63d
@@ -16,19 +16,15 @@ jobs:
16
16
  fail-fast: false
17
17
  matrix:
18
18
  ruby:
19
+ - '3.2'
20
+ - '3.1'
19
21
  - '3.0'
20
22
  - '2.7'
21
23
  - '2.6'
22
24
  - '2.5'
23
- - '2.4'
24
25
  - truffleruby
25
- gemfile:
26
- - openssl_2_2
27
- - openssl_2_1
28
- env:
29
- BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
30
26
  steps:
31
- - uses: actions/checkout@v2
27
+ - uses: actions/checkout@v3
32
28
  - uses: ruby/setup-ruby@v1
33
29
  with:
34
30
  ruby-version: ${{ matrix.ruby }}
@@ -0,0 +1,21 @@
1
+ # Syntax reference:
2
+ # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
3
+ name: Git Checks
4
+
5
+ on:
6
+ pull_request:
7
+ types: [opened, synchronize]
8
+
9
+ jobs:
10
+ # Fixup commits are OK in pull requests, but should generally be squashed
11
+ # before merging to master, e.g. using `git rebase -i --autosquash master`.
12
+ # See https://github.com/marketplace/actions/block-autosquash-commits
13
+ block-fixup:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v3
18
+ - name: Block autosquash commits
19
+ uses: xt0rted/block-autosquash-commits-action@v2
20
+ with:
21
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
data/.rubocop.yml CHANGED
@@ -7,7 +7,7 @@ inherit_mode:
7
7
  - AllowedNames
8
8
 
9
9
  AllCops:
10
- TargetRubyVersion: 2.4
10
+ TargetRubyVersion: 2.5
11
11
  DisabledByDefault: true
12
12
  NewCops: disable
13
13
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,11 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.0.0] - 2023-02-15
4
+
5
+ ### Added
6
+
7
+ - Add the capability of handling appid extension #319 [@santiagorodriguez96]
8
+ - Add support for credential backup flags #378 [@santiagorodriguez96]
9
+ - Update dependencies to make gem compatible with OpenSSL 3.1 ([@bdewater],[@santiagorodriguez96])
10
+
11
+ ## [v3.0.0.alpha2] - 2022-09-12
12
+
13
+ ### Added
14
+
15
+ - Rebased support for multiple relying parties from v3.0.0.alpha1 on top of v2.5.2, the previous alpha version was based on v2.3.0 ([@bdewater])
16
+
17
+ ### BREAKING CHANGES
18
+
19
+ - Bumped minimum required Ruby version to 2.5 ([@bdewater])
20
+
3
21
  ## [v3.0.0.alpha1] - 2020-06-27
4
22
 
5
23
  ### Added
6
24
 
7
25
  - Ability to define multiple relying parties with the introduction of the `WebAuthn::RelyingParty` class ([@padulafacundo], [@brauliomartinezlm])
8
26
 
27
+ ## [v2.5.2] - 2022-07-13
28
+
29
+ ### Added
30
+
31
+ - Updated dependencies to make the gem compatible with openssl-3 [@ClearlyClaire]
32
+
33
+ ## [v2.5.1] - 2022-03-20
34
+
35
+ ### Added
36
+
37
+ - Updated openssl support to be ~>2.2 [@bdewater]
38
+
39
+ ### Removed
40
+
41
+ - Removed dependency [secure_compare dependency] (https://rubygems.org/gems/secure_compare/versions/0.0.1) and use OpenSSL#secure_compare instead [@bdewater]
42
+
9
43
  ## [v2.5.0] - 2021-03-14
10
44
 
11
45
  ### Added
@@ -324,7 +358,11 @@ Note: Both additions should help making it compatible with Chrome for Android 70
324
358
  - `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
325
359
  - Works with ruby 2.5
326
360
 
327
- [v3.0.0.alpha1]: https://github.com/cedarcode/webauthn-ruby/compare/2-stable...v3.0.0.alpha1/
361
+ [v3.0.0]: https://github.com/cedarcode/webauthn-ruby/compare/2-stable...v3.0.0/
362
+ [v3.0.0.alpha2]: https://github.com/cedarcode/webauthn-ruby/compare/2-stable...v3.0.0.alpha2/
363
+ [v3.0.0.alpha1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.3.0...v3.0.0.alpha1
364
+ [v2.5.2]: https://github.com/cedarcode/webauthn-ruby/compare/v2.5.1...v2.5.2/
365
+ [v2.5.1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.5.0...v2.5.1/
328
366
  [v2.5.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.4.1...v2.5.0/
329
367
  [v2.4.1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.4.0...v2.4.1/
330
368
  [v2.4.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.3.0...v2.4.0/
@@ -367,3 +405,6 @@ Note: Both additions should help making it compatible with Chrome for Android 70
367
405
  [@lgarron]: https://github.com/lgarron
368
406
  [@juanarias93]: https://github.com/juanarias93
369
407
  [@kingjan1999]: https://github.com/@kingjan1999
408
+ [@jdongelmans]: https://github.com/jdongelmans
409
+ [@petergoldstein]: https://github.com/petergoldstein
410
+ [@ClearlyClaire]: https://github.com/ClearlyClaire
data/README.md CHANGED
@@ -16,7 +16,7 @@ Makes your Ruby/Rails web server become a functional [WebAuthn Relying Party](ht
16
16
 
17
17
  Takes care of the [server-side operations](https://www.w3.org/TR/webauthn/#rp-operations) needed to
18
18
  [register](https://www.w3.org/TR/webauthn/#registration) or [authenticate](https://www.w3.org/TR/webauthn/#authentication)
19
- a user [credential](https://www.w3.org/TR/webauthn/#public-key-credential), including the necessary cryptographic checks.
19
+ a user's [public key credential](https://www.w3.org/TR/webauthn/#public-key-credential) (also called a "passkey"), including the necessary cryptographic checks.
20
20
 
21
21
  ## Table of Contents
22
22
 
@@ -52,7 +52,7 @@ WebAuthn (Web Authentication) is a W3C standard for secure public-key authentica
52
52
 
53
53
  - WebAuthn [W3C Recommendation](https://www.w3.org/TR/webauthn/) (i.e. "The Standard")
54
54
  - [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) in MDN
55
- - How to use [WebAuthn in Android apps](https://developers.google.com/identity/fido/android/native-apps)
55
+ - How to use WebAuthn in native [Android](https://developers.google.com/identity/fido/android/native-apps) or [macOS/iOS/iPadOS](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication) apps.
56
56
  - [Security Benefits for WebAuthn Servers (a.k.a Relying Parties)](https://www.w3.org/TR/webauthn/#sctn-rp-benefits)
57
57
 
58
58
  ## Prerequisites
@@ -89,13 +89,15 @@ Or install it yourself as:
89
89
 
90
90
  ## Usage
91
91
 
92
- You can find a working example on how to use this gem in a __Rails__ app in [webauthn-rails-demo-app](https://github.com/cedarcode/webauthn-rails-demo-app).
92
+ You can find a working example on how to use this gem in a pasword-less login in a __Rails__ app in [webauthn-rails-demo-app](https://github.com/cedarcode/webauthn-rails-demo-app). If you want to see an example on how to use this gem as a second factor authenticator in a __Rails__ application instead, you can check it in [webauthn-2fa-rails-demo](https://github.com/cedarcode/webauthn-2fa-rails-demo).
93
93
 
94
94
  If you are migrating an existing application from the legacy FIDO U2F JavaScript API to WebAuthn, also refer to
95
95
  [`docs/u2f_migration.md`](docs/u2f_migration.md).
96
96
 
97
97
  ### Configuration
98
98
 
99
+ If you have a multi-tenant application or just need to configure WebAuthn differently for separate parts of your application (e.g. if your users authenticate to different subdomains in the same application), we strongly recommend you look at this [Advanced Configuration](docs/advanced_configuration.md) section instead of this.
100
+
99
101
  For a Rails application this would go in `config/initializers/webauthn.rb`.
100
102
 
101
103
  ```ruby
@@ -0,0 +1,174 @@
1
+ # Advanced Configuration
2
+
3
+ ## Global vs Instance Based Configuration
4
+
5
+ Which approach suits best your needs will depend on the architecture of your application and how do your users need to register and authenticate to it.
6
+
7
+ If you have a multi-tenant application, or any application segmenation, where your users register and authenticate to each of these tenants or segments individuallly using different hostnames, or with different security needs, you need to go through [Instance Based Configuration](#instance-based-configuration).
8
+
9
+ However, if your application is served for just one hostname, or else if your users authenticate to only one subdmain (e.g. your application serves www.example.com and admin.example.com but all you users authenticate through auth.example.com) you can still rely on one [Global Configuration](../README.md#configuration).
10
+
11
+ If you are still not sure, or want to keep your options open, be aware that [Instance Based Configuration](#instance-based-configuration) is also a valid way of defining a single instance configuration and how you share such configuration across your application, it's up to you.
12
+
13
+
14
+ ## Instance Based Configuration
15
+
16
+ Intead of the [Global Configuration](../README.md#configuration) you place in `config/initializers/webauthn.rb`,
17
+ you can now have an on-demand instance of `WebAuthn::RelyingParty` with the same configuration options, that
18
+ you can build anywhere in you application, in the following way:
19
+
20
+ ```ruby
21
+ relying_party = WebAuthn::RelyingParty.new(
22
+ # This value needs to match `window.location.origin` evaluated by
23
+ # the User Agent during registration and authentication ceremonies.
24
+ origin: "https://admin.example.com",
25
+
26
+ # Relying Party name for display purposes
27
+ name: "Admin Site for Example Inc."
28
+
29
+ # Optionally configure a client timeout hint, in milliseconds.
30
+ # This hint specifies how long the browser should wait for any
31
+ # interaction with the user.
32
+ # This hint may be overridden by the browser.
33
+ # https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
34
+ # credential_options_timeout: 120_000
35
+
36
+ # You can optionally specify a different Relying Party ID
37
+ # (https://www.w3.org/TR/webauthn/#relying-party-identifier)
38
+ # if it differs from the default one.
39
+ #
40
+ # In this case the default would be "admin.example.com", but you can set it to
41
+ # the suffix "example.com"
42
+ #
43
+ # id: "example.com"
44
+
45
+ # Configure preferred binary-to-text encoding scheme. This should match the encoding scheme
46
+ # used in your client-side (user agent) code before sending the credential to the server.
47
+ # Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding.
48
+ #
49
+ # encoding: :base64url
50
+
51
+ # Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1"
52
+ # Default: ["ES256", "PS256", "RS256"]
53
+ #
54
+ # algorithms: ["ES384"]
55
+ )
56
+ ```
57
+
58
+ ## Instance Based API
59
+
60
+ **DISCLAIMER: This API was released on version 3.0.0.alpha1 and is still under evaluation. Although it has been throughly tested and it is fully functional it might be changed until the final release of version 3.0.0.**
61
+
62
+ The explanation for each ceremony can be found in depth in [Credential Registration](../README.md#credential-registration) and [Credential Authentication](../README.md#credential-authentication) but if you choose this instance based approach to define your WebAuthn configurations and assuming `relying_party` is the result of an instance you get through `WebAuthn::RelytingParty.new(...)` the code in those explanations needs to be updated to:
63
+
64
+ ### Credential Registration
65
+
66
+ #### Initiation phase
67
+
68
+ ```ruby
69
+ # Generate and store the WebAuthn User ID the first time the user registers a credential
70
+ if !user.webauthn_id
71
+ user.update!(webauthn_id: WebAuthn.generate_user_id)
72
+ end
73
+
74
+ options = relying_party.options_for_registration(
75
+ user: { id: user.webauthn_id, name: user.name },
76
+ exclude: user.credentials.map { |c| c.webauthn_id }
77
+ )
78
+
79
+ # Store the newly generated challenge somewhere so you can have it
80
+ # for the verification phase.
81
+ session[:creation_challenge] = options.challenge
82
+
83
+ # Send `options` back to the browser, so that they can be used
84
+ # to call `navigator.credentials.create({ "publicKey": options })`
85
+ #
86
+ # You can call `options.as_json` to get a ruby hash with a JSON representation if needed.
87
+
88
+ # If inside a Rails controller, `render json: options` will just work.
89
+ # I.e. it will encode and convert the options to JSON automatically.
90
+
91
+ # For your frontend code, you might find @github/webauthn-json npm package useful.
92
+ # Especially for handling the necessary decoding of the options, and sending the
93
+ # `PublicKeyCredential` object back to the server.
94
+ ```
95
+
96
+ #### Verification phase
97
+
98
+ ```ruby
99
+ # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back
100
+ # in params[:publicKeyCredential]:
101
+ begin
102
+ webauthn_credential = relying_party.verify_registration(
103
+ params[:publicKeyCredential],
104
+ params[:create_challenge]
105
+ )
106
+
107
+ # Store Credential ID, Credential Public Key and Sign Count for future authentications
108
+ user.credentials.create!(
109
+ webauthn_id: webauthn_credential.id,
110
+ public_key: webauthn_credential.public_key,
111
+ sign_count: webauthn_credential.sign_count
112
+ )
113
+ rescue WebAuthn::Error => e
114
+ # Handle error
115
+ end
116
+ ```
117
+
118
+ ### Credential Authentication
119
+
120
+ #### Initiation phase
121
+
122
+ ```ruby
123
+ options = relying_party.options_for_get(allow: user.credentials.map { |c| c.webauthn_id })
124
+
125
+ # Store the newly generated challenge somewhere so you can have it
126
+ # for the verification phase.
127
+ session[:authentication_challenge] = options.challenge
128
+
129
+ # Send `options` back to the browser, so that they can be used
130
+ # to call `navigator.credentials.get({ "publicKey": options })`
131
+
132
+ # You can call `options.as_json` to get a ruby hash with a JSON representation if needed.
133
+
134
+ # If inside a Rails controller, `render json: options` will just work.
135
+ # I.e. it will encode and convert the options to JSON automatically.
136
+
137
+ # For your frontend code, you might find @github/webauthn-json npm package useful.
138
+ # Especially for handling the necessary decoding of the options, and sending the
139
+ # `PublicKeyCredential` object back to the server.
140
+ ```
141
+
142
+ #### Verification phase
143
+
144
+ ```ruby
145
+ begin
146
+ # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back
147
+ # in params[:publicKeyCredential]:
148
+ webauthn_credential, stored_credential = relying_party.verify_authentication(
149
+ params[:publicKeyCredential],
150
+ session[:authentication_challenge]
151
+ ) do
152
+ # the returned object needs to respond to #public_key and #sign_count
153
+ user.credentials.find_by(webauthn_id: webauthn_credential.id)
154
+ end
155
+
156
+ # Update the stored credential sign count with the value from `webauthn_credential.sign_count`
157
+ stored_credential.update!(sign_count: webauthn_credential.sign_count)
158
+
159
+ # Continue with successful sign in or 2FA verification...
160
+
161
+ rescue WebAuthn::SignCountVerificationError => e
162
+ # Cryptographic verification of the authenticator data succeeded, but the signature counter was less then or equal
163
+ # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or
164
+ # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter
165
+ rescue WebAuthn::Error => e
166
+ # Handle error
167
+ end
168
+ ```
169
+
170
+ ## Moving from Global to Instance Based Configuration
171
+
172
+ Adding a configuration for a new instance does not mean you need to get rid of your Global configuration. They can co-exist in your application and be both available for the different usages you might have. `WebAuthn.configuration.relying_party` will always return the global one while `WebAuthn::RelyingParty.new`, executed anywhere in your codebase, will allow you to create a different instance as you see the need. They will not collide and instead operate in isolation without any shared state.
173
+
174
+ The gem API described in the current [Usage](../README.md#usage) section for the [Global Configuration](../README.md#configuration) approach will still valid but the [Instance Based API](#instance-based-api) also works with the global `relying_party` that is maintain globally at `WebAuthn.configuration.relying_party`.
@@ -53,7 +53,7 @@ migrated_credential.authenticator_data.sign_count
53
53
 
54
54
  ## Authenticate migrated U2F credentials
55
55
 
56
- Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#authentication),
56
+ Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#initiation-phase-1),
57
57
  you need to specify the [FIDO AppID extension](https://www.w3.org/TR/webauthn/#sctn-appid-extension) for U2F migratedq
58
58
  credentials. The WebAuthn standard explains:
59
59
 
@@ -65,32 +65,26 @@ For the earlier given example `domain` this means:
65
65
  - FIDO AppID: `https://login.example.com`
66
66
  - Valid RP IDs: `login.example.com` (default) and `example.com`
67
67
 
68
+ You can request the use of the `appid` extension by setting the AppID in the configuration, like this:
69
+
70
+ ```ruby
71
+ WebAuthn.configure do |config|
72
+ config.legacy_u2f_appid = "https://login.example.com"
73
+ end
74
+ ```
75
+
76
+ By doing this, the `appid` extension will be automatically requested when generating the options for get:
77
+
68
78
  ```ruby
69
- credential_request_options = WebAuthn.credential_request_options
70
- credential_request_options[:extensions] = { appid: domain.to_s }
79
+ options = WebAuthn::Credential.options_for_get
71
80
  ```
72
81
 
73
82
  On the frontend, in the resolved value from `navigator.credentials.get({ "publicKey": credentialRequestOptions })` add
74
83
  a call to [getClientExtensionResults()](https://www.w3.org/TR/webauthn/#dom-publickeycredential-getclientextensionresults)
75
84
  and send its result to your backend alongside the `id`/`rawId` and `response` values. If the authenticator used the AppID
76
- extension, the returned value will contain `{ "appid": true }`. In the example below, we use `clientExtensionResults`.
85
+ extension, the returned value will contain `{ "appid": true }`.
77
86
 
78
- During authentication verification phase, you must pass either the original AppID or the RP ID as the `rp_id` argument:
87
+ During authentication verification phase, if you followed the [verification phase documentation](https://github.com/cedarcode/webauthn-ruby#verification-phase-1) and have set the AppID in the config, the method `PublicKeyCredentialWithAssertion#verify` will be smart enough to determine if it should use the AppID or the RP ID to verify the WebAuthn credential, depending on the output of the `appid` client extension:
79
88
 
80
89
  > If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the `rpIdHash` to be
81
90
  > the hash of the _AppID_, not the RP ID.
82
-
83
- ```ruby
84
- assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
85
- credential_id: params[:id],
86
- authenticator_data: params[:response][:authenticatorData],
87
- client_data_json: params[:response][:clientDataJSON],
88
- signature: params[:response][:signature],
89
- )
90
-
91
- assertion_response.verify(
92
- expected_challenge,
93
- allowed_credentials: [credential],
94
- rp_id: params[:clientExtensionResults][:appid] ? domain.to_s : domain.host,
95
- )
96
- ```
@@ -10,18 +10,22 @@ module WebAuthn
10
10
  class AttestationObject
11
11
  extend Forwardable
12
12
 
13
- def self.deserialize(attestation_object)
14
- from_map(CBOR.decode(attestation_object))
13
+ def self.deserialize(attestation_object, relying_party)
14
+ from_map(CBOR.decode(attestation_object), relying_party)
15
15
  end
16
16
 
17
- def self.from_map(map)
17
+ def self.from_map(map, relying_party)
18
18
  new(
19
19
  authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]),
20
- attestation_statement: WebAuthn::AttestationStatement.from(map["fmt"], map["attStmt"])
20
+ attestation_statement: WebAuthn::AttestationStatement.from(
21
+ map["fmt"],
22
+ map["attStmt"],
23
+ relying_party: relying_party
24
+ )
21
25
  )
22
26
  end
23
27
 
24
- attr_reader :authenticator_data, :attestation_statement
28
+ attr_reader :authenticator_data, :attestation_statement, :relying_party
25
29
 
26
30
  def initialize(authenticator_data:, attestation_statement:)
27
31
  @authenticator_data = authenticator_data
@@ -37,10 +37,10 @@ module WebAuthn
37
37
  private
38
38
 
39
39
  def valid_nonce?(authenticator_data, client_data_hash)
40
- extension = cred_cert&.extensions&.detect { |ext| ext.oid == NONCE_EXTENSION_OID }
40
+ extension = cred_cert&.find_extension(NONCE_EXTENSION_OID)
41
41
 
42
42
  if extension
43
- sequence = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(extension.to_der).value[1].value)
43
+ sequence = OpenSSL::ASN1.decode(extension.value_der)
44
44
 
45
45
  sequence.tag == OpenSSL::ASN1::SEQUENCE &&
46
46
  sequence.value.size == 1 &&
@@ -28,8 +28,9 @@ module WebAuthn
28
28
  class Base
29
29
  AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
30
30
 
31
- def initialize(statement)
31
+ def initialize(statement, relying_party = WebAuthn.configuration.relying_party)
32
32
  @statement = statement
33
+ @relying_party = relying_party
33
34
  end
34
35
 
35
36
  def valid?(_authenticator_data, _client_data_hash)
@@ -45,20 +46,18 @@ module WebAuthn
45
46
  end
46
47
 
47
48
  def attestation_certificate_key_id
48
- raw_subject_key_identifier&.unpack("H*")&.[](0)
49
+ attestation_certificate.subject_key_identifier&.unpack("H*")&.[](0)
49
50
  end
50
51
 
51
52
  private
52
53
 
53
- attr_reader :statement
54
+ attr_reader :statement, :relying_party
54
55
 
55
56
  def matching_aaguid?(attested_credential_data_aaguid)
56
- extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
57
+ extension = attestation_certificate&.find_extension(AAGUID_EXTENSION_OID)
57
58
  if extension
58
- # `extension.value` mangles data into ASCII, so we must manually compare bytes
59
- # see https://github.com/ruby/openssl/pull/234
60
- extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
61
- attested_credential_data_aaguid
59
+ aaguid_value = OpenSSL::ASN1.decode(extension.value_der).value
60
+ aaguid_value == attested_credential_data_aaguid
62
61
  else
63
62
  true
64
63
  end
@@ -95,10 +94,10 @@ module WebAuthn
95
94
 
96
95
  def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
97
96
  if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
98
- configuration.acceptable_attestation_types.include?(attestation_type) &&
97
+ relying_party.acceptable_attestation_types.include?(attestation_type) &&
99
98
  valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id)
100
99
  else
101
- configuration.acceptable_attestation_types.include?(attestation_type)
100
+ relying_party.acceptable_attestation_types.include?(attestation_type)
102
101
  end
103
102
  end
104
103
 
@@ -122,7 +121,7 @@ module WebAuthn
122
121
 
123
122
  def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
124
123
  root_certificates =
125
- configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder|
124
+ relying_party.attestation_root_certificates_finders.reduce([]) do |certs, finder|
126
125
  if certs.empty?
127
126
  finder.find(
128
127
  attestation_format: format,
@@ -141,15 +140,6 @@ module WebAuthn
141
140
  end
142
141
  end
143
142
 
144
- def raw_subject_key_identifier
145
- extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" }
146
- return unless extension
147
-
148
- ext_asn1 = OpenSSL::ASN1.decode(extension.to_der)
149
- ext_value = ext_asn1.value.last
150
- OpenSSL::ASN1.decode(ext_value.value).value
151
- end
152
-
153
143
  def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
154
144
  raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
155
145
 
@@ -169,14 +159,10 @@ module WebAuthn
169
159
  def cose_algorithm
170
160
  @cose_algorithm ||=
171
161
  COSE::Algorithm.find(algorithm).tap do |alg|
172
- alg && configuration.algorithms.include?(alg.name) ||
162
+ alg && relying_party.algorithms.include?(alg.name) ||
173
163
  raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
174
164
  end
175
165
  end
176
-
177
- def configuration
178
- WebAuthn.configuration
179
- end
180
166
  end
181
167
  end
182
168
  end
@@ -46,7 +46,7 @@ module WebAuthn
46
46
 
47
47
  attestation_certificate.version == 2 &&
48
48
  subject.assoc('OU')&.at(1) == "Authenticator Attestation" &&
49
- attestation_certificate.extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == 'CA:FALSE'
49
+ attestation_certificate.find_extension('basicConstraints')&.value == 'CA:FALSE'
50
50
  else
51
51
  true
52
52
  end
@@ -42,7 +42,7 @@ module WebAuthn
42
42
  OpenSSL::Digest.digest(cose_algorithm.hash_function, certified_extra_data),
43
43
  signature_algorithm: tpm_algorithm[:signature],
44
44
  hash_algorithm: tpm_algorithm[:hash],
45
- root_certificates: root_certificates(aaguid: aaguid)
45
+ trusted_certificates: root_certificates(aaguid: aaguid)
46
46
  )
47
47
 
48
48
  key_attestation.valid? && key_attestation.key && key_attestation.key.to_pem == key.to_pem
@@ -54,7 +54,7 @@ module WebAuthn
54
54
  end
55
55
 
56
56
  def default_root_certificates
57
- ::TPM::KeyAttestation::ROOT_CERTIFICATES
57
+ ::TPM::KeyAttestation::TRUSTED_CERTIFICATES
58
58
  end
59
59
 
60
60
  def tpm_algorithm
@@ -31,11 +31,11 @@ module WebAuthn
31
31
  ATTESTATION_FORMAT_APPLE => WebAuthn::AttestationStatement::Apple
32
32
  }.freeze
33
33
 
34
- def self.from(format, statement)
34
+ def self.from(format, statement, relying_party: WebAuthn.configuration.relying_party)
35
35
  klass = FORMAT_TO_CLASS[format]
36
36
 
37
37
  if klass
38
- klass.new(statement)
38
+ klass.new(statement, relying_party)
39
39
  else
40
40
  raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
41
41
  end
@@ -10,8 +10,8 @@ module WebAuthn
10
10
  class SignCountVerificationError < VerificationError; end
11
11
 
12
12
  class AuthenticatorAssertionResponse < AuthenticatorResponse
13
- def self.from_client(response)
14
- encoder = WebAuthn.configuration.encoder
13
+ def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
14
+ encoder = relying_party.encoder
15
15
 
16
16
  user_handle =
17
17
  if response["userHandle"]
@@ -22,7 +22,8 @@ module WebAuthn
22
22
  authenticator_data: encoder.decode(response["authenticatorData"]),
23
23
  client_data_json: encoder.decode(response["clientDataJSON"]),
24
24
  signature: encoder.decode(response["signature"]),
25
- user_handle: user_handle
25
+ user_handle: user_handle,
26
+ relying_party: relying_party
26
27
  )
27
28
  end
28
29
 
@@ -18,12 +18,13 @@ module WebAuthn
18
18
  class AuthenticatorAttestationResponse < AuthenticatorResponse
19
19
  extend Forwardable
20
20
 
21
- def self.from_client(response)
22
- encoder = WebAuthn.configuration.encoder
21
+ def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
22
+ encoder = relying_party.encoder
23
23
 
24
24
  new(
25
25
  attestation_object: encoder.decode(response["attestationObject"]),
26
- client_data_json: encoder.decode(response["clientDataJSON"])
26
+ client_data_json: encoder.decode(response["clientDataJSON"]),
27
+ relying_party: relying_party
27
28
  )
28
29
  end
29
30
 
@@ -33,13 +34,14 @@ module WebAuthn
33
34
  super(**options)
34
35
 
35
36
  @attestation_object_bytes = attestation_object
37
+ @relying_party = relying_party
36
38
  end
37
39
 
38
40
  def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
39
41
  super
40
42
 
41
43
  verify_item(:attested_credential)
42
- if WebAuthn.configuration.verify_attestation_statement
44
+ if relying_party.verify_attestation_statement
43
45
  verify_item(:attestation_statement)
44
46
  end
45
47
 
@@ -47,7 +49,7 @@ module WebAuthn
47
49
  end
48
50
 
49
51
  def attestation_object
50
- @attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes)
52
+ @attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes, relying_party)
51
53
  end
52
54
 
53
55
  def_delegators(
@@ -63,14 +65,15 @@ module WebAuthn
63
65
 
64
66
  private
65
67
 
66
- attr_reader :attestation_object_bytes
68
+ attr_reader :attestation_object_bytes, :relying_party
67
69
 
68
70
  def type
69
71
  WebAuthn::TYPES[:create]
70
72
  end
71
73
 
72
74
  def valid_attested_credential?
73
- attestation_object.valid_attested_credential?
75
+ attestation_object.valid_attested_credential? &&
76
+ relying_party.algorithms.include?(authenticator_data.credential.algorithm)
74
77
  end
75
78
 
76
79
  def valid_attestation_statement?