web_authn 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +37 -0
- data/VERSION +1 -1
- data/lib/web_authn.rb +3 -0
- data/lib/web_authn/attestation_object.rb +16 -5
- data/lib/web_authn/attestation_statement.rb +6 -0
- data/lib/web_authn/attestation_statement/android_safetynet.rb +81 -0
- data/lib/web_authn/client_data_json.rb +13 -13
- data/lib/web_authn/context/registration.rb +7 -1
- data/spec/context/registration_spec.rb +38 -0
- data/web_authn.gemspec +1 -0
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba07fe60ce73457f0f61ddc7c53dbf2b07886d93
|
4
|
+
data.tar.gz: a9d76ab96805dd91c619551c09e0701a2a7b0795
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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(
|
13
|
-
self.format =
|
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 '
|
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
|
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
|
-
|
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,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(
|
6
|
-
self.type =
|
7
|
-
self.origin =
|
8
|
-
self.challenge =
|
9
|
-
self.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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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.
|
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-
|
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.
|
184
|
+
rubygems_version: 2.5.2
|
169
185
|
signing_key:
|
170
186
|
specification_version: 4
|
171
187
|
summary: WebAuthn RP library
|