webauthn 1.17.0 → 1.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +44 -0
- data/README.md +54 -46
- data/docs/u2f_migration.md +96 -0
- data/lib/webauthn.rb +0 -2
- data/lib/webauthn/attestation_statement/android_safetynet.rb +3 -5
- data/lib/webauthn/attestation_statement/fido_u2f.rb +1 -1
- data/lib/webauthn/attestation_statement/packed.rb +1 -1
- data/lib/webauthn/attestation_statement/tpm.rb +1 -1
- data/lib/webauthn/authenticator_assertion_response.rb +5 -1
- data/lib/webauthn/authenticator_attestation_response.rb +8 -1
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +5 -1
- data/lib/webauthn/configuration.rb +6 -0
- data/lib/webauthn/credential_creation_options.rb +1 -0
- data/lib/webauthn/credential_options.rb +4 -0
- data/lib/webauthn/credential_request_options.rb +5 -1
- data/lib/webauthn/encoder.rb +39 -0
- data/lib/webauthn/fake_authenticator.rb +27 -13
- data/lib/webauthn/fake_authenticator/attestation_object.rb +30 -8
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +3 -1
- data/lib/webauthn/fake_client.rb +41 -17
- data/lib/webauthn/public_key_credential.rb +54 -0
- data/lib/webauthn/u2f_migrator.rb +70 -0
- data/lib/webauthn/version.rb +1 -1
- data/webauthn-ruby.png +0 -0
- data/webauthn.gemspec +2 -2
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a6a15357ab32b764ce0694b5a6cc4950e9298e377c8a93f8b664befb67dffcb
|
4
|
+
data.tar.gz: 3481eba4bbdd21676eda5015c9ebc9d11a92422ab0d57b6fed1da31b2ebc9d14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78f3d1242e008dac5a7d1b66944f6e64dd2a11baced08a454ca587d5bb7b5a93b2e51052d044f8c34034355ab2536af96438b9178e9b6536e92366cd55fbe495
|
7
|
+
data.tar.gz: 1e4ecf4a58bad0213e0893834f57f5564ce9b2d9d76ad214e35a85d7ede4d693d0d9f55a289fe998ac4ee4479279d7c521afef98fd7ddce1eec1f849d346948d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.18.0] - 2019-07-27
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Ability to migrate U2F credentials to WebAuthn ([#211](https://github.com/cedarcode/webauthn-ruby/pull/211)) (@bdewater + @jdongelmans)
|
8
|
+
- Ability to skip attestation statement verification ([#219](https://github.com/cedarcode/webauthn-ruby/pull/219)) (@MaximeNdutiye)
|
9
|
+
- Ability to configure default credential options timeout ([#243](https://github.com/cedarcode/webauthn-ruby/pull/243)) (@MaximeNdutiye)
|
10
|
+
- AttestedCredentialData presence verification ([#237](https://github.com/cedarcode/webauthn-ruby/pull/237))
|
11
|
+
- FakeClient learns how to increment sign count ([#225](https://github.com/cedarcode/webauthn-ruby/pull/225))
|
12
|
+
|
13
|
+
### Fixed
|
14
|
+
|
15
|
+
- Properly verify SafetyNet certificates from input ([#233](https://github.com/cedarcode/webauthn-ruby/pull/233)) (@bdewater)
|
16
|
+
- FakeClient default origin URL ([#242](https://github.com/cedarcode/webauthn-ruby/pull/242)) (@kalebtesfay)
|
17
|
+
|
3
18
|
## [v1.17.0] - 2019-06-18
|
4
19
|
|
5
20
|
### Added
|
@@ -194,6 +209,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
|
|
194
209
|
- `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
|
195
210
|
- Works with ruby 2.5
|
196
211
|
|
212
|
+
[v1.18.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.17.0...v1.18.0/
|
197
213
|
[v1.17.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.16.0...v1.17.0/
|
198
214
|
[v1.16.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.15.0...v1.16.0/
|
199
215
|
[v1.15.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.14.0...v1.15.0/
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
## Contributing to webauthn-ruby
|
2
|
+
|
3
|
+
### How?
|
4
|
+
|
5
|
+
- Creating a new issue to report a bug
|
6
|
+
- Creating a new issue to suggest a new feature
|
7
|
+
- Commenting on an existing issue to answer an open question
|
8
|
+
- Commenting on an existing issue to ask the reporter for more details to aid reproducing the problem
|
9
|
+
- Improving documentation
|
10
|
+
- Creating a pull request that fixes an issue (see [beginner friendly issues](https://github.com/cedarcode/webauthn-ruby/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22))
|
11
|
+
- Creating a pull request that implements a new feature (worth first creating an issue to discuss the suggested feature)
|
12
|
+
|
13
|
+
### Development
|
14
|
+
|
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
|
+
|
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:
|
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
|
20
|
+
```
|
21
|
+
|
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).
|
23
|
+
|
24
|
+
### Styleguide
|
25
|
+
|
26
|
+
#### Ruby
|
27
|
+
|
28
|
+
We use [rubocop](https://rubygems.org/gems/rubocop) to check ruby code style.
|
29
|
+
|
30
|
+
#### Git commit messages
|
31
|
+
|
32
|
+
We try to follow [Conventional Commits](https://conventionalcommits.org) specification since `v1.17.0`.
|
33
|
+
|
34
|
+
On top of `fix` and `feat` types, we also use optional:
|
35
|
+
|
36
|
+
* __build__: Changes that affect the build system or external dependencies
|
37
|
+
* __ci__: Changes to the CI configuration files and scripts
|
38
|
+
* __docs__: Documentation only changes
|
39
|
+
* __perf__: A code change that improves performance
|
40
|
+
* __refactor__: A code change that neither fixes a bug nor adds a feature
|
41
|
+
* __style__: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
42
|
+
* __test__: Adding missing tests or correcting existing tests
|
43
|
+
|
44
|
+
Partially inspired in [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines).
|
data/README.md
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
#
|
1
|
+
# webauthn-ruby
|
2
|
+
|
3
|
+
![banner](webauthn-ruby.png)
|
4
|
+
|
5
|
+
[![Gem](https://img.shields.io/gem/v/webauthn.svg?style=flat-square)](https://rubygems.org/gems/webauthn)
|
6
|
+
[![Travis](https://img.shields.io/travis/cedarcode/webauthn-ruby/master.svg?style=flat-square)](https://travis-ci.org/cedarcode/webauthn-ruby)
|
7
|
+
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-informational.svg?style=flat-square)](https://conventionalcommits.org)
|
8
|
+
[![Join the chat at https://gitter.im/cedarcode/webauthn-ruby](https://badges.gitter.im/cedarcode/webauthn-ruby.svg)](https://gitter.im/cedarcode/webauthn-ruby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
9
|
+
|
10
|
+
> WebAuthn ruby server library
|
2
11
|
|
3
12
|
Makes your Ruby/Rails web server become a functional [WebAuthn Relying Party](https://www.w3.org/TR/webauthn/#webauthn-relying-party).
|
4
13
|
|
@@ -6,24 +15,40 @@ Takes care of the [server-side operations](https://www.w3.org/TR/webauthn/#rp-op
|
|
6
15
|
[register](https://www.w3.org/TR/webauthn/#registration) or [authenticate](https://www.w3.org/TR/webauthn/#authentication)
|
7
16
|
a user [credential](https://www.w3.org/TR/webauthn/#public-key-credential), including the necessary cryptographic checks.
|
8
17
|
|
9
|
-
|
10
|
-
|
11
|
-
[
|
18
|
+
## Table of Contents
|
19
|
+
|
20
|
+
- [Security](#security)
|
21
|
+
- [Background](#background)
|
22
|
+
- [Prerequisites](#prerequisites)
|
23
|
+
- [Install](#install)
|
24
|
+
- [Usage](#usage)
|
25
|
+
- [API](#api)
|
26
|
+
- [Attestation Statement Formats](#attestation-statement-formats)
|
27
|
+
- [Testing Your Integration](#testing-your-integration)
|
28
|
+
- [Contributing](#contributing)
|
29
|
+
- [License](#license)
|
12
30
|
|
13
|
-
##
|
31
|
+
## Security
|
32
|
+
|
33
|
+
If you have discovered a security bug, please send an email to security@cedarcode.com instead of posting to the GitHub issue tracker.
|
14
34
|
|
15
|
-
|
35
|
+
## Background
|
16
36
|
|
17
|
-
|
37
|
+
### What is WebAuthn?
|
18
38
|
|
19
39
|
WebAuthn (Web Authentication) is a W3C standard for secure public-key authentication on the Web supported by all leading browsers and platforms.
|
20
40
|
|
41
|
+
#### Good Intros
|
42
|
+
|
43
|
+
- [Guide to Web Authentication](https://webauthn.guide) by Duo
|
44
|
+
- [What is WebAuthn?](https://www.yubico.com/webauthn/) by Yubico
|
45
|
+
|
46
|
+
#### In Depth
|
47
|
+
|
21
48
|
- WebAuthn [W3C Recommendation](https://www.w3.org/TR/webauthn/) (i.e. "The Standard")
|
22
|
-
- WebAuthn [intro](https://www.yubico.com/webauthn/) by Yubico
|
23
|
-
- WebAuthn [article](https://en.wikipedia.org/wiki/WebAuthn) in Wikipedia
|
24
49
|
- [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) in MDN
|
25
|
-
- WebAuthn [article with talk](https://developers.google.com/web/updates/2018/05/webauthn) in Google Developers
|
26
50
|
- How to use [WebAuthn in Android apps](https://developers.google.com/identity/fido/android/native-apps)
|
51
|
+
- [Security Benefits for WebAuthn Servers (a.k.a Relying Parties)](https://www.w3.org/TR/webauthn/#sctn-rp-benefits)
|
27
52
|
|
28
53
|
## Prerequisites
|
29
54
|
|
@@ -41,7 +66,7 @@ For a detailed picture about what is conformant and what not, you can refer to:
|
|
41
66
|
- [FIDO certified products](https://fidoalliance.org/certification/fido-certified-products)
|
42
67
|
|
43
68
|
|
44
|
-
##
|
69
|
+
## Install
|
45
70
|
|
46
71
|
Add this line to your application's Gemfile:
|
47
72
|
|
@@ -59,7 +84,10 @@ Or install it yourself as:
|
|
59
84
|
|
60
85
|
## Usage
|
61
86
|
|
62
|
-
|
87
|
+
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).
|
88
|
+
|
89
|
+
If you are migrating an existing application from the legacy FIDO U2F JavaScript API to WebAuthn, also refer to
|
90
|
+
[`docs/u2f_migration.md`](docs/u2f_migration.md).
|
63
91
|
|
64
92
|
### Configuration
|
65
93
|
|
@@ -74,6 +102,13 @@ WebAuthn.configure do |config|
|
|
74
102
|
# Relying Party name for display purposes
|
75
103
|
config.rp_name = "Example Inc."
|
76
104
|
|
105
|
+
# Optionally configure a client timeout hint, in milliseconds.
|
106
|
+
# This hint specifies how long the browser should wait for an
|
107
|
+
# attestation or an assertion response.
|
108
|
+
# This hint may be overridden by the browser.
|
109
|
+
# https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
|
110
|
+
config.credential_options_timeout = 120000
|
111
|
+
|
77
112
|
# You can optionally specify a different Relying Party ID
|
78
113
|
# (https://www.w3.org/TR/webauthn/#relying-party-identifier)
|
79
114
|
# if it differs from the default one.
|
@@ -204,6 +239,10 @@ end
|
|
204
239
|
# Update the stored sign count with the value from `assertion_response.authenticator_data.sign_count`
|
205
240
|
```
|
206
241
|
|
242
|
+
## API
|
243
|
+
|
244
|
+
_Pending_
|
245
|
+
|
207
246
|
## Attestation Statement Formats
|
208
247
|
|
209
248
|
| Attestation Statement Format | Supported? |
|
@@ -224,43 +263,12 @@ NOTE: Be aware that it is up to you to do "trust path validation" (steps 15 and
|
|
224
263
|
|
225
264
|
The Webauthn spec requires for data that is signed and authenticated. As a result, it can be difficult to create valid test authenticator data when testing your integration. webauthn-ruby exposes [WebAuthn::FakeClient](https://github.com/cedarcode/webauthn-ruby/blob/master/lib/webauthn/fake_client.rb) for you to use in your tests. Example usage can be found in [webauthn-ruby/spec/webauthn/authenticator_assertion_response_spec.rb](https://github.com/cedarcode/webauthn-ruby/blob/master/spec/webauthn/authenticator_assertion_response_spec.rb).
|
226
265
|
|
227
|
-
## Development
|
228
|
-
|
229
|
-
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.
|
230
|
-
|
231
|
-
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:
|
232
|
-
```shell
|
233
|
-
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
|
234
|
-
```
|
235
|
-
|
236
|
-
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).
|
237
|
-
|
238
|
-
### Commit message format
|
239
|
-
|
240
|
-
Each commit message follows the `<type>: <message>` format.
|
241
|
-
|
242
|
-
The "message" starts with lowercase and the "type" is one of:
|
243
|
-
|
244
|
-
* __build__: Changes that affect the build system or external dependencies
|
245
|
-
* __ci__: Changes to the CI configuration files and scripts
|
246
|
-
* __docs__: Documentation only changes
|
247
|
-
* __feat__: A new feature
|
248
|
-
* __fix__: A bug fix
|
249
|
-
* __perf__: A code change that improves performance
|
250
|
-
* __refactor__: A code change that neither fixes a bug nor adds a feature
|
251
|
-
* __style__: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
252
|
-
* __test__: Adding missing tests or correcting existing tests
|
253
|
-
|
254
|
-
Inspired in a subset of [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines).
|
255
|
-
|
256
266
|
## Contributing
|
257
267
|
|
258
|
-
|
259
|
-
|
260
|
-
### Security
|
268
|
+
See [the contributing file](CONTRIBUTING.md)!
|
261
269
|
|
262
|
-
|
270
|
+
Bug reports, feature suggestions, and pull requests are welcome on GitHub at https://github.com/cedarcode/webauthn-ruby.
|
263
271
|
|
264
272
|
## License
|
265
273
|
|
266
|
-
The
|
274
|
+
The library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# Migrating from U2F to WebAuthn
|
2
|
+
|
3
|
+
The Chromium team [recommends](https://groups.google.com/a/chromium.org/forum/#!msg/security-dev/BGWA1d7a6rI/W2avestmBAAJ)
|
4
|
+
application developers to switch from the U2F API to the WebAuthn API. This document describes how a Ruby application
|
5
|
+
using the [u2f gem by Castle](https://github.com/castle/ruby-u2f) can migrate existing credentials so that their users
|
6
|
+
do not experience interruption or need to re-register their security keys.
|
7
|
+
|
8
|
+
Note that the migration is one-way: credentials registered using WebAuthn cannot be made compatible with the U2F API.
|
9
|
+
It is recommended to successfully migrate authorization flows before migrating registration flows.
|
10
|
+
|
11
|
+
## Migrate registered U2F credentials
|
12
|
+
|
13
|
+
Assuming you have a registered credential per the u2f gem readme, base64 urlsafe encoded in a database:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# This domain will be used in all code examples. It's a single-facet app but a multi-facet AppID
|
17
|
+
# (e.g. https://example.com/app-id.json) will work as well.
|
18
|
+
domain = URI("https://login.example.com")
|
19
|
+
|
20
|
+
u2f_registration = U2F::U2F.new(domain.to_s).register!(u2f_challenge, u2f_register_response)
|
21
|
+
# => #<U2F::Registration:0x00007fd62f43d688
|
22
|
+
# @certificate=
|
23
|
+
# "MIIBCzCBsgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQDDAdVMkZUZXN0MB4XDTE5MDUzMDE3MjIwM1oXDTIwMDUyOTE3MjIwM1owEjEQMA4GA1UEAwwHVTJGVGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBMKpejumzlH6NxWIx2Ol+EManS9oX5nguG4RT43rZNkyn/zGjdJEhXksN5zT34rLZgFheBgkDGJCdtTPlhVK10wCgYIKoZIzj0EAwIDSAAwRQIhALYxFcROCeifWpv5+wNZIiaO/bGQg8rFBHCw3aHgehdZAiBJ3xFmQh7+Gjxt6CeAcY/k/VVAYu2vP4sUqXnCQFgJUA==",
|
24
|
+
# @key_handle="mbVMRTgzST5xLumckGztJ9VFW6veObfNIYWSn3sTqIY",
|
25
|
+
# @public_key="BOJQXlFg+ZfZKm48FNq2Ye5vSwOscE1i7YsGRSIjIe3GI0OXrSBDADDn0dQlz2iDzZ7LvCwiHz72U1qhVas3vus=">
|
26
|
+
```
|
27
|
+
|
28
|
+
The `U2fMigrator` class quacks like `WebAuthn::AuthenticatorAttestationResponse` and can be used similarly as documented
|
29
|
+
in the [registration verification phase](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#verification-phase).
|
30
|
+
Of course a `verify` instance method is not implemented, as there is no real interaction with an authenticator.
|
31
|
+
|
32
|
+
The migrator can be used to convert credentials in real time during authentication while keeping them stored in the U2F
|
33
|
+
format, and in a backfill task to store credentials in the new format, depending on how you are approaching your
|
34
|
+
migration.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require "webauthn/u2f_migrator"
|
38
|
+
|
39
|
+
migrated_credential = WebAuthn::U2fMigrator.new(
|
40
|
+
app_id: domain,
|
41
|
+
certificate: u2f_registration.certificate,
|
42
|
+
key_handle: u2f_registration.key_handle,
|
43
|
+
public_key: u2f_registration.public_key,
|
44
|
+
counter: u2f_registration.counter
|
45
|
+
)
|
46
|
+
migrated_credential.credential.id
|
47
|
+
# => "\x99\xB5LE83I>q.\xE9\x9C\x90l\xED'\xD5E[\xAB\xDE9\xB7\xCD!\x85\x92\x9F{\x13\xA8\x86"
|
48
|
+
migrated_credential.credential.public_key
|
49
|
+
# => "\xA5\x03& \x01!X \xE2P^Q`\xF9\x97\xD9*n<\x14\xDA\xB6a\xEEoK\x03\xACpMb\xED\x8B\x06E\"#!\xED\xC6\x01\x02\"X #C\x97\xAD C\x000\xE7\xD1\xD4%\xCFh\x83\xCD\x9E\xCB\xBC,\"\x1F>\xF6SZ\xA1U\xAB7\xBE\xEB"
|
50
|
+
migrated_credential.authenticator_data.sign_count
|
51
|
+
# => 41
|
52
|
+
```
|
53
|
+
|
54
|
+
## Authenticate migrated U2F credentials
|
55
|
+
|
56
|
+
Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#authentication),
|
57
|
+
you need to specify the [FIDO AppID extension](https://www.w3.org/TR/webauthn/#sctn-appid-extension) for U2F migratedq
|
58
|
+
credentials. The WebAuthn standard explains:
|
59
|
+
|
60
|
+
> The FIDO APIs use an alternative identifier for Relying Parties called an _AppID_, and any credentials created using
|
61
|
+
> those APIs will be scoped to that identifier. Without this extension, they would need to be re-registered in order to
|
62
|
+
> be scoped to an RP ID.
|
63
|
+
|
64
|
+
For the earlier given example `domain` this means:
|
65
|
+
- FIDO AppID: `https://login.example.com`
|
66
|
+
- Valid RP IDs: `login.example.com` (default) and `example.com`
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
credential_request_options = WebAuthn.credential_request_options
|
70
|
+
credential_request_options[:extensions] = { appid: domain.to_s }
|
71
|
+
```
|
72
|
+
|
73
|
+
On the frontend, in the resolved value from `navigator.credentials.get({ "publicKey": credentialRequestOptions })` add
|
74
|
+
a call to [getClientExtensionResults()](https://www.w3.org/TR/webauthn/#dom-publickeycredential-getclientextensionresults)
|
75
|
+
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`.
|
77
|
+
|
78
|
+
During authentication verification phase, you must pass either the original AppID or the RP ID as the `rp_id` argument:
|
79
|
+
|
80
|
+
> If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the `rpIdHash` to be
|
81
|
+
> 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
|
+
```
|
data/lib/webauthn.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "webauthn/authenticator_attestation_response"
|
4
|
-
require "webauthn/authenticator_assertion_response"
|
5
3
|
require "webauthn/configuration"
|
6
4
|
require "webauthn/credential_creation_options"
|
7
5
|
require "webauthn/credential_request_options"
|
@@ -6,7 +6,7 @@ require "webauthn/attestation_statement/base"
|
|
6
6
|
|
7
7
|
module WebAuthn
|
8
8
|
module AttestationStatement
|
9
|
-
# Implements https://www.w3.org/TR/
|
9
|
+
# Implements https://www.w3.org/TR/webauthn-1/#sctn-android-safetynet-attestation
|
10
10
|
class AndroidSafetynet < Base
|
11
11
|
def self.default_trust_store
|
12
12
|
OpenSSL::X509::Store.new.tap { |trust_store| trust_store.set_default_paths }
|
@@ -22,11 +22,9 @@ module WebAuthn
|
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
+
# FIXME: This should be a responsibility of AndroidSafetynet::AttestationResponse#verify
|
25
26
|
def trusted_attestation_certificate?(trust_store)
|
26
|
-
|
27
|
-
trust_store.add_cert(certificate)
|
28
|
-
end
|
29
|
-
trust_store.verify(attestation_certificate)
|
27
|
+
trust_store.verify(attestation_certificate, signing_certificates)
|
30
28
|
end
|
31
29
|
|
32
30
|
def valid_response?(authenticator_data, client_data_hash)
|
@@ -16,7 +16,7 @@ module WebAuthn
|
|
16
16
|
valid_format? &&
|
17
17
|
valid_certificate_public_key? &&
|
18
18
|
valid_credential_public_key?(authenticator_data.credential.public_key) &&
|
19
|
-
valid_aaguid?(authenticator_data.attested_credential_data.
|
19
|
+
valid_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
|
20
20
|
valid_signature?(authenticator_data, client_data_hash) &&
|
21
21
|
[WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, [attestation_certificate]]
|
22
22
|
end
|
@@ -18,7 +18,7 @@ module WebAuthn
|
|
18
18
|
valid_certificate_chain? &&
|
19
19
|
valid_ec_public_keys?(authenticator_data.credential) &&
|
20
20
|
meet_certificate_requirement? &&
|
21
|
-
matching_aaguid?(authenticator_data.attested_credential_data.
|
21
|
+
matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
|
22
22
|
valid_signature?(authenticator_data, client_data_hash) &&
|
23
23
|
attestation_type_and_trust_path
|
24
24
|
end
|
@@ -25,7 +25,7 @@ module WebAuthn
|
|
25
25
|
valid_attestation_certificate? &&
|
26
26
|
pub_area.valid?(authenticator_data.credential.public_key) &&
|
27
27
|
cert_info.valid?(statement["pubArea"], OpenSSL::Digest.digest(cose_algorithm.hash, att_to_be_signed)) &&
|
28
|
-
matching_aaguid?(authenticator_data.attested_credential_data.
|
28
|
+
matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
|
29
29
|
[attestation_type, attestation_trust_path]
|
30
30
|
when ATTESTATION_TYPE_ECDAA
|
31
31
|
raise(
|
@@ -13,12 +13,16 @@ module WebAuthn
|
|
13
13
|
class SignCountVerificationError < VerificationError; end
|
14
14
|
|
15
15
|
class AuthenticatorAssertionResponse < AuthenticatorResponse
|
16
|
-
|
16
|
+
attr_reader :user_handle
|
17
|
+
|
18
|
+
# FIXME: credential_id doesn't belong inside AuthenticatorAssertionResponse
|
19
|
+
def initialize(credential_id:, authenticator_data:, signature:, user_handle: nil, **options)
|
17
20
|
super(options)
|
18
21
|
|
19
22
|
@credential_id = credential_id
|
20
23
|
@authenticator_data_bytes = authenticator_data
|
21
24
|
@signature = signature
|
25
|
+
@user_handle = user_handle
|
22
26
|
end
|
23
27
|
|
24
28
|
def verify(expected_challenge, expected_origin = nil, allowed_credentials:, user_verification: nil, rp_id: nil)
|
@@ -11,6 +11,7 @@ require "webauthn/client_data"
|
|
11
11
|
|
12
12
|
module WebAuthn
|
13
13
|
class AttestationStatementVerificationError < VerificationError; end
|
14
|
+
class AttestedCredentialVerificationError < VerificationError; end
|
14
15
|
|
15
16
|
class AuthenticatorAttestationResponse < AuthenticatorResponse
|
16
17
|
attr_reader :attestation_type, :attestation_trust_path
|
@@ -24,7 +25,8 @@ module WebAuthn
|
|
24
25
|
def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil)
|
25
26
|
super
|
26
27
|
|
27
|
-
verify_item(:
|
28
|
+
verify_item(:attested_credential)
|
29
|
+
verify_item(:attestation_statement) if WebAuthn.configuration.verify_attestation_statement
|
28
30
|
|
29
31
|
true
|
30
32
|
end
|
@@ -58,6 +60,11 @@ module WebAuthn
|
|
58
60
|
WebAuthn::TYPES[:create]
|
59
61
|
end
|
60
62
|
|
63
|
+
def valid_attested_credential?
|
64
|
+
authenticator_data.attested_credential_data_included? &&
|
65
|
+
authenticator_data.attested_credential_data.valid?
|
66
|
+
end
|
67
|
+
|
61
68
|
def valid_attestation_statement?
|
62
69
|
@attestation_type, @attestation_trust_path = attestation_statement.valid?(authenticator_data, client_data.hash)
|
63
70
|
end
|
@@ -26,10 +26,14 @@ module WebAuthn
|
|
26
26
|
data.length >= AAGUID_LENGTH + ID_LENGTH_LENGTH && valid_credential_public_key?
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
29
|
+
def raw_aaguid
|
30
30
|
data_at(0, AAGUID_LENGTH)
|
31
31
|
end
|
32
32
|
|
33
|
+
def aaguid
|
34
|
+
raw_aaguid.unpack("H8H4H4H4H12").join("-")
|
35
|
+
end
|
36
|
+
|
33
37
|
def credential
|
34
38
|
@credential ||=
|
35
39
|
if id
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "openssl"
|
4
|
+
|
3
5
|
module WebAuthn
|
4
6
|
def self.configuration
|
5
7
|
@configuration ||= Configuration.new
|
@@ -20,9 +22,13 @@ module WebAuthn
|
|
20
22
|
attr_accessor :origin
|
21
23
|
attr_accessor :rp_id
|
22
24
|
attr_accessor :rp_name
|
25
|
+
attr_accessor :verify_attestation_statement
|
26
|
+
attr_accessor :credential_options_timeout
|
23
27
|
|
24
28
|
def initialize
|
25
29
|
@algorithms = DEFAULT_ALGORITHMS.dup
|
30
|
+
@verify_attestation_statement = true
|
31
|
+
@credential_options_timeout = 120000
|
26
32
|
end
|
27
33
|
end
|
28
34
|
end
|
@@ -17,7 +17,11 @@ module WebAuthn
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def to_h
|
20
|
-
options = {
|
20
|
+
options = {
|
21
|
+
challenge: challenge,
|
22
|
+
timeout: timeout,
|
23
|
+
allowCredentials: allow_credentials
|
24
|
+
}
|
21
25
|
|
22
26
|
if extensions
|
23
27
|
options[:extensions] = extensions
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module WebAuthn
|
6
|
+
class Encoder
|
7
|
+
attr_reader :encoding
|
8
|
+
|
9
|
+
def initialize(encoding = :base64)
|
10
|
+
@encoding = encoding
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode(data)
|
14
|
+
case encoding
|
15
|
+
when :base64
|
16
|
+
Base64.strict_encode64(data)
|
17
|
+
when :base64url
|
18
|
+
Base64.urlsafe_encode64(data, padding: false)
|
19
|
+
when nil, false
|
20
|
+
data
|
21
|
+
else
|
22
|
+
raise "Unsupported or unknown encoding: #{encoding}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def decode(data)
|
27
|
+
case encoding
|
28
|
+
when :base64
|
29
|
+
Base64.strict_decode64(data)
|
30
|
+
when :base64url
|
31
|
+
Base64.urlsafe_decode64(data)
|
32
|
+
when nil, false
|
33
|
+
data
|
34
|
+
else
|
35
|
+
raise "Unsupported or unknown encoding: #{encoding}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -12,22 +12,33 @@ module WebAuthn
|
|
12
12
|
@credentials = {}
|
13
13
|
end
|
14
14
|
|
15
|
-
def make_credential(
|
16
|
-
|
15
|
+
def make_credential(
|
16
|
+
rp_id:,
|
17
|
+
client_data_hash:,
|
18
|
+
user_present: true,
|
19
|
+
user_verified: false,
|
20
|
+
attested_credential_data: true,
|
21
|
+
sign_count: nil
|
22
|
+
)
|
23
|
+
credential_id, credential_key, credential_sign_count = new_credential
|
24
|
+
sign_count ||= credential_sign_count
|
17
25
|
|
18
|
-
|
26
|
+
credentials[rp_id] ||= {}
|
27
|
+
credentials[rp_id][credential_id] = {
|
28
|
+
credential_key: credential_key,
|
29
|
+
sign_count: sign_count + 1
|
30
|
+
}
|
31
|
+
|
32
|
+
AttestationObject.new(
|
19
33
|
client_data_hash: client_data_hash,
|
20
34
|
rp_id_hash: hashed(rp_id),
|
21
35
|
credential_id: credential_id,
|
22
36
|
credential_key: credential_key,
|
23
37
|
user_present: user_present,
|
24
|
-
user_verified: user_verified
|
38
|
+
user_verified: user_verified,
|
39
|
+
attested_credential_data: attested_credential_data,
|
40
|
+
sign_count: sign_count
|
25
41
|
).serialize
|
26
|
-
|
27
|
-
credentials[rp_id] ||= {}
|
28
|
-
credentials[rp_id][credential_id] = credential_key
|
29
|
-
|
30
|
-
attestation_object
|
31
42
|
end
|
32
43
|
|
33
44
|
def get_assertion(
|
@@ -36,22 +47,25 @@ module WebAuthn
|
|
36
47
|
user_present: true,
|
37
48
|
user_verified: false,
|
38
49
|
aaguid: AuthenticatorData::AAGUID,
|
39
|
-
sign_count:
|
50
|
+
sign_count: nil
|
40
51
|
)
|
41
52
|
credential_options = credentials[rp_id]
|
42
53
|
|
43
54
|
if credential_options
|
44
|
-
credential_id,
|
55
|
+
credential_id, credential = credential_options.first
|
56
|
+
credential_key = credential[:credential_key]
|
57
|
+
credential_sign_count = credential[:sign_count]
|
45
58
|
|
46
59
|
authenticator_data = AuthenticatorData.new(
|
47
60
|
rp_id_hash: hashed(rp_id),
|
48
61
|
user_present: user_present,
|
49
62
|
user_verified: user_verified,
|
50
63
|
aaguid: aaguid,
|
51
|
-
sign_count: sign_count,
|
64
|
+
sign_count: sign_count || credential_sign_count,
|
52
65
|
).serialize
|
53
66
|
|
54
67
|
signature = credential_key.sign("SHA256", authenticator_data + client_data_hash)
|
68
|
+
credential[:sign_count] += 1
|
55
69
|
|
56
70
|
{
|
57
71
|
credential_id: credential_id,
|
@@ -68,7 +82,7 @@ module WebAuthn
|
|
68
82
|
attr_reader :credentials
|
69
83
|
|
70
84
|
def new_credential
|
71
|
-
[SecureRandom.random_bytes(16), OpenSSL::PKey::EC.new("prime256v1").generate_key]
|
85
|
+
[SecureRandom.random_bytes(16), OpenSSL::PKey::EC.new("prime256v1").generate_key, 0]
|
72
86
|
end
|
73
87
|
|
74
88
|
def hashed(target)
|
@@ -12,7 +12,9 @@ module WebAuthn
|
|
12
12
|
credential_id:,
|
13
13
|
credential_key:,
|
14
14
|
user_present: true,
|
15
|
-
user_verified: false
|
15
|
+
user_verified: false,
|
16
|
+
attested_credential_data: true,
|
17
|
+
sign_count: 0
|
16
18
|
)
|
17
19
|
@client_data_hash = client_data_hash
|
18
20
|
@rp_id_hash = rp_id_hash
|
@@ -20,6 +22,8 @@ module WebAuthn
|
|
20
22
|
@credential_key = credential_key
|
21
23
|
@user_present = user_present
|
22
24
|
@user_verified = user_verified
|
25
|
+
@attested_credential_data = attested_credential_data
|
26
|
+
@sign_count = sign_count
|
23
27
|
end
|
24
28
|
|
25
29
|
def serialize
|
@@ -32,15 +36,33 @@ module WebAuthn
|
|
32
36
|
|
33
37
|
private
|
34
38
|
|
35
|
-
attr_reader
|
39
|
+
attr_reader(
|
40
|
+
:client_data_hash,
|
41
|
+
:rp_id_hash,
|
42
|
+
:credential_id,
|
43
|
+
:credential_key,
|
44
|
+
:user_present,
|
45
|
+
:user_verified,
|
46
|
+
:attested_credential_data,
|
47
|
+
:sign_count
|
48
|
+
)
|
36
49
|
|
37
50
|
def authenticator_data
|
38
|
-
@authenticator_data ||=
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
51
|
+
@authenticator_data ||=
|
52
|
+
begin
|
53
|
+
credential_data =
|
54
|
+
if attested_credential_data
|
55
|
+
{ id: credential_id, public_key: credential_key.public_key }
|
56
|
+
end
|
57
|
+
|
58
|
+
AuthenticatorData.new(
|
59
|
+
rp_id_hash: rp_id_hash,
|
60
|
+
credential: credential_data,
|
61
|
+
user_present: user_present,
|
62
|
+
user_verified: user_verified,
|
63
|
+
sign_count: 0
|
64
|
+
)
|
65
|
+
end
|
44
66
|
end
|
45
67
|
end
|
46
68
|
end
|
@@ -9,6 +9,8 @@ module WebAuthn
|
|
9
9
|
class AuthenticatorData
|
10
10
|
AAGUID = SecureRandom.random_bytes(16)
|
11
11
|
|
12
|
+
attr_reader :sign_count
|
13
|
+
|
12
14
|
def initialize(
|
13
15
|
rp_id_hash:,
|
14
16
|
credential: {
|
@@ -36,7 +38,7 @@ module WebAuthn
|
|
36
38
|
|
37
39
|
private
|
38
40
|
|
39
|
-
attr_reader :rp_id_hash, :credential, :
|
41
|
+
attr_reader :rp_id_hash, :credential, :user_present, :user_verified, :extensions
|
40
42
|
|
41
43
|
def flags
|
42
44
|
[
|
data/lib/webauthn/fake_client.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "base64"
|
4
4
|
require "openssl"
|
5
5
|
require "webauthn/authenticator_data"
|
6
|
+
require "webauthn/encoder"
|
6
7
|
require "webauthn/fake_authenticator"
|
7
8
|
|
8
9
|
module WebAuthn
|
@@ -11,13 +12,24 @@ module WebAuthn
|
|
11
12
|
|
12
13
|
attr_reader :origin, :token_binding
|
13
14
|
|
14
|
-
def initialize(
|
15
|
+
def initialize(
|
16
|
+
origin = fake_origin,
|
17
|
+
token_binding: nil,
|
18
|
+
authenticator: WebAuthn::FakeAuthenticator.new,
|
19
|
+
encoding: nil
|
20
|
+
)
|
15
21
|
@origin = origin
|
16
22
|
@token_binding = token_binding
|
17
23
|
@authenticator = authenticator
|
24
|
+
@encoding = encoding
|
18
25
|
end
|
19
26
|
|
20
|
-
def create(
|
27
|
+
def create(
|
28
|
+
challenge: fake_challenge,
|
29
|
+
rp_id: nil, user_present: true,
|
30
|
+
user_verified: false,
|
31
|
+
attested_credential_data: true
|
32
|
+
)
|
21
33
|
rp_id ||= URI.parse(origin).host
|
22
34
|
|
23
35
|
client_data_json = data_json_for(:create, challenge)
|
@@ -27,21 +39,30 @@ module WebAuthn
|
|
27
39
|
rp_id: rp_id,
|
28
40
|
client_data_hash: client_data_hash,
|
29
41
|
user_present: user_present,
|
30
|
-
user_verified: user_verified
|
42
|
+
user_verified: user_verified,
|
43
|
+
attested_credential_data: attested_credential_data
|
31
44
|
)
|
32
45
|
|
33
|
-
id =
|
46
|
+
id =
|
47
|
+
if attested_credential_data
|
48
|
+
WebAuthn::AuthenticatorData.new(CBOR.decode(attestation_object)["authData"]).credential.id
|
49
|
+
else
|
50
|
+
"id-for-pk-without-attested-credential-data"
|
51
|
+
end
|
34
52
|
|
53
|
+
# TODO: return camelCase string keys instead of snakecase symbols
|
35
54
|
{
|
36
|
-
|
55
|
+
type: "public-key",
|
56
|
+
id: Base64.urlsafe_encode64(id),
|
57
|
+
raw_id: encoder.encode(id),
|
37
58
|
response: {
|
38
|
-
attestation_object: attestation_object,
|
39
|
-
client_data_json: client_data_json
|
59
|
+
attestation_object: encoder.encode(attestation_object),
|
60
|
+
client_data_json: encoder.encode(client_data_json)
|
40
61
|
}
|
41
62
|
}
|
42
63
|
end
|
43
64
|
|
44
|
-
def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false, sign_count:
|
65
|
+
def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false, sign_count: nil)
|
45
66
|
rp_id ||= URI.parse(origin).host
|
46
67
|
|
47
68
|
client_data_json = data_json_for(:get, challenge)
|
@@ -55,24 +76,27 @@ module WebAuthn
|
|
55
76
|
sign_count: sign_count,
|
56
77
|
)
|
57
78
|
|
79
|
+
# TODO: return camelCase string keys instead of snakecase symbols
|
58
80
|
{
|
59
|
-
|
81
|
+
type: "public-key",
|
82
|
+
id: Base64.urlsafe_encode64(assertion[:credential_id]),
|
83
|
+
raw_id: encoder.encode(assertion[:credential_id]),
|
60
84
|
response: {
|
61
|
-
client_data_json: client_data_json,
|
62
|
-
authenticator_data: assertion[:authenticator_data],
|
63
|
-
signature: assertion[:signature]
|
85
|
+
client_data_json: encoder.encode(client_data_json),
|
86
|
+
authenticator_data: encoder.encode(assertion[:authenticator_data]),
|
87
|
+
signature: encoder.encode(assertion[:signature])
|
64
88
|
}
|
65
89
|
}
|
66
90
|
end
|
67
91
|
|
68
92
|
private
|
69
93
|
|
70
|
-
attr_reader :authenticator
|
94
|
+
attr_reader :authenticator, :encoding
|
71
95
|
|
72
96
|
def data_json_for(method, challenge)
|
73
97
|
data = {
|
74
98
|
type: type_for(method),
|
75
|
-
challenge:
|
99
|
+
challenge: Base64.urlsafe_encode64(challenge, padding: false),
|
76
100
|
origin: origin
|
77
101
|
}
|
78
102
|
|
@@ -83,8 +107,8 @@ module WebAuthn
|
|
83
107
|
data.to_json
|
84
108
|
end
|
85
109
|
|
86
|
-
def
|
87
|
-
|
110
|
+
def encoder
|
111
|
+
@encoder ||= WebAuthn::Encoder.new(encoding)
|
88
112
|
end
|
89
113
|
|
90
114
|
def hashed(data)
|
@@ -96,7 +120,7 @@ module WebAuthn
|
|
96
120
|
end
|
97
121
|
|
98
122
|
def fake_origin
|
99
|
-
"http://localhost#{rand(1000)}"
|
123
|
+
"http://localhost#{rand(1000)}.test"
|
100
124
|
end
|
101
125
|
|
102
126
|
def type_for(method)
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "base64"
|
4
|
+
require "webauthn/authenticator_assertion_response"
|
5
|
+
require "webauthn/authenticator_attestation_response"
|
6
|
+
require "webauthn/encoder"
|
4
7
|
|
5
8
|
module WebAuthn
|
6
9
|
class PublicKeyCredential
|
@@ -8,6 +11,43 @@ module WebAuthn
|
|
8
11
|
|
9
12
|
attr_reader :type, :id, :raw_id, :response
|
10
13
|
|
14
|
+
def self.from_create(credential, encoding: :base64)
|
15
|
+
encoder = WebAuthn::Encoder.new(encoding)
|
16
|
+
|
17
|
+
new(
|
18
|
+
type: credential["type"],
|
19
|
+
id: credential["id"],
|
20
|
+
raw_id: encoder.decode(credential["rawId"]),
|
21
|
+
response: WebAuthn::AuthenticatorAttestationResponse.new(
|
22
|
+
attestation_object: encoder.decode(credential["response"]["attestationObject"]),
|
23
|
+
client_data_json: encoder.decode(credential["response"]["clientDataJSON"])
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from_get(credential, encoding: :base64)
|
29
|
+
encoder = WebAuthn::Encoder.new(encoding)
|
30
|
+
|
31
|
+
user_handle =
|
32
|
+
if credential["response"]["userHandle"]
|
33
|
+
encoder.decode(credential["response"]["userHandle"])
|
34
|
+
end
|
35
|
+
|
36
|
+
new(
|
37
|
+
type: credential["type"],
|
38
|
+
id: credential["id"],
|
39
|
+
raw_id: encoder.decode(credential["rawId"]),
|
40
|
+
response: WebAuthn::AuthenticatorAssertionResponse.new(
|
41
|
+
# FIXME: credential_id doesn't belong inside AuthenticatorAssertionResponse
|
42
|
+
credential_id: Base64.urlsafe_decode64(credential["id"]),
|
43
|
+
authenticator_data: encoder.decode(credential["response"]["authenticatorData"]),
|
44
|
+
client_data_json: encoder.decode(credential["response"]["clientDataJSON"]),
|
45
|
+
signature: encoder.decode(credential["response"]["signature"]),
|
46
|
+
user_handle: user_handle
|
47
|
+
)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
11
51
|
def initialize(type:, id:, raw_id:, response:)
|
12
52
|
@type = type
|
13
53
|
@id = id
|
@@ -23,6 +63,20 @@ module WebAuthn
|
|
23
63
|
true
|
24
64
|
end
|
25
65
|
|
66
|
+
def public_key
|
67
|
+
response&.authenticator_data&.credential&.public_key
|
68
|
+
end
|
69
|
+
|
70
|
+
def user_handle
|
71
|
+
if response.is_a?(WebAuthn::AuthenticatorAssertionResponse)
|
72
|
+
response.user_handle
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def sign_count
|
77
|
+
response&.authenticator_data&.sign_count
|
78
|
+
end
|
79
|
+
|
26
80
|
private
|
27
81
|
|
28
82
|
def valid_type?
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'webauthn/fake_client'
|
4
|
+
require 'webauthn/attestation_statement/fido_u2f'
|
5
|
+
|
6
|
+
module WebAuthn
|
7
|
+
class U2fMigrator
|
8
|
+
def initialize(app_id:, certificate:, key_handle:, public_key:, counter:)
|
9
|
+
@app_id = app_id
|
10
|
+
@certificate = certificate
|
11
|
+
@key_handle = key_handle
|
12
|
+
@public_key = public_key
|
13
|
+
@counter = counter
|
14
|
+
end
|
15
|
+
|
16
|
+
def authenticator_data
|
17
|
+
@authenticator_data ||= WebAuthn::FakeAuthenticator::AuthenticatorData.new(
|
18
|
+
rp_id_hash: OpenSSL::Digest::SHA256.digest(@app_id.to_s),
|
19
|
+
credential: {
|
20
|
+
id: credential_id,
|
21
|
+
public_key: credential_cose_key
|
22
|
+
},
|
23
|
+
sign_count: @counter,
|
24
|
+
user_present: true,
|
25
|
+
user_verified: false,
|
26
|
+
aaguid: WebAuthn::AttestationStatement::FidoU2f::VALID_ATTESTED_AAGUID,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def credential
|
31
|
+
@credential ||= begin
|
32
|
+
hash = authenticator_data.send(:credential)
|
33
|
+
WebAuthn::AuthenticatorData::AttestedCredentialData::Credential.new(hash[:id], hash[:public_key].serialize)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def attestation_type
|
38
|
+
WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA
|
39
|
+
end
|
40
|
+
|
41
|
+
def attestation_trust_path
|
42
|
+
@attestation_trust_path ||= [OpenSSL::X509::Certificate.new(Base64.strict_decode64(@certificate))]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#u2f-authenticatorMakeCredential-interoperability
|
48
|
+
# Let credentialId be a credentialIdLength byte array initialized with CTAP1/U2F response key handle bytes.
|
49
|
+
def credential_id
|
50
|
+
Base64.urlsafe_decode64(@key_handle)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Let x9encodedUserPublicKey be the user public key returned in the U2F registration response message [U2FRawMsgs].
|
54
|
+
# Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value from ANS X9.62 /
|
55
|
+
# Sec-1 v2 uncompressed curve point representation [SEC1V2] to COSE_Key representation ([RFC8152] Section 7).
|
56
|
+
def credential_cose_key
|
57
|
+
decoded_public_key = Base64.strict_decode64(@public_key)
|
58
|
+
if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(decoded_public_key)
|
59
|
+
COSE::Key::EC2.new(
|
60
|
+
alg: COSE::Algorithm.by_name("ES256").id,
|
61
|
+
crv: 1,
|
62
|
+
x: decoded_public_key[1..32],
|
63
|
+
y: decoded_public_key[33..-1]
|
64
|
+
)
|
65
|
+
else
|
66
|
+
raise "expected U2F public key to be in uncompressed point format"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/webauthn/version.rb
CHANGED
data/webauthn-ruby.png
ADDED
Binary file
|
data/webauthn.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
|
13
13
|
spec.summary = "WebAuthn ruby server library"
|
14
14
|
spec.description = 'WebAuthn ruby server library ― Make your application a W3C Web Authentication conformant
|
15
|
-
Relying Party and allow your users to authenticate with U2F and
|
15
|
+
Relying Party and allow your users to authenticate with U2F and FIDO2 authenticators.'
|
16
16
|
spec.homepage = "https://github.com/cedarcode/webauthn-ruby"
|
17
17
|
spec.license = "MIT"
|
18
18
|
|
@@ -43,5 +43,5 @@ Gem::Specification.new do |spec|
|
|
43
43
|
spec.add_development_dependency "byebug", "~> 11.0"
|
44
44
|
spec.add_development_dependency "rake", "~> 12.3"
|
45
45
|
spec.add_development_dependency "rspec", "~> 3.8"
|
46
|
-
spec.add_development_dependency "rubocop", "0.
|
46
|
+
spec.add_development_dependency "rubocop", "0.73.0"
|
47
47
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webauthn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.18.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-
|
12
|
+
date: 2019-07-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bindata
|
@@ -183,17 +183,17 @@ dependencies:
|
|
183
183
|
requirements:
|
184
184
|
- - '='
|
185
185
|
- !ruby/object:Gem::Version
|
186
|
-
version: 0.
|
186
|
+
version: 0.73.0
|
187
187
|
type: :development
|
188
188
|
prerelease: false
|
189
189
|
version_requirements: !ruby/object:Gem::Requirement
|
190
190
|
requirements:
|
191
191
|
- - '='
|
192
192
|
- !ruby/object:Gem::Version
|
193
|
-
version: 0.
|
193
|
+
version: 0.73.0
|
194
194
|
description: |-
|
195
195
|
WebAuthn ruby server library ― Make your application a W3C Web Authentication conformant
|
196
|
-
Relying Party and allow your users to authenticate with U2F and
|
196
|
+
Relying Party and allow your users to authenticate with U2F and FIDO2 authenticators.
|
197
197
|
email:
|
198
198
|
- gonzalo@cedarcode.com
|
199
199
|
- braulio@cedarcode.com
|
@@ -207,12 +207,14 @@ files:
|
|
207
207
|
- ".travis.yml"
|
208
208
|
- Appraisals
|
209
209
|
- CHANGELOG.md
|
210
|
+
- CONTRIBUTING.md
|
210
211
|
- Gemfile
|
211
212
|
- LICENSE.txt
|
212
213
|
- README.md
|
213
214
|
- Rakefile
|
214
215
|
- bin/console
|
215
216
|
- bin/setup
|
217
|
+
- docs/u2f_migration.md
|
216
218
|
- gemfiles/openssl_2_0.gemfile
|
217
219
|
- gemfiles/openssl_2_1.gemfile
|
218
220
|
- lib/android_safetynet/attestation_response.rb
|
@@ -251,6 +253,7 @@ files:
|
|
251
253
|
- lib/webauthn/credential_request_options.rb
|
252
254
|
- lib/webauthn/credential_rp_entity.rb
|
253
255
|
- lib/webauthn/credential_user_entity.rb
|
256
|
+
- lib/webauthn/encoder.rb
|
254
257
|
- lib/webauthn/error.rb
|
255
258
|
- lib/webauthn/fake_authenticator.rb
|
256
259
|
- lib/webauthn/fake_authenticator/attestation_object.rb
|
@@ -259,7 +262,9 @@ files:
|
|
259
262
|
- lib/webauthn/public_key_credential.rb
|
260
263
|
- lib/webauthn/security_utils.rb
|
261
264
|
- lib/webauthn/signature_verifier.rb
|
265
|
+
- lib/webauthn/u2f_migrator.rb
|
262
266
|
- lib/webauthn/version.rb
|
267
|
+
- webauthn-ruby.png
|
263
268
|
- webauthn.gemspec
|
264
269
|
homepage: https://github.com/cedarcode/webauthn-ruby
|
265
270
|
licenses:
|