web_authn 0.2.2 → 0.3.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
  SHA1:
3
- metadata.gz: 0ff488512635e47137ce3c48273fb327c4a381a4
4
- data.tar.gz: '09322b2c65d28acf9e26965b3bc080e47c687d24'
3
+ metadata.gz: ba07fe60ce73457f0f61ddc7c53dbf2b07886d93
4
+ data.tar.gz: a9d76ab96805dd91c619551c09e0701a2a7b0795
5
5
  SHA512:
6
- metadata.gz: a0716efec15f12ff84672def323021fdb43890968551ffc18ffb0ce6b7fbbff949809ae54936cc683802b0ad573995cbc325f59a498fba917860d66036dbb51e
7
- data.tar.gz: 9706f9c8c1fe371118d8f703bae4bfefeeade541661575b4c591ae4159ba15b320e3a3c90acafec0c9297b10014b9ae6a8e9fb88d28e7e22c534f5de48c38a37
6
+ metadata.gz: 4550a5b91a35c2c5e302b58072b2a063b203370a8128d03cc851d7866e8f6bee92eab6d86dda0a41dcc366b47f9c86bfc9a0eef1e6f4a7df24bba18bbf61e32f
7
+ data.tar.gz: 5aa59a73d18cfd24b6bd86431676c4c570ee6e5ff0e7af5c83fc70767a134ec87a577d1a7ebe0aab187dd92d9e467987b0e0adb085fe244aa57be6d4ac6f9849
data/README.md CHANGED
@@ -26,6 +26,43 @@ $ gem install web_authn
26
26
 
27
27
  ## Usage
28
28
 
