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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 368f1cffa77727f2edab0c872a6e9414af8db2871edfbb3623f06273bb71f5ae
4
- data.tar.gz: 4b21d7e5cd9ff589bb97f4264d0dbf7f3f04da53aa619893f88a1d831d7c2755
3
+ metadata.gz: c0b7730e28517a0a0c8490fe77a7f1826de3914f7af3ccf6aefcc1e5268e4a45
4
+ data.tar.gz: 5a9433f01315b4360388d4d08667b041539f5fef43d48034d1deece03d24b48b
5
5
  SHA512:
6
- metadata.gz: c88fb415b6141de835c2d1536f8e859b1ba1a146b1e9c13971cf8bb07478bda5efa9c6a85f69c06466b91984b7f00e14f0cf29229cf14bc10f70040659411586
7
- data.tar.gz: f94c85b184e96399f71bc7c070b691160640a5de898e381cd23f4bddc2fce139a2c523172cc8aeb682715b66a038f1ad3ed7798893a645d31eef53268784dc08
6
+ metadata.gz: cbfc86b1a82b1906a1adda4ab47e4cb7a523b520ecb533aa9587e047f463f74eccde0a88bff0ffb3acc037dc163cdbd40bad173c3e35a52492c230432897aeef
7
+ data.tar.gz: 887545a11d8d387deb137d228fb2f7a0c13f4462d7ca8b9247b68be7cdb40fcf478ee7b53bb5d39d7f89b6230d8c332676c20c7f5623f272522838f14b5213ef
data/.gitignore CHANGED
@@ -11,4 +11,5 @@
11
11
  .rspec_status
12
12
 
13
13
  /Gemfile.lock
14
+ /gemfiles/*.gemfile.lock
14
15
  /.byebug_history
data/.rubocop.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.3
3
3
  DisabledByDefault: true
4
+ Exclude:
5
+ - "gemfiles/**/*"
4
6
 
5
7
  Bundler:
6
8
  Enabled: true
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.4
8
- - 2.4.5
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise "openssl_2_1" do
4
+ gem "openssl", "~> 2.1.0"
5
+ end
6
+
7
+ appraise "openssl_2_0" do
8
+ gem "openssl", "~> 2.0.0"
9
+ end
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
- Easily implement WebAuthn in your ruby/rails app
3
+ Make your Ruby/Rails web server become a conformant WebAuthn Relying Party.
4
4
 
