web_authn 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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