web_authn 0.0.1 → 0.0.2

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: fd0d84bc14a9f6773426c3c509b379268f26b36ee5555fd8c3603a45cc677221
4
- data.tar.gz: 124bfe151246e6380a59984f95784f6f39671038ddf98aed99dd8d4b8b59233f
3
+ metadata.gz: 648a6cc93f91835ee94c9c5d7efafd7ba788863d61a2e44efba388c4a45f066c
4
+ data.tar.gz: a0fa7c5a9848ec784ed62297131022223b6c6c43467532cb2ebb3fa71df706ae
5
5
  SHA512:
6
- metadata.gz: 811e1addfca6f94a181bb59dc02fd1bde4ac6c5692e5a1ed5903eb8b7063db5409061ab2880e3425be6f940aac2394d90d1223dea6d4c0a1262560388ba2fa38
7
- data.tar.gz: faf55e02e28045d0cd1a767170bf20f606a9329bd6c351f8d81ea974e6c78d0e08d4d02bd2e714431cddc3ff3fd0651106325487e8c257da3fecc2072ebcf356
6
+ metadata.gz: 44f73a5564326062ce9614d3df2e3238fad34c0ce5ea8e4381a0ca81e541fb0a893670527f013c12f9698da077d946b140746972ad8718601fd1562bf7a99399
7
+ data.tar.gz: 1858d4cb4e878ba8ae0595689f307cb13a4b17e2b21e6baf1569f496d94b01cf0f486bf8758224c114a9c3052e1ac3e46210321718ea4ef9ed5c02db9b923e12
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # WebAuthn
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/web_authn`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ W3C WebAuthn (a.k.a. FIDO2) RP library in Ruby
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ [![Build Status](https://secure.travis-ci.org/nov/web_authn.png)](http://travis-ci.org/nov/web_authn)
6
6
 
7
7
  ## Installation
8
8
 
@@ -22,7 +22,12 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ See samples for usage.
26
+
27
+ Currently, there are several restrictions.
28
+ * only `none` attestation format is supported.
29
+ * only EC key w/ `P-(256|384|521)` public key is supported.
30
+ * authenticator data w/ extensions aren't supported.
26
31
 
27
32
  ## Development
28
33
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
data/lib/web_authn.rb CHANGED
@@ -6,6 +6,11 @@ require 'cose/key/ec2'
6
6
  require 'json/jwt'
7
7
 
8
8
  module WebAuthn
9
+ class Exception < StandardError; end
10
+ class InvalidContext < Exception; end
11
+ class InvalidAssertion < Exception; end
12
+ class NotImplementedError < NotImplementedError; end
13
+
9
14
  module_function
10
15
 
11
16
  def context_for(encoded_client_data_json, origin:, challenge:)
@@ -15,9 +15,9 @@ module WebAuthn
15
15
  when 'none'
16
16
  nil
17
17
  when 'packed', 'tpm', 'android-key', 'android-safetynet', 'fido-u2f'
18
- raise "Unsupported Attestation Format: #{attestation_object[:fmt]}"
18
+ raise NotImplementedError, "Unsupported Attestation Format: #{attestation_object[:fmt]}"
19
19
  else
20
- raise 'Unknown Attestation Format'
20
+ raise InvalidContext, 'Unknown Attestation Format'
21
21
  end
22
22
  self.authenticator_data = AuthenticatorData.decode attrs[:authData]
23
23
  end
@@ -30,7 +30,7 @@ module WebAuthn
30
30
  when 3
31
31
  :'P-521'
32
32
  else
33
- raise 'Non-supported EC curve'
33
+ raise NotImplementedError, 'Non-supported EC curve'
34
34
  end
35
35
  jwk = JSON::JWK.new(
36
36
  kty: :EC,
@@ -25,7 +25,7 @@ module WebAuthn
25
25
  flags = Flags.decode(_flags_)
26
26
  attested_credential_data = if flags.at?
27
27
  if flags.ex?
28
- raise 'Extension Data Not Supported Yet'
28
+ raise NotImplementedError, 'Extension Data Not Supported Yet'
29
29
  else
30
30
  AttestedCredentialData.decode auth_data.byteslice(37..-1)
31
31
  end
@@ -14,6 +14,13 @@ module WebAuthn
14
14
  self.ex = ex
15
15
  end
16
16
 
17
+ def ==(target)
18
+ up == target.up &&
19
+ uv == target.uv &&
20
+ at == target.at &&
21
+ ex == target.ex
22
+ end
23
+
17
24
  class << self
18
25
  def decode(input)
19
26
  bit_array = input.getbyte(0)
@@ -7,8 +7,12 @@ module WebAuthn
7
7
  end
8
8
 
9
9
  def verify_session!(origin:, challenge:)
10
- raise 'Invalid Client Data JSON Origin' unless client_data_json.origin == origin
11
- raise 'Invalid Client Data JSON Session' unless client_data_json.challenge == challenge
10
+ if client_data_json.origin != origin
11
+ raise InvalidContext, 'Invalid Origin'
12
+ end
13
+ if client_data_json.challenge != challenge
14
+ raise InvalidContext, 'Invalid Challenge'
15
+ end
12
16
  self
13
17
  end
14
18
 
@@ -30,7 +34,7 @@ module WebAuthn
30
34
  when 'webauthn.get'
31
35
  Authentication.new(client_data_json)
32
36
  else
33
- raise 'Unknown Client Data JSON Type'
37
+ raise InvalidContext, 'Unknown Client Data JSON Type'
34
38
  end
35
39
 
36
40
  context.verify_session!(origin: origin, challenge: challenge)
@@ -30,7 +30,7 @@ module WebAuthn
30
30
  elsif before < current
31
31
  self
32
32
  else
33
- raise 'Invalid Sign Count'
33
+ raise InvalidAssertion, 'Invalid Sign Count'
34
34
  end
35
35
  end
36
36
 
@@ -39,14 +39,15 @@ module WebAuthn
39
39
  raw_authenticator_data,
40
40
  OpenSSL::Digest::SHA256.digest(raw_client_data_json)
41
41
  ].join
42
- result = public_key.dsa_verify_asn1(
43
- OpenSSL::Digest::SHA256.digest(signature_base_string),
44
- Base64.urlsafe_decode64(signature)
42
+ result = public_key.verify(
43
+ OpenSSL::Digest::SHA256.new,
44
+ Base64.urlsafe_decode64(signature),
45
+ signature_base_string
45
46
  )
46
47
  if result
47
48
  self
48
49
  else
49
- raise 'Invalid Signature'
50
+ raise InvalidAssertion, 'Invalid Signature'
50
51
  end
51
52
  end
52
53
  end
@@ -1,9 +1,9 @@
1
1
  require 'web_authn'
2
2
 
3
- authenticator_data = 'MsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJUBAAAAOw'
3
+ authenticator_data = 'MsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJUBAAAASg'
4
4
 
5
- signature = 'MEUCIQDXp8Wqzz3ZYV7avKvH3R3XQhW7xPYb5Cq2nx3gpflDGwIgPN0tSy2mmgpI06IIKmjrIUxCvL4Rfc53mFXfVd_yL58'
6
- sign_count = 0
5
+ signature = 'MEYCIQDmAVQcoMNRJiQZe9o5jJMnYvzza3nkDpnWdmdgKYBfwAIhAINDcFyIpIB8fql4QkllVXrQOkICfi595Gkn313gYG2r'
6
+ sign_count = 73
7
7
 
8
8
  client_data_json = 'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJvcmlnaW4iOiJodHRwczovL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0'
9
9
 
@@ -1,8 +1,8 @@
1
1
  require 'web_authn'
2
2
 
3
- attestation_object = 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEMsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJVBAAAAMAAAAAAAAAAAAAAAAAAAAAAAQM1zXqvmYeVH9o2q1YcBZDSlkhvVs_2RjnKESVUktkQwQnYcU8jdo-duNLKrIOZNg0g4RCm0UMDZxtdXhR2bCu2lAQIDJiABIVggDMGhDLXoZit2uSMLyL-_emlFrGzlH7b2KpKpgYNzPRYiWCAl795OxcS2QimEnC9Jl_pNG3Gy_9O6m3_GbZdGsk90aw'
3
+ attestation_object = 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEMsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJVBAAAASQAAAAAAAAAAAAAAAAAAAAAAQA3sNJGmV3TJ2Y5pYDZP-DpZIku6rXhmhUZJ5kNNlaI3-4wwfhl296tAl52zStVDUsLVIm4_e4apQWUwl6eGnXalAQIDJiABIVggmQvMomjF0F3asbDWda13XeA1UbXdcS5j3Wg3G1LtgaMiWCBlNRc_WstNxVl56t6fVIJuVjMZqon1GpDp_UDDTXO7_g'
4
4
 
5
- client_data_json = 'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJuZXdfa2V5c19tYXlfYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViLWF1dGhuLnNlbGYtaXNzdWVkLmFwcCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ'
5
+ client_data_json = 'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJvcmlnaW4iOiJodHRwczovL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0'
6
6
 
7
7
  origin = 'https://web-authn.self-issued.app'
8
8
  challenge = 'cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy'
@@ -0,0 +1,45 @@
1
+ RSpec.describe WebAuthn::AuthenticatorData::Flags do
2
+ describe '.decode' do
3
+ subject { described_class.decode [bits].pack('b*') }
4
+
5
+ describe 'when all false' do
6
+ let(:bits) { '00000000' }
7
+ its(:up?) { should == false }
8
+ its(:uv?) { should == false }
9
+ its(:at?) { should == false }
10
+ its(:ex?) { should == false }
11
+ end
12
+
13
+ describe 'when up is on' do
14
+ let(:bits) { '10000000' }
15
+ its(:up?) { should == true }
16
+ its(:uv?) { should == false }
17
+ its(:at?) { should == false }
18
+ its(:ex?) { should == false }
19
+ end
20
+
21
+ describe 'when uv is on' do
22
+ let(:bits) { '00100000' }
23
+ its(:up?) { should == false }
24
+ its(:uv?) { should == true }
25
+ its(:at?) { should == false }
26
+ its(:ex?) { should == false }
27
+ end
28
+
29
+ describe 'when at is on' do
30
+ let(:bits) { '00000010' }
31
+ its(:up?) { should == false }
32
+ its(:uv?) { should == false }
33
+ its(:at?) { should == true }
34
+ its(:ex?) { should == false }
35
+ end
36
+
37
+ describe 'when ex is on' do
38
+ let(:bits) { '00000001' }
39
+ its(:up?) { should == false }
40
+ its(:uv?) { should == false }
41
+ its(:at?) { should == false }
42
+ its(:ex?) { should == true }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,72 @@
1
+ RSpec.describe WebAuthn::Context::Authentication do
2
+ let(:context_instance) do
3
+ WebAuthn.context_for(
4
+ client_data_json,
5
+ origin: origin,
6
+ challenge: challenge
7
+ )
8
+ end
9
+
10
+ describe '#verify!' do
11
+ let(:client_data_json) do
12
+ 'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJvcmlnaW4iOiJodHRwczovL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0'
13
+ end
14
+ let(:origin) { 'https://web-authn.self-issued.app' }
15
+ let(:challenge) { 'cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy' }
16
+ let(:rp_id_hash) do
17
+ 'MsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJU'
18
+ end
19
+ let(:flags) do
20
+ WebAuthn::AuthenticatorData::Flags.new(
21
+ up: true, uv: false, at: false, ex: false
22
+ )
23
+ end
24
+ let(:public_key) do
25
+ OpenSSL::PKey::EC.new <<~PEM
26
+ -----BEGIN PUBLIC KEY-----
27
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpNU/8TjYoyN8FlhZ+YsOMAvyfQ4
28
+ i6/JN0/DPXuZMoxLvdb1vjh7vPUt2Osw3Bq+0NZsx3U/8kmpFuwsZhTi9A==
29
+ -----END PUBLIC KEY-----
30
+ PEM
31
+ end
32
+ let(:sign_count) { 74 }
33
+ let(:signature) do
34
+ 'MEYCIQDmAVQcoMNRJiQZe9o5jJMnYvzza3nkDpnWdmdgKYBfwAIhAINDcFyIpIB8fql4QkllVXrQOkICfi595Gkn313gYG2r'
35
+ end
36
+ let(:authenticator_data) do
37
+ 'MsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJUBAAAASg'
38
+ end
39
+ subject do
40
+ context_instance.verify!(
41
+ authenticator_data,
42
+ public_key: public_key,
43
+ sign_count: sign_count - 1,
44
+ signature: signature
45
+ )
46
+ end
47
+
48
+ its(:rp_id_hash) { should == rp_id_hash }
49
+ its(:flags) { should == flags }
50
+ its(:sign_count) { should == sign_count }
51
+
52
+ context 'when sign count is invalid' do
53
+ let(:sign_count) { 75 }
54
+ it do
55
+ expect do
56
+ subject
57
+ end.to raise_error WebAuthn::InvalidAssertion, 'Invalid Sign Count'
58
+ end
59
+ end
60
+
61
+ context 'when signature is invalid' do
62
+ let(:signature) do
63
+ 'MEQCIB09d1yFCLxDUJdYIW3HwrZPsNqA1jwLqv_EqyN-xFpYAiAZ5h3RvpxCKvcbwQhqFdw2Chw5rmVD6aAZBNC9tJNmZw'
64
+ end
65
+ it do
66
+ expect do
67
+ subject
68
+ end.to raise_error WebAuthn::InvalidAssertion, 'Invalid Signature'
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,46 @@
1
+ RSpec.describe WebAuthn::Context::Registration do
2
+ let(:context) { registration_context }
3
+ let(:context_instance) do
4
+ WebAuthn.context_for(
5
+ client_data_json,
6
+ origin: context[:origin],
7
+ challenge: context[:challenge]
8
+ )
9
+ end
10
+
11
+ describe '#verify!' do
12
+ let(:credential_id) do
13
+ 'Dew0kaZXdMnZjmlgNk_4OlkiS7qteGaFRknmQ02Vojf7jDB-GXb3q0CXnbNK1UNSwtUibj97hqlBZTCXp4addg'
14
+ end
15
+ let(:rp_id_hash) do
16
+ 'MsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJU'
17
+ end
18
+ let(:flags) do
19
+ WebAuthn::AuthenticatorData::Flags.new(
20
+ up: true, uv: false, at: true, ex: false
21
+ )
22
+ end
23
+ let(:public_key_pem) do
24
+ <<~PEM
25
+ -----BEGIN PUBLIC KEY-----
26
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmQvMomjF0F3asbDWda13XeA1UbXd
27
+ cS5j3Wg3G1LtgaNlNRc/WstNxVl56t6fVIJuVjMZqon1GpDp/UDDTXO7/g==
28
+ -----END PUBLIC KEY-----
29
+ PEM
30
+ end
31
+ let(:sign_count) { 73 }
32
+ let(:attestation_object) do
33
+ 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEMsuA3KzDw1JGLLAfO_4wLebzcS8w_SDs0Zw7pbhYlJVBAAAASQAAAAAAAAAAAAAAAAAAAAAAQA3sNJGmV3TJ2Y5pYDZP-DpZIku6rXhmhUZJ5kNNlaI3-4wwfhl296tAl52zStVDUsLVIm4_e4apQWUwl6eGnXalAQIDJiABIVggmQvMomjF0F3asbDWda13XeA1UbXdcS5j3Wg3G1LtgaMiWCBlNRc_WstNxVl56t6fVIJuVjMZqon1GpDp_UDDTXO7_g'
34
+ end
35
+ subject { context_instance.verify! attestation_object }
36
+
37
+ its(:credential_id) { should == credential_id }
38
+ its(:rp_id_hash) { should == rp_id_hash }
39
+ its(:flags) { should == flags }
40
+ its(:public_key) { should be_instance_of OpenSSL::PKey::EC }
41
+ its(:public_key_pem) do
42
+ subject.public_key.to_pem.should == public_key_pem
43
+ end
44
+ its(:sign_count) { should == sign_count }
45
+ end
46
+ end
data/spec/spec_helper.rb CHANGED
@@ -13,3 +13,5 @@ RSpec.configure do |config|
13
13
  c.syntax = [:should, :expect]
14
14
  end
15
15
  end
16
+
17
+ require 'support/context_factory'
@@ -0,0 +1,26 @@
1
+ module ContextFactory
2
+ extend RSpec::Core::SharedContext
3
+
4
+ let(:base_context) do
5
+ {
6
+ challenge: SecureRandom.hex(8),
7
+ origin: 'https://rp.example.com'
8
+ }
9
+ end
10
+ let(:registration_context) do
11
+ base_context.merge(type: 'webauthn.create')
12
+ end
13
+ let(:authentication_context) do
14
+ base_context.merge(type: 'webauthn.get')
15
+ end
16
+ let(:unknown_context) do
17
+ base_context
18
+ end
19
+ let(:client_data_json) do
20
+ Base64.urlsafe_encode64(context.to_json, padding: false)
21
+ end
22
+ end
23
+
24
+ RSpec.configure do |config|
25
+ config.include ContextFactory
26
+ end
@@ -1,5 +1,54 @@
1
1
  RSpec.describe WebAuthn do
2
- it "does something useful" do
3
- expect(true).to eq(true)
2
+ shared_examples_for :invalid_context do
3
+ it do
4
+ expect do
5
+ subject
6
+ end.to raise_error WebAuthn::InvalidContext
7
+ end
8
+ end
9
+
10
+ shared_examples_for :context_validator do
11
+ context 'when invalid origin' do
12
+ let(:origin) { 'https://other-rp.example.com' }
13
+ it_behaves_like :invalid_context
14
+ end
15
+
16
+ context 'when invalid challenge' do
17
+ let(:challenge) { SecureRandom.hex(8) }
18
+ it_behaves_like :invalid_context
19
+ end
20
+ end
21
+
22
+ describe '#context_for' do
23
+ let(:origin) { context[:origin] }
24
+ let(:challenge) { context[:challenge] }
25
+ subject do
26
+ described_class.context_for(
27
+ client_data_json,
28
+ origin: origin,
29
+ challenge: challenge,
30
+ )
31
+ end
32
+
33
+ context 'when registration context' do
34
+ let(:context) { registration_context }
35
+ it_behaves_like :context_validator
36
+ it { should be_instance_of WebAuthn::Context::Registration }
37
+ it { should be_registration }
38
+ it { should_not be_authentication }
39
+ end
40
+
41
+ context 'when authentication context' do
42
+ let(:context) { authentication_context }
43
+ it_behaves_like :context_validator
44
+ it { should be_instance_of WebAuthn::Context::Authentication }
45
+ it { should_not be_registration }
46
+ it { should be_authentication }
47
+ end
48
+
49
+ context 'when unknown context' do
50
+ let(:context) { unknown_context }
51
+ it_behaves_like :invalid_context
52
+ end
4
53
  end
5
54
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_authn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - nov matake
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-03 00:00:00.000000000 Z
11
+ date: 2018-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -149,7 +149,11 @@ files:
149
149
  - samples/authentication_response.rb
150
150
  - samples/concept.rb
151
151
  - samples/registration_response.rb
152
+ - spec/authenticator_data/flags_spec.rb
153
+ - spec/context/authentication_spec.rb
154
+ - spec/context/registration_spec.rb
152
155
  - spec/spec_helper.rb
156
+ - spec/support/context_factory.rb
153
157
  - spec/web_authn_spec.rb
154
158
  - web_authn.gemspec
155
159
  homepage: https://github.com/nov/web_authn
@@ -177,5 +181,9 @@ signing_key:
177
181
  specification_version: 4
178
182
  summary: W3C WebAuthn (a.k.a. FIDO2) RP library in Ruby
179
183
  test_files:
184
+ - spec/authenticator_data/flags_spec.rb
185
+ - spec/context/authentication_spec.rb
186
+ - spec/context/registration_spec.rb
180
187
  - spec/spec_helper.rb
188
+ - spec/support/context_factory.rb
181
189
  - spec/web_authn_spec.rb