5
5
  [![Gem](https://img.shields.io/gem/v/webauthn.svg?style=flat-square)](https://rubygems.org/gems/webauthn)
6
6
  [![Travis](https://img.shields.io/travis/cedarcode/webauthn-ruby/master.svg?style=flat-square)](https://travis-ci.org/cedarcode/webauthn-ruby)
7
+ [![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)
7
8
 
8
9
  ## What is WebAuthn?
9
10
 
10
- - [WebAuthn W3C Recommendation](https://www.w3.org/TR/webauthn/)
11
- - [WebAuthn intro by Yubico](https://www.yubico.com/webauthn/)
12
- - [WebAuthn in Wikipedia](https://en.wikipedia.org/wiki/WebAuthn)
13
- - [Web Authentication API in MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
14
- - [WebAuthn article with Google IO 2018 talk](https://developers.google.com/web/updates/2018/05/webauthn)
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 gem will help your ruby 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 work, you will also need
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
- ### A conforming User Agent
25
+ A very small set of known conformant pairs are for example:
21
26
 
22
- Currently supporting [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API):
23
- - [Mozilla Firefox](https://www.mozilla.org/firefox/) 60+
24
- - [Google Chrome](https://www.google.com/chrome/) 67+
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
- ### A conforming Authenticator
31
+ For a detailed picture about what is conformant and what not, you can refer to:
28
32
 
29
- * Roaming authenticators
30
- * [Security Key by Yubico](https://www.yubico.com/product/security-key-by-yubico/)
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. Webauthn-ruby exposes [WebAuthn::FakeAuthenticator](https://github.com/cedarcode/webauthn-ruby/blob/master/lib/webauthn/fake_authenticator.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).
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
 
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "openssl", "~> 2.0.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "openssl", "~> 2.1.0"
6
+
7
+ gemspec path: "../"
@@ -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[ATTESTATION_CHALLENGE_INDEX].value, client_data_hash)
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.none? { |data| data.tag == ALL_APPLICATIONS_TAG } &&
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.detect { |data| data.tag == ORIGIN_TAG }&.value&.at(0)&.value == KM_ORIGIN_GENERATED ||
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.detect { |data| data.tag == PURPOSE_TAG }&.value&.at(0)&.value&.at(0)&.value == KM_PURPOSE_SIGN ||
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[SOFTWARE_ENFORCED_INDEX].value
57
+ key_description.tee_enforced
69
58
  end
70
59
 
71
60
  def software_enforced
72
- key_description[TEE_ENFORCED_INDEX].value
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
- OpenSSL::ASN1.decode(raw_key_description.value).value
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) && certificate_public_key.check_key
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?(public_key_bytes)
36
- group = OpenSSL::PKey::EC::Group.new("prime256v1")
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 "webauthn/authenticator_data/attested_credential_data/public_key"
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
- group = OpenSSL::PKey::EC::Group.new("prime256v1")
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 && public_key.valid?
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.to_str)
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 ||= PublicKey.new(data_at(public_key_position, public_key_length))
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
- class Base
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
- def bit(flag)
83
- if context[flag].nil? || context[flag]
84
- "1"
85
- else
86
- "0"
87
- end
88
- end
13
+ def initialize
14
+ @credentials = {}
15
+ end
89
16
 
90
- def origin
91
- @origin ||= context[:origin] || fake_origin
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
- def encode(bytes)
95
- Base64.urlsafe_encode64(bytes, padding: false)
96
- end
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
- def fake_challenge
99
- SecureRandom.random_bytes(32)
100
- end
29
+ credentials[rp_id] ||= {}
30
+ credentials[rp_id][credential_id] = credential_key
101
31
 
102
- def fake_origin
103
- "http://localhost"
104
- end
32
+ attestation_object
105
33
  end
106
34
 
107
- class Create < Base
108
- def attestation_object
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
- private
38
+ if credential_options
39
+ credential_id, credential_key = credential_options.first
117
40
 
118
- def attested_credential_data
119
- aaguid + [credential_id.length].pack("n*") + credential_id + cose_credential_public_key
120
- end
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
- def aaguid
123
- @aaguid ||= SecureRandom.random_bytes(16)
124
- end
47
+ signature = credential_key.sign("SHA256", authenticator_data + client_data_hash)
125
48
 
126
- def cose_credential_public_key
127
- fake_cose_credential_key(
128
- x_coordinate: key_bytes(credential_key.public_key)[1..32],
129
- y_coordinate: key_bytes(credential_key.public_key)[33..64]
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
- def type
134
- "webauthn.create"
135
- end
59
+ private
136
60
 
137
- def fake_cose_credential_key(algorithm: nil, x_coordinate: nil, y_coordinate: nil)
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
- def key_bytes(public_key)
158
- public_key.to_bn.to_s(2)
159
- end
63
+ def new_credential
64
+ [SecureRandom.random_bytes(16), OpenSSL::PKey::EC.new("prime256v1").generate_key]
160
65
  end
161
66
 
162
- class Get < Base
163
- def signature
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.hexdigest(first_string)
14
- second_string_sha256 = ::Digest::SHA256.hexdigest(second_string)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebAuthn
4
- VERSION = "1.11.0"
4
+ VERSION = "1.12.0"
5
5
  end
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 in ruby ― Ruby implementation of a WebAuthn Relying Party"
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.4.1"
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.11.0
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-15 00:00:00.000000000 Z
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.4.1
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.4.1
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 in ruby ― Ruby implementation of a WebAuthn Relying Party
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