29
+ ```ruby
30
+ context = WebAuthn.context_for(
31
+ client_data_json, # NOTE: URL-safe Base64 encoded
32
+ origin: request.base_url,
33
+ challenge: session[:challenge],
34
+ )
35
+
36
+ if context.registration?
37
+ context.verify!(
38
+ attestation_object # URL-safe Base64 encoded
39
+ )
40
+ context.credential_id
41
+ context.public_key # => `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC`
42
+ context.public_cose_key # => `COSE::Key::RSA` or `COSE::Key::EC2` ref.) https://github.com/nov/cose-key
43
+ context.sign_count # => `Integer`
44
+ elsif context.authentication?
45
+ context.verify!(
46
+ authenticator_data, # URL-safe Base64 encoded
47
+
48
+ # NOTE:
49
+ # either 'public_key' or 'public_cose_key' is required.
50
+ # if `public_key` is given, you can also specify `digest` (default: `OpenSSL::Digest::SHA256.new`).
51
+ # if `public_cose_key` is given, it includes digest size information, so no `digest` is required.
52
+
53
+ # public_key: public_key, # `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC`
54
+ # digest: OpenSSL::Digest::SHA256.new, # `OpenSSL::Digest::SHA(1|256|384|512)`` (default: `OpenSSL::Digest::SHA256`)
55
+ public_cose_key: public_cose_key, # `COSE::Key::RSA` or `COSE::Key::EC` ref.) https://github.com/nov/cose-key
56
+
57
+ sign_count: previously_stored_sign_count,
58
+ signature: signature # URL-safe Base64 encoded
59
+ )
60
+ context.sign_count # => Integer
61
+ else
62
+ # should never happen.
63
+ end
64
+ ```
65
+
29
66
  See sample code in this repository, or [working sample site](https://web-authn.herokuapp.com/).
30
67
 
31
68
  Currently, there are several restrictions.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.3.0
data/lib/web_authn.rb CHANGED
@@ -3,10 +3,12 @@ require 'active_support'
3
3
  require 'active_support/core_ext'
4
4
  require 'cbor'
5
5
  require 'cose/key'
6
+ require 'json/jwt'
6
7
 
7
8
  module WebAuthn
8
9
  class Exception < StandardError; end
9
10
  class InvalidContext < Exception; end
11
+ class InvalidAttestation < Exception; end
10
12
  class InvalidAssertion < Exception; end
11
13
  class NotImplementedError < NotImplementedError; end
12
14
 
@@ -22,6 +24,7 @@ module WebAuthn
22
24
  end
23
25
 
24
26
  require 'web_authn/attestation_object'
27
+ require 'web_authn/attestation_statement'
25
28
  require 'web_authn/attested_credential_data'
26
29
  require 'web_authn/authenticator_data'
27
30
  require 'web_authn/client_data_json'
@@ -9,24 +9,35 @@ module WebAuthn
9
9
  delegate method, to: :authenticator_data
10
10
  end
11
11
 
12
- def initialize(attrs)
13
- self.format = attrs[:fmt]
12
+ def initialize(fmt:, att_stmt:, auth_data:)
13
+ self.format = fmt
14
14
  self.attestation_statement = case format
15
15
  when 'none'
16
16
  nil
17
- when 'packed', 'tpm', 'android-key', 'android-safetynet', 'fido-u2f'
17
+ when 'android-safetynet'
18
+ AttestationStatement::AndroidSafetynet.decode att_stmt
19
+ when 'packed', 'tpm', 'android-key', 'fido-u2f'
18
20
  raise NotImplementedError, "Unsupported Attestation Format: #{format}"
19
21
  else
20
22
  raise InvalidContext, 'Unknown Attestation Format'
21
23
  end
22
- self.authenticator_data = AuthenticatorData.decode attrs[:authData]
24
+ self.authenticator_data = AuthenticatorData.decode auth_data
25
+ end
26
+
27
+ def verify_signature!(client_data_json)
28
+ attestation_statement.try(:verify!, authenticator_data, client_data_json)
23
29
  end
24
30
 
25
31
  class << self
26
32
  def decode(encoded_attestation_object)
27
- new CBOR.decode(
33
+ cbor = CBOR.decode(
28
34
  Base64.urlsafe_decode64 encoded_attestation_object
29
35
  ).with_indifferent_access
36
+ new(
37
+ fmt: cbor[:fmt],
38
+ att_stmt: cbor[:attStmt],
39
+ auth_data: cbor[:authData]
40
+ )
30
41
  end
31
42
  end
32
43
  end
@@ -0,0 +1,6 @@
1
+ module WebAuthn
2
+ class AttestationStatement
3
+ end
4
+ end
5
+
6
+ require 'web_authn/attestation_statement/android_safetynet'
@@ -0,0 +1,81 @@
1
+ module WebAuthn
2
+ class AttestationStatement
3
+ class AndroidSafetynet < AttestationStatement
4
+ attr_accessor :ver, :response, :certs
5
+
6
+ def initialize(ver:, response:)
7
+ self.ver = ver
8
+ self.response = response
9
+ self.certs = response.x5c.collect do |x5c|
10
+ OpenSSL::X509::Certificate.new(
11
+ Base64.decode64 x5c
12
+ )
13
+ end
14
+ end
15
+
16
+ def verify!(authenticator_data, client_data_json)
17
+ verify_nonce! authenticator_data, client_data_json
18
+ verify_signature!
19
+ verify_certificate!
20
+
21
+ # TODO: put more ref.) https://www.w3.org/TR/webauthn/#android-safetynet-attestation
22
+ unless response[:ctsProfileMatch]
23
+ raise InvalidAttestation, 'Invalid Android Safetynet Response: ctsProfileMatch'
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def verify_nonce!(authenticator_data, client_data_json)
30
+ nonce = Base64.encode64(
31
+ OpenSSL::Digest::SHA256.digest [
32
+ authenticator_data.raw,
33
+ OpenSSL::Digest::SHA256.digest(client_data_json.raw)
34
+ ].join
35
+ ).strip
36
+ unless response[:nonce] == nonce
37
+ raise InvalidAttestation, 'Invalid Android Safetynet Response: nonce'
38
+ end
39
+ end
40
+
41
+ def verify_signature!
42
+ response.verify! certs.first.public_key
43
+ rescue JSON::JWS::VerificationFailed => e
44
+ raise InvalidAttestation, 'Invalid Android Safetynet Response: signature'
45
+ end
46
+
47
+ def verify_certificate!
48
+ signing_cert = certs.first
49
+ remaining_chain = certs[1..-1]
50
+
51
+ store = OpenSSL::X509::Store.new
52
+ store.set_default_paths
53
+ valid_chain = store.verify(signing_cert, remaining_chain)
54
+
55
+ valid_subject = signing_cert.subject.to_a.detect do |key, value, type|
56
+ key == 'CN'
57
+ end.second == 'attest.android.com'
58
+
59
+ valid_timestamp = (
60
+ signing_cert.not_after > Time.now &&
61
+ signing_cert.not_before < Time.now
62
+ )
63
+
64
+ # TODO: do we need CRL check?
65
+
66
+ unless valid_chain && valid_subject && valid_timestamp
67
+ raise InvalidAttestation, 'Invalid Android Safetynet Response: certificate'
68
+ end
69
+ end
70
+
71
+ class << self
72
+ def decode(att_stmt)
73
+ new(
74
+ ver: att_stmt[:ver],
75
+ response: JSON::JWT.decode(att_stmt[:response], :skip_verification)
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -2,23 +2,23 @@ module WebAuthn
2
2
  class ClientDataJSON
3
3
  attr_accessor :type, :origin, :challenge, :raw
4
4
 
5
- def initialize(attrs = {})
6
- self.type = attrs[:type]
7
- self.origin = attrs[:origin]
8
- self.challenge = attrs[:challenge]
9
- self.raw = attrs[:raw]
5
+ def initialize(type:, origin:, challenge:, raw: nil)
6
+ self.type = type
7
+ self.origin = origin
8
+ self.challenge = challenge
9
+ self.raw = raw
10
10
  end
11
11
 
12
12
  class << self
13
13
  def decode(encoded_client_data_json)
14
- raw_client_data_json = Base64.urlsafe_decode64 encoded_client_data_json
15
- attrs = JSON.parse(
16
- raw_client_data_json
17
- ).merge(
18
- raw: raw_client_data_json
19
- ).with_indifferent_access
20
- attrs[:challenge] = Base64.urlsafe_decode64 attrs[:challenge]
21
- new attrs
14
+ raw = Base64.urlsafe_decode64 encoded_client_data_json
15
+ json = JSON.parse(raw).with_indifferent_access
16
+ new(
17
+ type: json[:type],
18
+ origin: json[:origin],
19
+ challenge: Base64.urlsafe_decode64(json[:challenge]),
20
+ raw: raw
21
+ )
22
22
  end
23
23
  end
24
24
  end
@@ -4,7 +4,8 @@ module WebAuthn
4
4
  attr_accessor :attestation_object
5
5
 
6
6
  # TODO: will need more methods, or let developers access deep methods by themselves.
7
- %i(credential_id rp_id_hash flags public_key public_cose_key sign_count).each do |method|
7
+ %i(credential_id rp_id_hash flags public_key public_cose_key sign_count
8
+ attestation_statement).each do |method|
8
9
  delegate method, to: :attestation_object
9
10
  end
10
11
 
@@ -17,6 +18,7 @@ module WebAuthn
17
18
  encoded_attestation_object
18
19
  )
19
20
  verify_flags!
21
+ verify_signature!
20
22
  self
21
23
  end
22
24
 
@@ -26,6 +28,10 @@ module WebAuthn
26
28
  super
27
29
  raise InvalidAssertion, 'Missing Flag: "at"' unless flags.at?
28
30
  end
31
+
32
+ def verify_signature!
33
+ attestation_object.verify_signature! client_data_json
34
+ end
29
35
  end
30
36
  end
31
37
  end
@@ -43,5 +43,43 @@ RSpec.describe WebAuthn::Context::Registration do
43
43
  subject.public_key.to_pem.should == public_key_pem
44
44
  end
45
45
  its(:sign_count) { should == sign_count }
46
+
47
+ context 'when android-safetynet attestation given' do
48
+ let(:context) do
49
+ {
50
+ origin: 'https://web-authn.self-issued.app',
51
+ challenge: 'random-string-generated-by-rp-server'
52
+ }
53
+ end
54
+ let(:client_data_json) do
55
+ 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRMWE4wY21sdVp5MW5aVzVsY21GMFpXUXRZbmt0Y25BdGMyVnlkbVZ5Iiwib3JpZ2luIjoiaHR0cHM6XC9cL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uY2hyb21lLmNhbmFyeSJ9'
56
+ end
57
+ let(:attestation_object) do
58
+ 'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDEyODc0MDQxaHJlc3BvbnNlWRMHZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxGYVdwRFEwRXpTMmRCZDBsQ1FXZEpTVmxyV1c4MVJqQm5PRFpyZDBSUldVcExiMXBKYUhaalRrRlJSVXhDVVVGM1ZrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaElha0ZqUW1kT1ZrSkJiMVJHVldSMllqSmtjMXBUUWxWamJsWjZaRU5DVkZwWVNqSmhWMDVzWTNwRmJFMURUVWRCTVZWRlFYaE5ZMUl5T1haYU1uaHNTVVZzZFdSSFZubGliVll3U1VWR01XUkhhSFpqYld3d1pWTkNTRTE2UVdWR2R6QjRUbnBGZVUxRVVYaE5la1UwVGtST1lVWjNNSGhQUkVWNVRVUk5kMDFFUVhkTlJFSmhUVWQzZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOVWsxM1JWRlpSRlpSVVVsRVFYQkVXVmQ0Y0ZwdE9YbGliV3hvVFZKWmQwWkJXVVJXVVZGSVJFRXhUbUl6Vm5Wa1IwWndZbWxDVjJGWFZqTk5VazEzUlZGWlJGWlJVVXRFUVhCSVlqSTVibUpIVldkVFZ6VnFUVkp6ZDBkUldVUldVVkZFUkVKS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVU5WYWpoM1dXOVFhWGhMWW1KV09ITm5XV2QyVFZSbVdDdGtTWE5HVkU5clowdFBiR2hVTUdrd1ltTkVSbHBMTW5KUGVFcGFNblZUVEZOV2FGbDJhWEJhVGtVelNFcFJXWFYxV1hkR2FtbDVLM2xyWm1GMFFVZFRhbEo2UmpGaU16RjFORE12TjI5SE5XcE5hRE5UTXpkaGJIZHFWV0k0UTFkcFZIaHZhWEJXVDFsM1MwdDZkVlY1YTNGRlEzUnFiR2hLTkVGclYyRkVVeXRhZUV0RmNVOWhaVGwwYmtOblpVaHNiRnBGTDA5U1oyVk5ZWGd5V0U1RGIwZzJjM0pVUlZKamEzTnFlbHBhY2tGWGVFdHpaR1oyVm5KWVRucERVamxFZUZaQlUzVkpOa3g2ZDJnNFJGTnNNa1ZQYjJ0aWMyRnVXaXNyTDBweFRXVkJRa1ptVUhkcWVYZHlZakJ3Y2tWVmVUQndZV1ZXYzNWa0t6QndaV1Y0U3k4MUswVTJhM0JaUjBzMFdrc3libXR2Vmt4MVowVTFkR0ZJY2tGcU9ETlJLMUJQWW1KMlQzcFhZMFpyY0c1V1MzbHFielpMVVVGdFdEWlhTa0ZuVFVKQlFVZHFaMmRHUjAxSlNVSlJha0ZVUW1kT1ZraFRWVVZFUkVGTFFtZG5ja0puUlVaQ1VXTkVRVlJCWkVKblRsWklVa1ZGUm1wQlZXZG9TbWhrU0ZKc1l6TlJkVmxYTld0amJUbHdXa00xYW1JeU1IZGhRVmxKUzNkWlFrSlJWVWhCVVVWRldFUkNZVTFETUVkRFEzTkhRVkZWUmtKNlFVTm9hVVp2WkVoU2QwOXBPSFpqUjNSd1RHMWtkbUl5WTNaYU0wNTVUV2s1U0ZaR1RraFRWVVpJVFhrMWFtTnVVWGRMVVZsSlMzZFpRa0pSVlVoTlFVZEhTRmRvTUdSSVFUWk1lVGwyV1ROT2QweHVRbkpoVXpWdVlqSTVia3d3WkZWVk1HUktVVlZqZWsxQ01FZEJNVlZrUkdkUlYwSkNVVWM0U1hKUmRFWlNOa05WVTJ0cGEySXpZV2x0YzIweU5tTkNWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDT0VkQk1WVmtTWGRSV1UxQ1lVRkdTR1pEZFVaRFlWb3pXakp6VXpORGFIUkRSRzlJTm0xbWNuQk1UVU5GUjBFeFZXUkpRVkZoVFVKbmQwUkJXVXRMZDFsQ1FrRklWMlZSU1VaQmVrRkpRbWRhYm1kUmQwSkJaMGwzVFZGWlJGWlNNR1pDUTI5M1MwUkJiVzlEVTJkSmIxbG5ZVWhTTUdORWIzWk1NazU1WWtNMWQyRXlhM1ZhTWpsMlduazVTRlpHVGtoVFZVWklUWGsxYW1OdGQzZEVVVmxLUzI5YVNXaDJZMDVCVVVWTVFsRkJSR2RuUlVKQlJpOVNlazV1UXpWRWVrSlZRblJ1YURKdWRFcE1WMFZSYURsNlJXVkdXbVpRVERsUmIydHliRUZ2V0dkcVYyZE9PSEJUVWxVeGJGWkhTWEIwZWsxNFIyaDVNeTlQVWxKYVZHRTJSREpFZVRob2RrTkVja1pKTXl0c1Exa3dNVTFNTlZFMldFNUZOVkp6TW1ReFVtbGFjRTF6ZWtRMFMxRmFUa2N6YUZvd1FrWk9VUzlqYW5KRGJVeENUMGRMYTBWVk1XUnRRVmh6UmtwWVNtbFBjakpEVGxSQ1QxUjFPVVZpVEZkb1VXWmtRMFl4WW5kNmVYVXJWelppVVZOMk9GRkVialZQWkUxVEwxQnhSVEZrUldkbGRDODJSVWxTUWpjMk1VdG1XbEVyTDBSRk5reHdNMVJ5V2xSd1QwWkVSR2RZYUN0TVowZFBjM2RvUld4cU9XTXpkbHBJUjBwdWFHcHdkRGh5YTJKcGNpOHlkVXhIWm5oc1ZsbzBTekY0TlVSU1RqQlFWVXhrT1hsUVUyMXFaeXRoYWpFcmRFaDNTVEZ0VVcxYVZsazNjWFpQTlVSbmFFOTRhRXBOUjJ4Nk5teE1hVnB0ZW05blBTSXNJazFKU1VWWVJFTkRRVEJUWjBGM1NVSkJaMGxPUVdWUGNFMUNlamhqWjFrMFVEVndWRWhVUVU1Q1oydHhhR3RwUnpsM01FSkJVWE5HUVVSQ1RVMVRRWGRJWjFsRVZsRlJURVY0WkVoaVJ6bHBXVmQ0VkdGWFpIVkpSa3AyWWpOUloxRXdSV2RNVTBKVFRXcEZWRTFDUlVkQk1WVkZRMmhOUzFJeWVIWlpiVVp6VlRKc2JtSnFSVlJOUWtWSFFURlZSVUY0VFV0U01uaDJXVzFHYzFVeWJHNWlha0ZsUm5jd2VFNTZRVEpOVkZWM1RVUkJkMDVFU21GR2R6QjVUVlJGZVUxVVZYZE5SRUYzVGtSS1lVMUdVWGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlVUVkkwZDBoQldVUldVVkZMUlhoV1NHSXlPVzVpUjFWblZraEtNV016VVdkVk1sWjVaRzFzYWxwWVRYaEtWRUZxUW1kT1ZrSkJUVlJJUldSMllqSmtjMXBUUWtwaWJsSnNZMjAxYkdSRFFrSmtXRkp2WWpOS2NHUklhMmRTZWsxM1oyZEZhVTFCTUVkRFUzRkhVMGxpTTBSUlJVSkJVVlZCUVRSSlFrUjNRWGRuWjBWTFFXOUpRa0ZSUkV0VmEzWnhTSFl2VDBwSGRXOHlia2xaWVU1V1YxaFJOVWxYYVRBeFExaGFZWG8yVkVsSVRFZHdMMnhQU2lzMk1EQXZOR2hpYmpkMmJqWkJRVUl6UkZaNlpGRlBkSE0zUnpWd1NEQnlTbTV1VDBaVlFVczNNVWMwYm5wTFRXWklRMGRWYTNOWEwyMXZibUVyV1RKbGJVcFJNazRyWVdsamQwcExaWFJRUzFKVFNXZEJkVkJQUWpaQllXaG9PRWhpTWxoUE0yZzVVbFZyTWxRd1NFNXZkVUl5Vm5wNGIwMVliR3Q1VnpkWVZWSTFiWGMyU210TVNHNUJOVEpZUkZadlVsUlhhMDUwZVRWdlEwbE9USFpIYlc1U2Mwb3hlbTkxUVhGWlIxWlJUV012TjNONUt5OUZXV2hCVEhKV1NrVkJPRXRpZEhsWUszSTRjMjUzVlRWRE1XaFZjbmRoVnpaTlYwOUJVbUU0Y1VKd1RsRmpWMVJyWVVsbGIxbDJlUzl6UjBsS1JXMXFVakIyUmtWM1NHUndNV05UWVZkSmNqWXZOR2MzTW00M1QzRllkMlpwYm5VM1dsbFhPVGRGWm05UFUxRktaVUY2UVdkTlFrRkJSMnBuWjBWNlRVbEpRa3g2UVU5Q1owNVdTRkU0UWtGbU9FVkNRVTFEUVZsWmQwaFJXVVJXVWpCc1FrSlpkMFpCV1VsTGQxbENRbEZWU0VGM1JVZERRM05IUVZGVlJrSjNUVU5OUWtsSFFURlZaRVYzUlVJdmQxRkpUVUZaUWtGbU9FTkJVVUYzU0ZGWlJGWlNNRTlDUWxsRlJraG1RM1ZHUTJGYU0xb3ljMU16UTJoMFEwUnZTRFp0Wm5Kd1RFMUNPRWRCTVZWa1NYZFJXVTFDWVVGR1NuWnBRakZrYmtoQ04wRmhaMkpsVjJKVFlVeGtMMk5IV1ZsMVRVUlZSME5EYzBkQlVWVkdRbmRGUWtKRGEzZEtla0ZzUW1kbmNrSm5SVVpDVVdOM1FWbFpXbUZJVWpCalJHOTJUREk1YW1NelFYVmpSM1J3VEcxa2RtSXlZM1phTTA1NVRXcEJlVUpuVGxaSVVqaEZTM3BCY0UxRFpXZEtZVUZxYUdsR2IyUklVbmRQYVRoMldUTktjMHh1UW5KaFV6VnVZakk1Ymt3eVpIcGpha2wyV2pOT2VVMXBOV3BqYlhkM1VIZFpSRlpTTUdkQ1JHZDNUbXBCTUVKbldtNW5VWGRDUVdkSmQwdHFRVzlDWjJkeVFtZEZSa0pSWTBOQlVsbGpZVWhTTUdOSVRUWk1lVGwzWVRKcmRWb3lPWFphZVRsNVdsaENkbU15YkRCaU0wbzFUSHBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpCUVU5RFFWRkZRVWhNWlVwc2RWSlVOMkoyY3pJMlozbEJXamh6YnpneGRISlZTVk5rTjA4ME5YTnJSRlZ0UVdkbE1XTnVlR2hITVZBeVkwNXRVM2hpVjNOdmFVTjBNbVYxZURsTVUwUXJVRUZxTWt4SldWSkdTRmN6TVM4MmVHOXBZekZyTkhSaVYxaHJSRU5xYVhJek4zaFVWRTV4VWtGTlVGVjVSbEpYVTJSMmRDdHViRkJ4ZDI1aU9FOWhNa2t2YldGVFNuVnJZM2hFYWs1VFpuQkVhQzlDWkRGc1drNW5aR1F2T0dOTVpITkZNeXQzZVhCMVprbzVkVmhQTVdsUmNHNW9PWHBpZFVaSmQzTkpUMDVIYkRGd00wRTRRMmQ0YTNGSkwxVkJhV2d6U21GSFQzRmpjR05rWVVOSmVtdENZVkk1ZFZsUk1WZzBhekpXWnpWQlVGSk1iM1Y2Vm5rM1lUaEpWbXMyZDNWNU5uQnRLMVEzU0ZRMFRGazRhV0pUTlVaRldteG1RVVpNVTFjNFRuZHpWbm81VTBKTE1sWnhiakZPTUZCSlRXNDFlRUUyVGxwV1l6ZHZPRE0xUkV4QlJuTm9SVmRtUXpkVVNXVXpaejA5SWwxOS5leUp1YjI1alpTSTZJaTlYVG1SVFZXOHpiRUl4ZWt0Mk5UVlpNV1EyY2psR1pXdFJkbE5rZERjNWNHaDRjMmhuTjNsMVIxazlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFNelUyT1Rjd056TTFOVEFzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJako1TXpBNFJ6Y3hMM2RaUmtZMk9XRnVSVzlKT1VGYWNrTmFPREZFV0dSMk1YaEhNMjg0UVZkUlNITTlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5QVW9fT0h0dW96SWUxZGZFNlctSUVlYkZtN0R0Qll6M2NmZ0UzRlB5X3dVQlFCMjUwUE1WWjNVbk1NVjYwb0Q2U3d0ajZtQ0lQYnNYZnBULXFuY184eHlTUURndXZQUmp1b191b1JtUWF4aV9FSEZVaTZrZFV0akhaNkY1bWpYcVF4LWRiaFlONU00dG01WlM2bUxaMkdlbGlVcE1UVG9nU2FQUVZiVnl1Y3g0aGpfT1VDLWVPM1lCRmRldmw0d0pmSy15QVJxaGtmOHN6NEgwa1E5TU9IT2U0N0x2a0h3RnI2MElQSjNaQ3QwSklKYnJWVDZnMHJJal9iSlo3aXFrTDFOWUhrbURvNUhnSkgyTXA3QnYtZlJfMHcydzJyWkIzZk5zVGhxRm9UbUI4RU5XUmJjQ3lWQXBncVdIbFU5bUh5SlJGWVVsbnVDcGRGSEwwUjFaX1FoYXV0aERhdGFYxTLLgNysw8NSRiywHzv-MC3m83EvMP0g7NGcO6W4WJSVRAAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBKg96NvDCk5gmyyLqM0zXE0ZZnSkTarHzUfYU2PHWwdQhjjWLXayf0-jYLazWjpSr-N8DM1Zhls4jfmQCqa50X6UBAgMmIAEhWCAGD72C5VXE3mwMjzc_X0_7wUgIOA6kt2KoDIn1-1PHdSJYIAoZmBXyEJkjvlu251BDb8VoOtIketAD1VvYc3WUJJrZ'
59
+ end
60
+
61
+ it do
62
+ expect do
63
+ subject
64
+ end.not_to raise_error
65
+ end
66
+
67
+ context 'when client_data_json is invalid' do
68
+ let(:client_data_json) do
69
+ Base64.urlsafe_encode64({
70
+ type: "webauthn.create",
71
+ challenge: "cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy",
72
+ origin: "https://web-authn.self-issued.app",
73
+ androidPackageName: "com.chrome.canary.malformed"
74
+ }.to_json, padding: false)
75
+ end
76
+
77
+ it do
78
+ expect do
79
+ subject
80
+ end.to raise_error WebAuthn::InvalidAttestation, 'Invalid Android Safetynet Response: nonce'
81
+ end
82
+ end
83
+ end
46
84
  end
47
85
  end
data/web_authn.gemspec CHANGED
@@ -15,6 +15,7 @@ Gem::Specification.new do |gem|
15
15
  gem.add_runtime_dependency 'activesupport'
16
16
  gem.add_runtime_dependency 'cbor'
17
17
  gem.add_runtime_dependency 'cose-key'
18
+ gem.add_runtime_dependency 'json-jwt'
18
19
  gem.add_development_dependency 'rake', '~> 10.0'
19
20
  gem.add_development_dependency 'simplecov'
20
21
  gem.add_development_dependency 'rspec'
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.2.2
4
+ version: 0.3.0
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-09 00:00:00.000000000 Z
11
+ date: 2018-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json-jwt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -128,6 +142,8 @@ files:
128
142
  - bin/setup
129
143
  - lib/web_authn.rb
130
144
  - lib/web_authn/attestation_object.rb
145
+ - lib/web_authn/attestation_statement.rb
146
+ - lib/web_authn/attestation_statement/android_safetynet.rb
131
147
  - lib/web_authn/attested_credential_data.rb
132
148
  - lib/web_authn/authenticator_data.rb
133
149
  - lib/web_authn/authenticator_data/flags.rb
@@ -165,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
181
  version: '0'
166
182
  requirements: []
167
183
  rubyforge_project:
168
- rubygems_version: 2.6.11
184
+ rubygems_version: 2.5.2
169
185
  signing_key:
170
186
  specification_version: 4
171
187
  summary: WebAuthn RP library