webauthn 2.5.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +3 -7
- data/.github/workflows/git.yml +21 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +42 -1
- data/README.md +5 -3
- data/docs/advanced_configuration.md +174 -0
- data/docs/u2f_migration.md +14 -20
- data/lib/webauthn/attestation_object.rb +9 -5
- data/lib/webauthn/attestation_statement/apple.rb +2 -2
- data/lib/webauthn/attestation_statement/base.rb +11 -25
- data/lib/webauthn/attestation_statement/packed.rb +1 -1
- data/lib/webauthn/attestation_statement/tpm.rb +2 -2
- data/lib/webauthn/attestation_statement.rb +2 -2
- data/lib/webauthn/authenticator_assertion_response.rb +4 -3
- data/lib/webauthn/authenticator_attestation_response.rb +10 -7
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +10 -5
- data/lib/webauthn/authenticator_data.rb +10 -2
- data/lib/webauthn/authenticator_response.rb +7 -7
- data/lib/webauthn/configuration.rb +38 -38
- data/lib/webauthn/credential.rb +5 -4
- data/lib/webauthn/fake_authenticator/attestation_object.rb +8 -0
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +20 -5
- data/lib/webauthn/fake_authenticator.rb +9 -1
- data/lib/webauthn/fake_client.rb +10 -2
- data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
- data/lib/webauthn/public_key_credential/options.rb +9 -8
- data/lib/webauthn/public_key_credential/request_options.rb +11 -1
- data/lib/webauthn/public_key_credential.rb +24 -5
- data/lib/webauthn/public_key_credential_with_assertion.rb +14 -1
- data/lib/webauthn/relying_party.rb +120 -0
- data/lib/webauthn/u2f_migrator.rb +4 -1
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +3 -5
- metadata +16 -45
- data/Appraisals +0 -9
- data/gemfiles/openssl_2_1.gemfile +0 -7
- data/gemfiles/openssl_2_2.gemfile +0 -7
- data/lib/webauthn/security_utils.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b133f46bd003c9bfb9a8557bb8caeca7239fcd6c185f78e74e0ae578444d576e
|
4
|
+
data.tar.gz: 14a32debc72ddcfe7e752118ebd4db832b8a46a5628ee54830f30eada4159bcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b4ff5cee18575b517473fef7d8000d7e41f113b178378e64a4d2b9036d248f4295edd846a519ab76036a270c3066020fbe44847019812c2a5758f2b1b428f4c
|
7
|
+
data.tar.gz: f3ab22709cbf537982e163acc60dbe114217f223b18cb4cf681a3d932e2f31276f8286091e49da392356c1958ada7e2c07a5b86b48d57b597f965a57c1bcf63d
|
data/.github/workflows/build.yml
CHANGED
@@ -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@
|
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
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
|
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
|
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`.
|
data/docs/u2f_migration.md
CHANGED
@@ -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#
|
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
|
-
|
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 }`.
|
85
|
+
extension, the returned value will contain `{ "appid": true }`.
|
77
86
|
|
78
|
-
During authentication verification phase, you
|
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(
|
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&.
|
40
|
+
extension = cred_cert&.find_extension(NONCE_EXTENSION_OID)
|
41
41
|
|
42
42
|
if extension
|
43
|
-
sequence = OpenSSL::ASN1.decode(
|
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
|
-
|
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&.
|
57
|
+
extension = attestation_certificate&.find_extension(AAGUID_EXTENSION_OID)
|
57
58
|
if extension
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 &&
|
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.
|
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
|
-
|
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::
|
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 =
|
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 =
|
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
|
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?
|