webauthn 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +8 -2
- data/Appraisals +9 -0
- data/CHANGELOG.md +17 -0
- data/README.md +37 -23
- data/gemfiles/openssl_2_0.gemfile +7 -0
- data/gemfiles/openssl_2_1.gemfile +7 -0
- data/lib/webauthn/attestation_statement/android_key.rb +9 -19
- data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +39 -0
- data/lib/webauthn/attestation_statement/android_key/key_description.rb +37 -0
- data/lib/webauthn/attestation_statement/fido_u2f.rb +15 -2
- data/lib/webauthn/attestation_statement/fido_u2f/public_key.rb +45 -0
- data/lib/webauthn/authenticator_assertion_response.rb +25 -9
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +11 -12
- data/lib/webauthn/fake_authenticator.rb +43 -149
- data/lib/webauthn/fake_authenticator/attestation_object.rb +47 -0
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +113 -0
- data/lib/webauthn/fake_client.rb +97 -0
- data/lib/webauthn/security_utils.rb +2 -2
- data/lib/webauthn/version.rb +1 -1
- data/webauthn.gemspec +4 -2
- metadata +30 -7
- data/lib/webauthn/authenticator_data/attested_credential_data/public_key.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0b7730e28517a0a0c8490fe77a7f1826de3914f7af3ccf6aefcc1e5268e4a45
|
|
4
|
+
data.tar.gz: 5a9433f01315b4360388d4d08667b041539f5fef43d48034d1deece03d24b48b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbfc86b1a82b1906a1adda4ab47e4cb7a523b520ecb533aa9587e047f463f74eccde0a88bff0ffb3acc037dc163cdbd40bad173c3e35a52492c230432897aeef
|
|
7
|
+
data.tar.gz: 887545a11d8d387deb137d228fb2f7a0c13f4462d7ca8b9247b68be7cdb40fcf478ee7b53bb5d39d7f89b6230d8c332676c20c7f5623f272522838f14b5213ef
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
dist: xenial
|
|
2
2
|
language: ruby
|
|
3
3
|
cache: bundler
|
|
4
|
+
|
|
4
5
|
rvm:
|
|
5
6
|
- ruby-head
|
|
6
7
|
- 2.6.2
|
|
7
|
-
- 2.5.
|
|
8
|
-
- 2.4.
|
|
8
|
+
- 2.5.5
|
|
9
|
+
- 2.4.6
|
|
9
10
|
- 2.3.8
|
|
11
|
+
|
|
12
|
+
gemfile:
|
|
13
|
+
- gemfiles/openssl_2_1.gemfile
|
|
14
|
+
- gemfiles/openssl_2_0.gemfile
|
|
15
|
+
|
|
10
16
|
matrix:
|
|
11
17
|
fast_finish: true
|
|
12
18
|
allow_failures:
|
data/Appraisals
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.12.0] - 2019-04-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Verification of the attestation certificate public key curve for `fido-u2f` attestation statements.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `Credential#public_key` now returns the COSE_Key formatted version of the credential public key, instead of the
|
|
12
|
+
uncompressed EC point format.
|
|
13
|
+
|
|
14
|
+
Note #1: A `Credential` instance is what is returned in `WebAuthn::AuthenticatorAttestationResponse#credential`.
|
|
15
|
+
|
|
16
|
+
Note #2: You don't need to do any convesion before passing the public key in `AuthenticatorAssertionResponse#verify`'s
|
|
17
|
+
`allowed_credentials` argument, `#verify` is backwards-compatible and will handle both public key formats properly.
|
|
18
|
+
|
|
3
19
|
## [v1.11.0] - 2019-03-15
|
|
4
20
|
|
|
5
21
|
### Added
|
|
@@ -137,6 +153,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
|
|
|
137
153
|
- `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
|
|
138
154
|
- Works with ruby 2.5
|
|
139
155
|
|
|
156
|
+
[v1.12.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.12.0...v1.12.0/
|
|
140
157
|
[v1.11.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.10.0...v1.11.0/
|
|
141
158
|
[v1.10.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.9.0...v1.10.0/
|
|
142
159
|
[v1.9.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.8.0...v1.9.0/
|
data/README.md
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
|
-
# WebAuthn :key:
|
|
1
|
+
# WebAuthn ruby library :key:
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Make your Ruby/Rails web server become a conformant WebAuthn Relying Party.
|
|
4
4
|
|
|
5
5
|
[](https://rubygems.org/gems/webauthn)
|
|
6
6
|
[](https://travis-ci.org/cedarcode/webauthn-ruby)
|
|
7
|
+
[](https://gitter.im/cedarcode/webauthn-ruby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
7
8
|
|
|
8
9
|
## What is WebAuthn?
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
11
|
+
WebAuthn (Web Authentication) is a W3C standard for secure public-key authentication on the Web supported by all leading browsers and platforms.
|
|
12
|
+
|
|
13
|
+
For more:
|
|
14
|
+
|
|
15
|
+
- WebAuthn [W3C Recommendation](https://www.w3.org/TR/webauthn/) (i.e. "The Standard")
|
|
16
|
+
- WebAuthn [intro](https://www.yubico.com/webauthn/) by Yubico
|
|
17
|
+
- WebAuthn [article](https://en.wikipedia.org/wiki/WebAuthn) in Wikipedia
|
|
18
|
+
- [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) in MDN
|
|
19
|
+
- WebAuthn [article with talk](https://developers.google.com/web/updates/2018/05/webauthn) in Google Developers
|
|
15
20
|
|
|
16
21
|
## Prerequisites
|
|
17
22
|
|
|
18
|
-
This
|
|
23
|
+
This ruby library will help your Ruby/Rails server act as a conforming [_Relying-Party_](https://www.w3.org/TR/webauthn/#relying-party), in WebAuthn terminology. But for the [_Registration_](https://www.w3.org/TR/webauthn/#registration) and [_Authentication_](https://www.w3.org/TR/webauthn/#authentication) ceremonies to fully work, you will also need to add two more pieces to the puzzle, a conforming [User Agent](https://www.w3.org/TR/webauthn/#conforming-user-agents) + [Authenticator](https://www.w3.org/TR/webauthn/#conforming-authenticators) pair.
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
A very small set of known conformant pairs are for example:
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- [Google Chrome for Android](https://play.google.com/store/apps/details?id=com.android.chrome) 70+
|
|
27
|
+
- Google Chrome for Android 70+ and Android's Fingerprint-based platform authenticator
|
|
28
|
+
- Microsoft Edge and Windows 10 platform authenticator
|
|
29
|
+
- Mozilla Firefox for Desktop and Yubico's Security Key roaming authenticator via USB
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
For a detailed picture about what is conformant and what not, you can refer to:
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* [YubiKey 5 Series](https://www.yubico.com/products/yubikey-5-overview/) key
|
|
32
|
-
* Platform authenticators
|
|
33
|
-
* Android's Fingerprint Scanner
|
|
34
|
-
* MacBook [Touch ID](https://en.wikipedia.org/wiki/Touch_ID)
|
|
33
|
+
- [apowers313/fido2-webauthn-status](https://github.com/apowers313/fido2-webauthn-status)
|
|
34
|
+
- [FIDO certified products](https://fidoalliance.org/certification/fido-certified-products)
|
|
35
35
|
|
|
36
|
-
NOTE: Firefox states ([Firefox 60 release notes](https://www.mozilla.org/en-US/firefox/60.0/releasenotes/)) they only support USB FIDO2 or FIDO U2F enabled devices in their current implementation (version 60).
|
|
37
|
-
It's up to the gem's user to verify user agent compatibility if any other device wants to be used as the authenticator component.
|
|
38
36
|
|
|
39
37
|
## Installation
|
|
40
38
|
|
|
@@ -175,9 +173,25 @@ rescue WebAuthn::VerificationError => e
|
|
|
175
173
|
end
|
|
176
174
|
```
|
|
177
175
|
|
|
176
|
+
## Attestation Statement Formats
|
|
177
|
+
|
|
178
|
+
| Attestation Statement Format | Supported? |
|
|
179
|
+
| -------- | :--------: |
|
|
180
|
+
| packed (self attestation) | Yes |
|
|
181
|
+
| packed (x5c attestation) | Yes |
|
|
182
|
+
| packed (ECDAA attestation) | No |
|
|
183
|
+
| tpm (x5c attestation) | No |
|
|
184
|
+
| tpm (ECDAA attestation) | No |
|
|
185
|
+
| android-key | Yes |
|
|
186
|
+
| android-safetynet | Yes |
|
|
187
|
+
| fido-u2f | Yes |
|
|
188
|
+
| none | Yes |
|
|
189
|
+
|
|
190
|
+
NOTE: Be aware that it is up to you to do "trust path validation" (steps 15 and 16 in [Registering a new credential](https://www.w3.org/TR/webauthn/#registering-a-new-credential)) if that's a requirement of your Relying Party policy. The gem doesn't perform that validation for you right now.
|
|
191
|
+
|
|
178
192
|
## Testing Your Integration
|
|
179
193
|
|
|
180
|
-
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.
|
|
194
|
+
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).
|
|
181
195
|
|
|
182
196
|
## Development
|
|
183
197
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
|
+
require "webauthn/attestation_statement/android_key/key_description"
|
|
4
5
|
require "webauthn/attestation_statement/base"
|
|
5
6
|
|
|
6
7
|
module WebAuthn
|
|
@@ -8,14 +9,6 @@ module WebAuthn
|
|
|
8
9
|
class AndroidKey < Base
|
|
9
10
|
EXTENSION_DATA_OID = "1.3.6.1.4.1.11129.2.1.17"
|
|
10
11
|
|
|
11
|
-
# https://developer.android.com/training/articles/security-key-attestation#certificate_schema
|
|
12
|
-
ATTESTATION_CHALLENGE_INDEX = 4
|
|
13
|
-
SOFTWARE_ENFORCED_INDEX = 6
|
|
14
|
-
TEE_ENFORCED_INDEX = 7
|
|
15
|
-
PURPOSE_TAG = 1
|
|
16
|
-
ALL_APPLICATIONS_TAG = 600
|
|
17
|
-
ORIGIN_TAG = 702
|
|
18
|
-
|
|
19
12
|
# https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/keymaster_defs.h
|
|
20
13
|
KM_ORIGIN_GENERATED = 0
|
|
21
14
|
KM_PURPOSE_SIGN = 2
|
|
@@ -45,38 +38,35 @@ module WebAuthn
|
|
|
45
38
|
end
|
|
46
39
|
|
|
47
40
|
def valid_attestation_challenge?(client_data_hash)
|
|
48
|
-
WebAuthn::SecurityUtils.secure_compare(key_description
|
|
41
|
+
WebAuthn::SecurityUtils.secure_compare(key_description.attestation_challenge, client_data_hash)
|
|
49
42
|
end
|
|
50
43
|
|
|
51
44
|
def all_applications_field_not_present?
|
|
52
|
-
tee_enforced.
|
|
53
|
-
software_enforced.none? { |data| data.tag == ALL_APPLICATIONS_TAG }
|
|
45
|
+
tee_enforced.all_applications.nil? && software_enforced.all_applications.nil?
|
|
54
46
|
end
|
|
55
47
|
|
|
56
48
|
def valid_authorization_list_origin?
|
|
57
|
-
tee_enforced.
|
|
58
|
-
software_enforced.detect { |data| data.tag == ORIGIN_TAG }&.value&.at(0)&.value == KM_ORIGIN_GENERATED
|
|
49
|
+
tee_enforced.origin == KM_ORIGIN_GENERATED || software_enforced.origin == KM_ORIGIN_GENERATED
|
|
59
50
|
end
|
|
60
51
|
|
|
61
52
|
def valid_authorization_list_purpose?
|
|
62
|
-
tee_enforced.
|
|
63
|
-
software_enforced.detect { |data| data.tag == PURPOSE_TAG }&.value&.at(0)&.value&.at(0)&.value ==
|
|
64
|
-
KM_PURPOSE_SIGN
|
|
53
|
+
tee_enforced.purpose == KM_PURPOSE_SIGN || software_enforced.purpose == KM_PURPOSE_SIGN
|
|
65
54
|
end
|
|
66
55
|
|
|
67
56
|
def tee_enforced
|
|
68
|
-
key_description
|
|
57
|
+
key_description.tee_enforced
|
|
69
58
|
end
|
|
70
59
|
|
|
71
60
|
def software_enforced
|
|
72
|
-
key_description
|
|
61
|
+
key_description.software_enforced
|
|
73
62
|
end
|
|
74
63
|
|
|
75
64
|
def key_description
|
|
76
65
|
@key_description ||= begin
|
|
77
66
|
extension_data = attestation_certificate.extensions.detect { |ext| ext.oid == EXTENSION_DATA_OID }
|
|
78
67
|
raw_key_description = OpenSSL::ASN1.decode(extension_data).value.last
|
|
79
|
-
|
|
68
|
+
|
|
69
|
+
KeyDescription.new(OpenSSL::ASN1.decode(raw_key_description.value).value)
|
|
80
70
|
end
|
|
81
71
|
end
|
|
82
72
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/attestation_statement/base"
|
|
4
|
+
|
|
5
|
+
module WebAuthn
|
|
6
|
+
module AttestationStatement
|
|
7
|
+
class AndroidKey < Base
|
|
8
|
+
class AuthorizationList
|
|
9
|
+
PURPOSE_TAG = 1
|
|
10
|
+
ALL_APPLICATIONS_TAG = 600
|
|
11
|
+
ORIGIN_TAG = 702
|
|
12
|
+
|
|
13
|
+
def initialize(sequence)
|
|
14
|
+
@sequence = sequence
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def purpose
|
|
18
|
+
find_by_tag(PURPOSE_TAG)&.value&.at(0)&.value&.at(0)&.value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def all_applications
|
|
22
|
+
find_by_tag(ALL_APPLICATIONS_TAG)&.value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def origin
|
|
26
|
+
find_by_tag(ORIGIN_TAG)&.value&.at(0)&.value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :sequence
|
|
32
|
+
|
|
33
|
+
def find_by_tag(tag)
|
|
34
|
+
sequence.detect { |data| data.tag == tag }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webauthn/attestation_statement/android_key/authorization_list"
|
|
4
|
+
require "webauthn/attestation_statement/base"
|
|
5
|
+
|
|
6
|
+
module WebAuthn
|
|
7
|
+
module AttestationStatement
|
|
8
|
+
class AndroidKey < Base
|
|
9
|
+
class KeyDescription
|
|
10
|
+
# https://developer.android.com/training/articles/security-key-attestation#certificate_schema
|
|
11
|
+
ATTESTATION_CHALLENGE_INDEX = 4
|
|
12
|
+
SOFTWARE_ENFORCED_INDEX = 6
|
|
13
|
+
TEE_ENFORCED_INDEX = 7
|
|
14
|
+
|
|
15
|
+
def initialize(sequence)
|
|
16
|
+
@sequence = sequence
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def attestation_challenge
|
|
20
|
+
sequence[ATTESTATION_CHALLENGE_INDEX].value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tee_enforced
|
|
24
|
+
@tee_enforced ||= AuthorizationList.new(sequence[TEE_ENFORCED_INDEX].value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def software_enforced
|
|
28
|
+
@software_enforced ||= AuthorizationList.new(sequence[SOFTWARE_ENFORCED_INDEX].value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :sequence
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "webauthn/attestation_statement/base"
|
|
5
|
+
require "webauthn/attestation_statement/fido_u2f/public_key"
|
|
5
6
|
|
|
6
7
|
module WebAuthn
|
|
7
8
|
module AttestationStatement
|
|
8
9
|
class FidoU2f < Base
|
|
9
10
|
VALID_ATTESTATION_CERTIFICATE_COUNT = 1
|
|
11
|
+
VALID_ATTESTATION_CERTIFICATE_KEY_CURVE = "prime256v1"
|
|
10
12
|
|
|
11
13
|
def valid?(authenticator_data, client_data_hash)
|
|
12
14
|
valid_format? &&
|
|
13
15
|
valid_certificate_public_key? &&
|
|
16
|
+
valid_credential_public_key?(authenticator_data.credential.public_key) &&
|
|
14
17
|
valid_signature?(authenticator_data, client_data_hash) &&
|
|
15
18
|
[WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, [attestation_certificate]]
|
|
16
19
|
end
|
|
@@ -27,7 +30,13 @@ module WebAuthn
|
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def valid_certificate_public_key?
|
|
30
|
-
certificate_public_key.is_a?(OpenSSL::PKey::EC) &&
|
|
33
|
+
certificate_public_key.is_a?(OpenSSL::PKey::EC) &&
|
|
34
|
+
certificate_public_key.group.curve_name == VALID_ATTESTATION_CERTIFICATE_KEY_CURVE &&
|
|
35
|
+
certificate_public_key.check_key
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid_credential_public_key?(public_key_bytes)
|
|
39
|
+
public_key_u2f(public_key_bytes).valid?
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
def certificate_public_key
|
|
@@ -55,7 +64,11 @@ module WebAuthn
|
|
|
55
64
|
authenticator_data.rp_id_hash +
|
|
56
65
|
client_data_hash +
|
|
57
66
|
authenticator_data.credential.id +
|
|
58
|
-
authenticator_data.credential.public_key
|
|
67
|
+
public_key_u2f(authenticator_data.credential.public_key).to_uncompressed_point
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def public_key_u2f(cose_key_data)
|
|
71
|
+
PublicKey.new(cose_key_data)
|
|
59
72
|
end
|
|
60
73
|
end
|
|
61
74
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cose/ecdsa"
|
|
4
|
+
require "cose/key/ec2"
|
|
5
|
+
require "webauthn/attestation_statement/base"
|
|
6
|
+
|
|
7
|
+
module WebAuthn
|
|
8
|
+
module AttestationStatement
|
|
9
|
+
class FidoU2f < Base
|
|
10
|
+
class PublicKey
|
|
11
|
+
COORDINATE_LENGTH = 32
|
|
12
|
+
UNCOMPRESSED_FORM_INDICATOR = "\x04"
|
|
13
|
+
|
|
14
|
+
def self.uncompressed_point?(data)
|
|
15
|
+
data.size &&
|
|
16
|
+
data.length == UNCOMPRESSED_FORM_INDICATOR.length + COORDINATE_LENGTH * 2 &&
|
|
17
|
+
data[0] == UNCOMPRESSED_FORM_INDICATOR
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(data)
|
|
21
|
+
@data = data
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
data.size >= COORDINATE_LENGTH * 2 &&
|
|
26
|
+
cose_key.x.length == COORDINATE_LENGTH &&
|
|
27
|
+
cose_key.y.length == COORDINATE_LENGTH &&
|
|
28
|
+
cose_key.alg == COSE::ECDSA::ALG_ES256
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_uncompressed_point
|
|
32
|
+
UNCOMPRESSED_FORM_INDICATOR + cose_key.x + cose_key.y
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :data
|
|
38
|
+
|
|
39
|
+
def cose_key
|
|
40
|
+
@cose_key ||= COSE::Key::EC2.deserialize(data)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cose/key"
|
|
4
|
+
require "webauthn/attestation_statement/fido_u2f/public_key"
|
|
3
5
|
require "webauthn/authenticator_response"
|
|
4
6
|
|
|
5
7
|
module WebAuthn
|
|
@@ -32,14 +34,8 @@ module WebAuthn
|
|
|
32
34
|
|
|
33
35
|
attr_reader :credential_id, :authenticator_data_bytes, :signature
|
|
34
36
|
|
|
35
|
-
def valid_signature?(
|
|
36
|
-
|
|
37
|
-
key = OpenSSL::PKey::EC.new(group)
|
|
38
|
-
public_key_bn = OpenSSL::BN.new(public_key_bytes, 2)
|
|
39
|
-
public_key = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
|
|
40
|
-
key.public_key = public_key
|
|
41
|
-
|
|
42
|
-
key.verify(
|
|
37
|
+
def valid_signature?(credential_public_key)
|
|
38
|
+
credential_public_key.verify(
|
|
43
39
|
"SHA256",
|
|
44
40
|
signature,
|
|
45
41
|
authenticator_data_bytes + client_data.hash
|
|
@@ -57,7 +53,27 @@ module WebAuthn
|
|
|
57
53
|
credential[:id] == credential_id
|
|
58
54
|
end
|
|
59
55
|
|
|
60
|
-
matched_credential[:public_key]
|
|
56
|
+
if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(matched_credential[:public_key])
|
|
57
|
+
# Gem version v1.11.0 and lower, used to behave so that Credential#public_key
|
|
58
|
+
# returned an EC P-256 uncompressed point.
|
|
59
|
+
#
|
|
60
|
+
# Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
|
|
61
|
+
# and Credential#public_key started returning the unchanged COSE_Key formatted
|
|
62
|
+
# credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
|
|
63
|
+
#
|
|
64
|
+
# Given that the credential public key is expected to be stored long-term by the gem
|
|
65
|
+
# user and later be passed as one of the allowed_credentials arguments in the
|
|
66
|
+
# AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
|
|
67
|
+
group = OpenSSL::PKey::EC::Group.new("prime256v1")
|
|
68
|
+
key = OpenSSL::PKey::EC.new(group)
|
|
69
|
+
public_key_bn = OpenSSL::BN.new(matched_credential[:public_key], 2)
|
|
70
|
+
public_key = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
|
|
71
|
+
key.public_key = public_key
|
|
72
|
+
|
|
73
|
+
key
|
|
74
|
+
else
|
|
75
|
+
COSE::Key.deserialize(matched_credential[:public_key]).to_pkey
|
|
76
|
+
end
|
|
61
77
|
end
|
|
62
78
|
|
|
63
79
|
def type
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "cose/key"
|
|
4
4
|
|
|
5
5
|
module WebAuthn
|
|
6
6
|
class AuthenticatorData
|
|
@@ -14,14 +14,7 @@ module WebAuthn
|
|
|
14
14
|
# FIXME: use keyword_init when we dropped Ruby 2.4 support
|
|
15
15
|
Credential = Struct.new(:id, :public_key) do
|
|
16
16
|
def public_key_object
|
|
17
|
-
|
|
18
|
-
key = OpenSSL::PKey::EC.new(group)
|
|
19
|
-
|
|
20
|
-
bn = OpenSSL::BN.new(public_key, 2)
|
|
21
|
-
point = OpenSSL::PKey::EC::Point.new(group, bn)
|
|
22
|
-
key.public_key = point
|
|
23
|
-
|
|
24
|
-
key
|
|
17
|
+
COSE::Key.deserialize(public_key).to_pkey
|
|
25
18
|
end
|
|
26
19
|
end
|
|
27
20
|
|
|
@@ -30,7 +23,7 @@ module WebAuthn
|
|
|
30
23
|
end
|
|
31
24
|
|
|
32
25
|
def valid?
|
|
33
|
-
data.length >= AAGUID_LENGTH + ID_LENGTH_LENGTH &&
|
|
26
|
+
data.length >= AAGUID_LENGTH + ID_LENGTH_LENGTH && valid_credential_public_key?
|
|
34
27
|
end
|
|
35
28
|
|
|
36
29
|
def aaguid
|
|
@@ -40,7 +33,7 @@ module WebAuthn
|
|
|
40
33
|
def credential
|
|
41
34
|
@credential ||=
|
|
42
35
|
if id
|
|
43
|
-
Credential.new(id, public_key
|
|
36
|
+
Credential.new(id, public_key)
|
|
44
37
|
end
|
|
45
38
|
end
|
|
46
39
|
|
|
@@ -54,6 +47,12 @@ module WebAuthn
|
|
|
54
47
|
|
|
55
48
|
attr_reader :data
|
|
56
49
|
|
|
50
|
+
def valid_credential_public_key?
|
|
51
|
+
cose_key = COSE::Key.deserialize(public_key)
|
|
52
|
+
|
|
53
|
+
!!cose_key.alg
|
|
54
|
+
end
|
|
55
|
+
|
|
57
56
|
def id
|
|
58
57
|
if valid?
|
|
59
58
|
data_at(id_position, id_length)
|
|
@@ -61,7 +60,7 @@ module WebAuthn
|
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def public_key
|
|
64
|
-
@public_key ||=
|
|
63
|
+
@public_key ||= data_at(public_key_position, public_key_length)
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
def id_position
|
|
@@ -3,175 +3,69 @@
|
|
|
3
3
|
require "cbor"
|
|
4
4
|
require "openssl"
|
|
5
5
|
require "securerandom"
|
|
6
|
+
require "webauthn/fake_authenticator/attestation_object"
|
|
7
|
+
require "webauthn/fake_authenticator/authenticator_data"
|
|
6
8
|
|
|
7
9
|
module WebAuthn
|
|
8
10
|
class FakeAuthenticator
|
|
9
|
-
|
|
10
|
-
def initialize(challenge: fake_challenge, rp_id: "localhost", sign_count: 0, context: {})
|
|
11
|
-
@challenge = challenge
|
|
12
|
-
@rp_id = rp_id
|
|
13
|
-
@sign_count = sign_count
|
|
14
|
-
@context = context
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def authenticator_data
|
|
18
|
-
@authenticator_data ||= rp_id_hash + raw_flags + raw_sign_count + attested_credential_data + extension_data
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def client_data_json
|
|
22
|
-
@client_data_json ||= { challenge: encode(challenge), origin: origin, type: type }.to_json
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def credential_key
|
|
26
|
-
@credential_key ||= OpenSSL::PKey::EC.new("prime256v1").generate_key
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def credential_id
|
|
30
|
-
@credential_id ||= SecureRandom.random_bytes(16)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def rp_id_hash
|
|
34
|
-
OpenSSL::Digest::SHA256.digest(rp_id)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
attr_reader :challenge, :context, :rp_id
|
|
40
|
-
|
|
41
|
-
def raw_flags
|
|
42
|
-
[
|
|
43
|
-
[
|
|
44
|
-
bit(:user_present),
|
|
45
|
-
"0",
|
|
46
|
-
bit(:user_verified),
|
|
47
|
-
"000",
|
|
48
|
-
attested_credential_data_present_bit,
|
|
49
|
-
extension_data_present_bit
|
|
50
|
-
].join
|
|
51
|
-
].pack("b*")
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def attested_credential_data_present_bit
|
|
55
|
-
if attested_credential_data.empty?
|
|
56
|
-
"0"
|
|
57
|
-
else
|
|
58
|
-
"1"
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def extension_data_present_bit
|
|
63
|
-
if extension_data.empty?
|
|
64
|
-
"0"
|
|
65
|
-
else
|
|
66
|
-
"1"
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def attested_credential_data
|
|
71
|
-
""
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def extension_data
|
|
75
|
-
CBOR.encode("fakeExtension" => "fakeValue")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def raw_sign_count
|
|
79
|
-
[@sign_count].pack('L>')
|
|
80
|
-
end
|
|
11
|
+
AAGUID = SecureRandom.random_bytes(16)
|
|
81
12
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
else
|
|
86
|
-
"0"
|
|
87
|
-
end
|
|
88
|
-
end
|
|
13
|
+
def initialize
|
|
14
|
+
@credentials = {}
|
|
15
|
+
end
|
|
89
16
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
end
|
|
17
|
+
def make_credential(rp_id:, client_data_hash:, user_present: true, user_verified: false)
|
|
18
|
+
credential_id, credential_key = new_credential
|
|
93
19
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
20
|
+
attestation_object = AttestationObject.new(
|
|
21
|
+
client_data_hash: client_data_hash,
|
|
22
|
+
rp_id_hash: hashed(rp_id),
|
|
23
|
+
credential_id: credential_id,
|
|
24
|
+
credential_key: credential_key,
|
|
25
|
+
user_present: user_present,
|
|
26
|
+
user_verified: user_verified
|
|
27
|
+
).serialize
|
|
97
28
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
end
|
|
29
|
+
credentials[rp_id] ||= {}
|
|
30
|
+
credentials[rp_id][credential_id] = credential_key
|
|
101
31
|
|
|
102
|
-
|
|
103
|
-
"http://localhost"
|
|
104
|
-
end
|
|
32
|
+
attestation_object
|
|
105
33
|
end
|
|
106
34
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
CBOR.encode(
|
|
110
|
-
"fmt" => "none",
|
|
111
|
-
"attStmt" => {},
|
|
112
|
-
"authData" => authenticator_data
|
|
113
|
-
)
|
|
114
|
-
end
|
|
35
|
+
def get_assertion(rp_id:, client_data_hash:, user_present: true, user_verified: false)
|
|
36
|
+
credential_options = credentials[rp_id]
|
|
115
37
|
|
|
116
|
-
|
|
38
|
+
if credential_options
|
|
39
|
+
credential_id, credential_key = credential_options.first
|
|
117
40
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
41
|
+
authenticator_data = AuthenticatorData.new(
|
|
42
|
+
rp_id_hash: hashed(rp_id),
|
|
43
|
+
user_present: user_present,
|
|
44
|
+
user_verified: user_verified
|
|
45
|
+
).serialize
|
|
121
46
|
|
|
122
|
-
|
|
123
|
-
@aaguid ||= SecureRandom.random_bytes(16)
|
|
124
|
-
end
|
|
47
|
+
signature = credential_key.sign("SHA256", authenticator_data + client_data_hash)
|
|
125
48
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
49
|
+
{
|
|
50
|
+
credential_id: credential_id,
|
|
51
|
+
authenticator_data: authenticator_data,
|
|
52
|
+
signature: signature
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
raise "No credentials found for RP #{rp_id}"
|
|
131
56
|
end
|
|
57
|
+
end
|
|
132
58
|
|
|
133
|
-
|
|
134
|
-
"webauthn.create"
|
|
135
|
-
end
|
|
59
|
+
private
|
|
136
60
|
|
|
137
|
-
|
|
138
|
-
kty_label = 1
|
|
139
|
-
alg_label = 3
|
|
140
|
-
crv_label = -1
|
|
141
|
-
x_label = -2
|
|
142
|
-
y_label = -3
|
|
143
|
-
|
|
144
|
-
kty_ec2 = 2
|
|
145
|
-
alg_es256 = -7
|
|
146
|
-
crv_p256 = 1
|
|
147
|
-
|
|
148
|
-
CBOR.encode(
|
|
149
|
-
kty_label => kty_ec2,
|
|
150
|
-
alg_label => algorithm || alg_es256,
|
|
151
|
-
crv_label => crv_p256,
|
|
152
|
-
x_label => x_coordinate || SecureRandom.random_bytes(32),
|
|
153
|
-
y_label => y_coordinate || SecureRandom.random_bytes(32)
|
|
154
|
-
)
|
|
155
|
-
end
|
|
61
|
+
attr_reader :credentials
|
|
156
62
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
63
|
+
def new_credential
|
|
64
|
+
[SecureRandom.random_bytes(16), OpenSSL::PKey::EC.new("prime256v1").generate_key]
|
|
160
65
|
end
|
|
161
66
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@signature ||= credential_key.sign(
|
|
165
|
-
"SHA256",
|
|
166
|
-
authenticator_data + OpenSSL::Digest::SHA256.digest(client_data_json)
|
|
167
|
-
)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
private
|
|
171
|
-
|
|
172
|
-
def type
|
|
173
|
-
"webauthn.get"
|
|
174
|
-
end
|
|
67
|
+
def hashed(target)
|
|
68
|
+
OpenSSL::Digest::SHA256.digest(target)
|
|
175
69
|
end
|
|
176
70
|
end
|
|
177
71
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cbor"
|
|
4
|
+
require "webauthn/fake_authenticator/authenticator_data"
|
|
5
|
+
|
|
6
|
+
module WebAuthn
|
|
7
|
+
class FakeAuthenticator
|
|
8
|
+
class AttestationObject
|
|
9
|
+
def initialize(
|
|
10
|
+
client_data_hash:,
|
|
11
|
+
rp_id_hash:,
|
|
12
|
+
credential_id:,
|
|
13
|
+
credential_key:,
|
|
14
|
+
user_present: true,
|
|
15
|
+
user_verified: false
|
|
16
|
+
)
|
|
17
|
+
@client_data_hash = client_data_hash
|
|
18
|
+
@rp_id_hash = rp_id_hash
|
|
19
|
+
@credential_id = credential_id
|
|
20
|
+
@credential_key = credential_key
|
|
21
|
+
@user_present = user_present
|
|
22
|
+
@user_verified = user_verified
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def serialize
|
|
26
|
+
CBOR.encode(
|
|
27
|
+
"fmt" => "none",
|
|
28
|
+
"attStmt" => {},
|
|
29
|
+
"authData" => authenticator_data.serialize
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :client_data_hash, :rp_id_hash, :credential_id, :credential_key, :user_present, :user_verified
|
|
36
|
+
|
|
37
|
+
def authenticator_data
|
|
38
|
+
@authenticator_data ||= AuthenticatorData.new(
|
|
39
|
+
rp_id_hash: rp_id_hash,
|
|
40
|
+
credential: { id: credential_id, public_key: credential_key.public_key },
|
|
41
|
+
user_present: user_present,
|
|
42
|
+
user_verified: user_verified
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cose/key"
|
|
4
|
+
require "cbor"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module WebAuthn
|
|
8
|
+
class FakeAuthenticator
|
|
9
|
+
class AuthenticatorData
|
|
10
|
+
def initialize(rp_id_hash:, credential: nil, sign_count: 0, user_present: true, user_verified: !user_present)
|
|
11
|
+
@rp_id_hash = rp_id_hash
|
|
12
|
+
@credential = credential
|
|
13
|
+
@sign_count = sign_count
|
|
14
|
+
@user_present = user_present
|
|
15
|
+
@user_verified = user_verified
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def serialize
|
|
19
|
+
rp_id_hash + flags + serialized_sign_count + attested_credential_data + extensions
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :rp_id_hash, :credential, :sign_count, :user_present, :user_verified
|
|
25
|
+
|
|
26
|
+
def flags
|
|
27
|
+
[
|
|
28
|
+
[
|
|
29
|
+
bit(:user_present),
|
|
30
|
+
reserved_for_future_use_bit,
|
|
31
|
+
bit(:user_verified),
|
|
32
|
+
reserved_for_future_use_bit,
|
|
33
|
+
reserved_for_future_use_bit,
|
|
34
|
+
reserved_for_future_use_bit,
|
|
35
|
+
attested_credential_data_included_bit,
|
|
36
|
+
extension_data_included_bit
|
|
37
|
+
].join
|
|
38
|
+
].pack("b*")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def serialized_sign_count
|
|
42
|
+
[sign_count].pack('L>')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def attested_credential_data
|
|
46
|
+
@attested_credential_data ||=
|
|
47
|
+
if credential
|
|
48
|
+
WebAuthn::FakeAuthenticator::AAGUID +
|
|
49
|
+
[credential[:id].length].pack("n*") +
|
|
50
|
+
credential[:id] +
|
|
51
|
+
cose_credential_public_key
|
|
52
|
+
else
|
|
53
|
+
""
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extensions
|
|
58
|
+
CBOR.encode("fakeExtension" => "fakeExtensionValue")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def bit(flag)
|
|
62
|
+
if context[flag]
|
|
63
|
+
"1"
|
|
64
|
+
else
|
|
65
|
+
"0"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def attested_credential_data_included_bit
|
|
70
|
+
if attested_credential_data.empty?
|
|
71
|
+
"0"
|
|
72
|
+
else
|
|
73
|
+
"1"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def extension_data_included_bit
|
|
78
|
+
if extensions.empty?
|
|
79
|
+
"0"
|
|
80
|
+
else
|
|
81
|
+
"1"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def reserved_for_future_use_bit
|
|
86
|
+
"0"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def context
|
|
90
|
+
{ user_present: user_present, user_verified: user_verified }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cose_credential_public_key
|
|
94
|
+
alg = {
|
|
95
|
+
COSE::Key::EC2::CRV_P256 => -7,
|
|
96
|
+
COSE::Key::EC2::CRV_P384 => -35,
|
|
97
|
+
COSE::Key::EC2::CRV_P521 => -36
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
key = COSE::Key::EC2.from_pkey(credential[:public_key])
|
|
101
|
+
|
|
102
|
+
# FIXME: Remove once writer in cose
|
|
103
|
+
key.instance_variable_set(:@alg, alg[key.crv])
|
|
104
|
+
|
|
105
|
+
key.serialize
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def key_bytes(public_key)
|
|
109
|
+
public_key.to_bn.to_s(2)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "webauthn/authenticator_data"
|
|
5
|
+
require "webauthn/fake_authenticator"
|
|
6
|
+
|
|
7
|
+
module WebAuthn
|
|
8
|
+
class FakeClient
|
|
9
|
+
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :origin
|
|
12
|
+
|
|
13
|
+
def initialize(origin = fake_origin, authenticator: WebAuthn::FakeAuthenticator.new)
|
|
14
|
+
@origin = origin
|
|
15
|
+
@authenticator = authenticator
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false)
|
|
19
|
+
rp_id ||= URI.parse(origin).host
|
|
20
|
+
|
|
21
|
+
client_data_json = data_json_for(:create, challenge)
|
|
22
|
+
client_data_hash = hashed(client_data_json)
|
|
23
|
+
|
|
24
|
+
attestation_object = authenticator.make_credential(
|
|
25
|
+
rp_id: rp_id,
|
|
26
|
+
client_data_hash: client_data_hash,
|
|
27
|
+
user_present: user_present,
|
|
28
|
+
user_verified: user_verified
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
id = WebAuthn::AuthenticatorData.new(CBOR.decode(attestation_object)["authData"]).credential.id
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
id: id,
|
|
35
|
+
response: {
|
|
36
|
+
attestation_object: attestation_object,
|
|
37
|
+
client_data_json: client_data_json
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false)
|
|
43
|
+
rp_id ||= URI.parse(origin).host
|
|
44
|
+
|
|
45
|
+
client_data_json = data_json_for(:get, challenge)
|
|
46
|
+
client_data_hash = hashed(client_data_json)
|
|
47
|
+
|
|
48
|
+
assertion = authenticator.get_assertion(
|
|
49
|
+
rp_id: rp_id,
|
|
50
|
+
client_data_hash: client_data_hash,
|
|
51
|
+
user_present: user_present,
|
|
52
|
+
user_verified: user_verified
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
id: assertion[:credential_id],
|
|
57
|
+
response: {
|
|
58
|
+
client_data_json: client_data_json,
|
|
59
|
+
authenticator_data: assertion[:authenticator_data],
|
|
60
|
+
signature: assertion[:signature]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
attr_reader :authenticator
|
|
68
|
+
|
|
69
|
+
def data_json_for(method, challenge)
|
|
70
|
+
{
|
|
71
|
+
type: type_for(method),
|
|
72
|
+
challenge: encode(challenge),
|
|
73
|
+
origin: origin
|
|
74
|
+
}.to_json
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def encode(data)
|
|
78
|
+
Base64.urlsafe_encode64(data, padding: false)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def hashed(data)
|
|
82
|
+
OpenSSL::Digest::SHA256.digest(data)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def fake_challenge
|
|
86
|
+
SecureRandom.random_bytes(32)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fake_origin
|
|
90
|
+
"http://localhost#{rand(1000)}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def type_for(method)
|
|
94
|
+
TYPES[method]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -10,8 +10,8 @@ module WebAuthn
|
|
|
10
10
|
# The values are first processed by SHA256, so that we don't leak length info
|
|
11
11
|
# via timing attacks.
|
|
12
12
|
def secure_compare(first_string, second_string)
|
|
13
|
-
first_string_sha256 = ::Digest::SHA256.
|
|
14
|
-
second_string_sha256 = ::Digest::SHA256.
|
|
13
|
+
first_string_sha256 = ::Digest::SHA256.digest(first_string)
|
|
14
|
+
second_string_sha256 = ::Digest::SHA256.digest(second_string)
|
|
15
15
|
|
|
16
16
|
SecureCompare.compare(first_string_sha256, second_string_sha256) && first_string == second_string
|
|
17
17
|
end
|
data/lib/webauthn/version.rb
CHANGED
data/webauthn.gemspec
CHANGED
|
@@ -10,7 +10,8 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.authors = ["Gonzalo Rodriguez", "Braulio Martinez"]
|
|
11
11
|
spec.email = ["gonzalo@cedarcode.com", "braulio@cedarcode.com"]
|
|
12
12
|
|
|
13
|
-
spec.summary = "WebAuthn
|
|
13
|
+
spec.summary = "WebAuthn ruby library"
|
|
14
|
+
spec.description = "Make your Ruby/Rails web server become a conformant WebAuthn Relying Party"
|
|
14
15
|
spec.homepage = "https://github.com/cedarcode/webauthn-ruby"
|
|
15
16
|
spec.license = "MIT"
|
|
16
17
|
|
|
@@ -30,11 +31,12 @@ Gem::Specification.new do |spec|
|
|
|
30
31
|
spec.required_ruby_version = ">= 2.3"
|
|
31
32
|
|
|
32
33
|
spec.add_dependency "cbor", "~> 0.5.9"
|
|
33
|
-
spec.add_dependency "cose", "~> 0.
|
|
34
|
+
spec.add_dependency "cose", "~> 0.6.0"
|
|
34
35
|
spec.add_dependency "jwt", [">= 1.5", "< 3.0"]
|
|
35
36
|
spec.add_dependency "openssl", "~> 2.0"
|
|
36
37
|
spec.add_dependency "securecompare", "~> 1.0"
|
|
37
38
|
|
|
39
|
+
spec.add_development_dependency "appraisal", "~> 2.2.0"
|
|
38
40
|
spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
|
|
39
41
|
spec.add_development_dependency "byebug", "~> 11.0"
|
|
40
42
|
spec.add_development_dependency "rake", "~> 12.3"
|
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.12.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-03
|
|
12
|
+
date: 2019-04-03 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: cbor
|
|
@@ -31,14 +31,14 @@ dependencies:
|
|
|
31
31
|
requirements:
|
|
32
32
|
- - "~>"
|
|
33
33
|
- !ruby/object:Gem::Version
|
|
34
|
-
version: 0.
|
|
34
|
+
version: 0.6.0
|
|
35
35
|
type: :runtime
|
|
36
36
|
prerelease: false
|
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
|
38
38
|
requirements:
|
|
39
39
|
- - "~>"
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: 0.
|
|
41
|
+
version: 0.6.0
|
|
42
42
|
- !ruby/object:Gem::Dependency
|
|
43
43
|
name: jwt
|
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -87,6 +87,20 @@ dependencies:
|
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
89
|
version: '1.0'
|
|
90
|
+
- !ruby/object:Gem::Dependency
|
|
91
|
+
name: appraisal
|
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 2.2.0
|
|
97
|
+
type: :development
|
|
98
|
+
prerelease: false
|
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 2.2.0
|
|
90
104
|
- !ruby/object:Gem::Dependency
|
|
91
105
|
name: bundler
|
|
92
106
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,7 +177,8 @@ dependencies:
|
|
|
163
177
|
- - '='
|
|
164
178
|
- !ruby/object:Gem::Version
|
|
165
179
|
version: 0.65.0
|
|
166
|
-
description:
|
|
180
|
+
description: Make your Ruby/Rails web server become a conformant WebAuthn Relying
|
|
181
|
+
Party
|
|
167
182
|
email:
|
|
168
183
|
- gonzalo@cedarcode.com
|
|
169
184
|
- braulio@cedarcode.com
|
|
@@ -175,6 +190,7 @@ files:
|
|
|
175
190
|
- ".rspec"
|
|
176
191
|
- ".rubocop.yml"
|
|
177
192
|
- ".travis.yml"
|
|
193
|
+
- Appraisals
|
|
178
194
|
- CHANGELOG.md
|
|
179
195
|
- Gemfile
|
|
180
196
|
- LICENSE.txt
|
|
@@ -182,24 +198,31 @@ files:
|
|
|
182
198
|
- Rakefile
|
|
183
199
|
- bin/console
|
|
184
200
|
- bin/setup
|
|
201
|
+
- gemfiles/openssl_2_0.gemfile
|
|
202
|
+
- gemfiles/openssl_2_1.gemfile
|
|
185
203
|
- lib/cose/ecdsa.rb
|
|
186
204
|
- lib/webauthn.rb
|
|
187
205
|
- lib/webauthn/attestation_statement.rb
|
|
188
206
|
- lib/webauthn/attestation_statement/android_key.rb
|
|
207
|
+
- lib/webauthn/attestation_statement/android_key/authorization_list.rb
|
|
208
|
+
- lib/webauthn/attestation_statement/android_key/key_description.rb
|
|
189
209
|
- lib/webauthn/attestation_statement/android_safetynet.rb
|
|
190
210
|
- lib/webauthn/attestation_statement/base.rb
|
|
191
211
|
- lib/webauthn/attestation_statement/fido_u2f.rb
|
|
212
|
+
- lib/webauthn/attestation_statement/fido_u2f/public_key.rb
|
|
192
213
|
- lib/webauthn/attestation_statement/none.rb
|
|
193
214
|
- lib/webauthn/attestation_statement/packed.rb
|
|
194
215
|
- lib/webauthn/authenticator_assertion_response.rb
|
|
195
216
|
- lib/webauthn/authenticator_attestation_response.rb
|
|
196
217
|
- lib/webauthn/authenticator_data.rb
|
|
197
218
|
- lib/webauthn/authenticator_data/attested_credential_data.rb
|
|
198
|
-
- lib/webauthn/authenticator_data/attested_credential_data/public_key.rb
|
|
199
219
|
- lib/webauthn/authenticator_response.rb
|
|
200
220
|
- lib/webauthn/client_data.rb
|
|
201
221
|
- lib/webauthn/error.rb
|
|
202
222
|
- lib/webauthn/fake_authenticator.rb
|
|
223
|
+
- lib/webauthn/fake_authenticator/attestation_object.rb
|
|
224
|
+
- lib/webauthn/fake_authenticator/authenticator_data.rb
|
|
225
|
+
- lib/webauthn/fake_client.rb
|
|
203
226
|
- lib/webauthn/security_utils.rb
|
|
204
227
|
- lib/webauthn/version.rb
|
|
205
228
|
- webauthn.gemspec
|
|
@@ -228,5 +251,5 @@ requirements: []
|
|
|
228
251
|
rubygems_version: 3.0.3
|
|
229
252
|
signing_key:
|
|
230
253
|
specification_version: 4
|
|
231
|
-
summary: WebAuthn
|
|
254
|
+
summary: WebAuthn ruby library
|
|
232
255
|
test_files: []
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "cose/ecdsa"
|
|
4
|
-
require "cose/key/ec2"
|
|
5
|
-
|
|
6
|
-
module WebAuthn
|
|
7
|
-
class AuthenticatorData
|
|
8
|
-
class AttestedCredentialData
|
|
9
|
-
class PublicKey
|
|
10
|
-
COORDINATE_LENGTH = 32
|
|
11
|
-
|
|
12
|
-
def initialize(data)
|
|
13
|
-
@data = data
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def valid?
|
|
17
|
-
data.size >= COORDINATE_LENGTH * 2 &&
|
|
18
|
-
cose_key.x_coordinate.length == COORDINATE_LENGTH &&
|
|
19
|
-
cose_key.y_coordinate.length == COORDINATE_LENGTH &&
|
|
20
|
-
cose_key.algorithm == COSE::ECDSA::ALG_ES256
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def to_str
|
|
24
|
-
"\x04" + cose_key.x_coordinate + cose_key.y_coordinate
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
attr_reader :data
|
|
30
|
-
|
|
31
|
-
def cose_key
|
|
32
|
-
@cose_key ||= COSE::Key::EC2.deserialize(data)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|