web_authn 0.2.1 → 0.5.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 +4 -4
- data/Gemfile.lock +71 -0
- data/README.md +37 -0
- data/VERSION +1 -1
- data/lib/web_authn.rb +3 -0
- data/lib/web_authn/attestation_object.rb +20 -5
- data/lib/web_authn/attestation_statement.rb +8 -0
- data/lib/web_authn/attestation_statement/android_safetynet.rb +80 -0
- data/lib/web_authn/attestation_statement/apple.rb +90 -0
- data/lib/web_authn/attestation_statement/packed.rb +71 -0
- data/lib/web_authn/attested_credential_data.rb +4 -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 +92 -0
- data/web_authn.gemspec +3 -2
- metadata +23 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a95868432d9b0378f32f89fcb8ebc3c64f30b2c881c52ec1e6cc43a1b94c48b
|
4
|
+
data.tar.gz: ed86036716a75ea287888670cc0d3e2555e2c18dad65aed820d067019aabafe4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f47a3020a2a9e54840eedcc38a816ef65503a90798b67cc15c684f02b9f6eeb4b5b7afa8b08ed6f4c629626a4a117cbdff8b8bdaab687c9c3a2d30a8561a98c5
|
7
|
+
data.tar.gz: 8b6fd8bbecfb96b24ab1359011d5868b6d1098645f74389221bd9b9861451a41aa279ca9f6b51e00022284db9b13cd25930a5c064291980872b53ecffe65e359
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
web_authn (0.4.1)
|
5
|
+
activesupport
|
6
|
+
cbor
|
7
|
+
cose-key (>= 0.2.0)
|
8
|
+
json-jwt
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
activesupport (5.2.2)
|
14
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
15
|
+
i18n (>= 0.7, < 2)
|
16
|
+
minitest (~> 5.1)
|
17
|
+
tzinfo (~> 1.1)
|
18
|
+
aes_key_wrap (1.0.1)
|
19
|
+
bindata (2.4.4)
|
20
|
+
cbor (0.5.9.3)
|
21
|
+
concurrent-ruby (1.1.4)
|
22
|
+
cose-key (0.2.0)
|
23
|
+
cbor
|
24
|
+
diff-lcs (1.3)
|
25
|
+
docile (1.3.1)
|
26
|
+
i18n (1.5.1)
|
27
|
+
concurrent-ruby (~> 1.0)
|
28
|
+
json (2.1.0)
|
29
|
+
json-jwt (1.10.0)
|
30
|
+
activesupport (>= 4.2)
|
31
|
+
aes_key_wrap
|
32
|
+
bindata
|
33
|
+
minitest (5.11.3)
|
34
|
+
rake (10.5.0)
|
35
|
+
rspec (3.8.0)
|
36
|
+
rspec-core (~> 3.8.0)
|
37
|
+
rspec-expectations (~> 3.8.0)
|
38
|
+
rspec-mocks (~> 3.8.0)
|
39
|
+
rspec-core (3.8.0)
|
40
|
+
rspec-support (~> 3.8.0)
|
41
|
+
rspec-expectations (3.8.2)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.8.0)
|
44
|
+
rspec-its (1.2.0)
|
45
|
+
rspec-core (>= 3.0.0)
|
46
|
+
rspec-expectations (>= 3.0.0)
|
47
|
+
rspec-mocks (3.8.0)
|
48
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
49
|
+
rspec-support (~> 3.8.0)
|
50
|
+
rspec-support (3.8.0)
|
51
|
+
simplecov (0.16.1)
|
52
|
+
docile (~> 1.1)
|
53
|
+
json (>= 1.8, < 3)
|
54
|
+
simplecov-html (~> 0.10.0)
|
55
|
+
simplecov-html (0.10.2)
|
56
|
+
thread_safe (0.3.6)
|
57
|
+
tzinfo (1.2.5)
|
58
|
+
thread_safe (~> 0.1)
|
59
|
+
|
60
|
+
PLATFORMS
|
61
|
+
ruby
|
62
|
+
|
63
|
+
DEPENDENCIES
|
64
|
+
rake (~> 10.0)
|
65
|
+
rspec
|
66
|
+
rspec-its
|
67
|
+
simplecov
|
68
|
+
web_authn!
|
69
|
+
|
70
|
+
BUNDLED WITH
|
71
|
+
1.17.1
|
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.5.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,39 @@ 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'
|
20
|
+
AttestationStatement::Packed.decode att_stmt
|
21
|
+
when 'apple'
|
22
|
+
AttestationStatement::Apple.decode att_stmt
|
23
|
+
when 'tpm', 'android-key', 'fido-u2f'
|
18
24
|
raise NotImplementedError, "Unsupported Attestation Format: #{format}"
|
19
25
|
else
|
20
26
|
raise InvalidContext, 'Unknown Attestation Format'
|
21
27
|
end
|
22
|
-
self.authenticator_data = AuthenticatorData.decode
|
28
|
+
self.authenticator_data = AuthenticatorData.decode auth_data
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify_signature!(client_data_json)
|
32
|
+
attestation_statement.try(:verify!, authenticator_data, client_data_json)
|
23
33
|
end
|
24
34
|
|
25
35
|
class << self
|
26
36
|
def decode(encoded_attestation_object)
|
27
|
-
|
37
|
+
cbor = CBOR.decode(
|
28
38
|
Base64.urlsafe_decode64 encoded_attestation_object
|
29
39
|
).with_indifferent_access
|
40
|
+
new(
|
41
|
+
fmt: cbor[:fmt],
|
42
|
+
att_stmt: cbor[:attStmt],
|
43
|
+
auth_data: cbor[:authData]
|
44
|
+
)
|
30
45
|
end
|
31
46
|
end
|
32
47
|
end
|
@@ -0,0 +1,80 @@
|
|
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
|
+
unless response[:ctsProfileMatch]
|
22
|
+
raise InvalidAttestation, 'Invalid Android Safetynet Response: ctsProfileMatch'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def verify_nonce!(authenticator_data, client_data_json)
|
29
|
+
nonce = Base64.encode64(
|
30
|
+
OpenSSL::Digest::SHA256.digest [
|
31
|
+
authenticator_data.raw,
|
32
|
+
OpenSSL::Digest::SHA256.digest(client_data_json.raw)
|
33
|
+
].join
|
34
|
+
).strip
|
35
|
+
unless response[:nonce] == nonce
|
36
|
+
raise InvalidAttestation, 'Invalid Android Safetynet Response: nonce'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def verify_signature!
|
41
|
+
response.verify! certs.first.public_key
|
42
|
+
rescue JSON::JWS::VerificationFailed => e
|
43
|
+
raise InvalidAttestation, 'Invalid Android Safetynet Response: signature'
|
44
|
+
end
|
45
|
+
|
46
|
+
def verify_certificate!
|
47
|
+
signing_cert = certs.first
|
48
|
+
remaining_chain = certs[1..-1]
|
49
|
+
|
50
|
+
store = OpenSSL::X509::Store.new
|
51
|
+
store.set_default_paths
|
52
|
+
valid_chain = store.verify(signing_cert, remaining_chain)
|
53
|
+
|
54
|
+
valid_subject = signing_cert.subject.to_a.detect do |key, value, type|
|
55
|
+
key == 'CN'
|
56
|
+
end.second == 'attest.android.com'
|
57
|
+
|
58
|
+
valid_timestamp = (
|
59
|
+
signing_cert.not_after > Time.now &&
|
60
|
+
signing_cert.not_before < Time.now
|
61
|
+
)
|
62
|
+
|
63
|
+
# TODO: do we need CRL check?
|
64
|
+
|
65
|
+
unless valid_chain && valid_subject && valid_timestamp
|
66
|
+
raise InvalidAttestation, 'Invalid Android Safetynet Response: certificate'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class << self
|
71
|
+
def decode(att_stmt)
|
72
|
+
new(
|
73
|
+
ver: att_stmt[:ver],
|
74
|
+
response: JSON::JWT.decode(att_stmt[:response], :skip_verification)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module WebAuthn
|
2
|
+
class AttestationStatement
|
3
|
+
class Apple < AttestationStatement
|
4
|
+
CERTIFICATE_EXTENSION_OID = '1.2.840.113635.100.8.2'
|
5
|
+
ROOT_CERTIFICATE = <<~PEM
|
6
|
+
-----BEGIN CERTIFICATE-----
|
7
|
+
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
|
8
|
+
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
|
9
|
+
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
|
10
|
+
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
|
11
|
+
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
|
12
|
+
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
|
13
|
+
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
|
14
|
+
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
|
15
|
+
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
|
16
|
+
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
|
17
|
+
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
18
|
+
1bWeT0vT
|
19
|
+
-----END CERTIFICATE-----
|
20
|
+
PEM
|
21
|
+
|
22
|
+
attr_accessor :alg, :x5c, :certs
|
23
|
+
|
24
|
+
def initialize(alg:, x5c:)
|
25
|
+
self.alg = alg
|
26
|
+
self.x5c = Array(x5c)
|
27
|
+
self.certs = self.x5c.collect do |x5c|
|
28
|
+
OpenSSL::X509::Certificate.new x5c
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify!(authenticator_data, client_data_json)
|
33
|
+
verify_nonce! authenticator_data, client_data_json
|
34
|
+
verify_certificate! authenticator_data.attested_credential_data
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def verify_nonce!(authenticator_data, client_data_json)
|
40
|
+
nonce = OpenSSL::Digest::SHA256.digest [
|
41
|
+
authenticator_data.raw,
|
42
|
+
OpenSSL::Digest::SHA256.digest(client_data_json.raw)
|
43
|
+
].join
|
44
|
+
|
45
|
+
extension = certs.first.extensions.detect { |ext| ext.oid == CERTIFICATE_EXTENSION_OID }
|
46
|
+
expected_nonce = OpenSSL::ASN1.decode(
|
47
|
+
OpenSSL::ASN1.decode(extension.to_der).value.last.value
|
48
|
+
).value.last.value.last.value
|
49
|
+
|
50
|
+
unless expected_nonce == nonce
|
51
|
+
raise InvalidAttestation, 'Invalid Apple Response: nonce'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def verify_certificate!(attested_credential_data)
|
56
|
+
attested_cert = certs.first
|
57
|
+
remaining_chain = certs[1..-1]
|
58
|
+
|
59
|
+
store = OpenSSL::X509::Store.new
|
60
|
+
store.add_cert OpenSSL::X509::Certificate.new ROOT_CERTIFICATE
|
61
|
+
valid_chain = store.verify(attested_cert, remaining_chain)
|
62
|
+
|
63
|
+
valid_timestamp = (
|
64
|
+
attested_cert.not_after > Time.now &&
|
65
|
+
attested_cert.not_before < Time.now
|
66
|
+
)
|
67
|
+
|
68
|
+
valid_attested_public_key = (
|
69
|
+
attested_credential_data.public_key.to_pem ==
|
70
|
+
attested_cert.public_key.to_pem
|
71
|
+
)
|
72
|
+
|
73
|
+
# TODO: do we need CRL check?
|
74
|
+
|
75
|
+
unless valid_chain && valid_attested_public_key && valid_timestamp
|
76
|
+
raise InvalidAttestation, 'Invalid Apple Response: certificate'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class << self
|
81
|
+
def decode(att_stmt)
|
82
|
+
new(
|
83
|
+
alg: att_stmt[:alg],
|
84
|
+
x5c: att_stmt[:x5c]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module WebAuthn
|
2
|
+
class AttestationStatement
|
3
|
+
class Packed < AttestationStatement
|
4
|
+
attr_accessor :alg, :sig, :x5c, :ecdaa_key_id
|
5
|
+
|
6
|
+
def initialize(alg:, sig:, x5c:, ecdaa_key_id:)
|
7
|
+
self.alg = alg
|
8
|
+
self.sig = sig
|
9
|
+
self.x5c = Array(x5c)
|
10
|
+
self.ecdaa_key_id = ecdaa_key_id
|
11
|
+
end
|
12
|
+
|
13
|
+
def verify!(authenticator_data, client_data_json)
|
14
|
+
verify_signature! authenticator_data, client_data_json
|
15
|
+
verify_certificate! unless self_issued?
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def self_issued?
|
21
|
+
[x5c, ecdaa_key_id].all?(&:blank?)
|
22
|
+
end
|
23
|
+
|
24
|
+
def verify_signature!(authenticator_data, client_data_json)
|
25
|
+
signature_base_string = [
|
26
|
+
authenticator_data.raw,
|
27
|
+
OpenSSL::Digest::SHA256.digest(client_data_json.raw)
|
28
|
+
].join
|
29
|
+
|
30
|
+
if self_issued? && authenticator_data.attested_credential_data.anonymous?
|
31
|
+
public_cose_key = authenticator_data.attested_credential_data.public_cose_key
|
32
|
+
unless alg == public_cose_key.alg
|
33
|
+
raise InvalidAttestation, 'Invalid Packed Self Attestation: alg'
|
34
|
+
end
|
35
|
+
unless public_cose_key.verify sig, signature_base_string
|
36
|
+
raise InvalidAttestation, 'Invalid Packed Self Attestation: signature'
|
37
|
+
end
|
38
|
+
else
|
39
|
+
attestation_certificate = OpenSSL::X509::Certificate.new x5c.first
|
40
|
+
public_key = attestation_certificate.public_key
|
41
|
+
digest = case public_key
|
42
|
+
when OpenSSL::PKey::EC
|
43
|
+
COSE::Key::EC2
|
44
|
+
when OpenSSL::PKey::RSA
|
45
|
+
COSE::Key::RSA
|
46
|
+
end.new.tap do |k|
|
47
|
+
k.alg = alg
|
48
|
+
end.digest
|
49
|
+
unless public_key.verify digest, sig, signature_base_string
|
50
|
+
raise InvalidAttestation, 'Invalid Packed Attestation: signature'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def verify_certificate!
|
56
|
+
raise NotImplementedError, 'Certificate Chain Verification Not Implemented Yet: packed'
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
def decode(att_stmt)
|
61
|
+
new(
|
62
|
+
alg: att_stmt[:alg],
|
63
|
+
sig: att_stmt[:sig],
|
64
|
+
x5c: att_stmt[:x5c],
|
65
|
+
ecdaa_key_id: att_stmt[:ecdaaKeyId]
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -9,6 +9,10 @@ module WebAuthn
|
|
9
9
|
self.public_cose_key = public_cose_key
|
10
10
|
end
|
11
11
|
|
12
|
+
def anonymous?
|
13
|
+
aaguid == 'AAAAAAAAAAAAAAAAAAAAAA' # NOTE: equals to `Base64.urlsafe_encode64("\0" * 16, padding: false)``
|
14
|
+
end
|
15
|
+
|
12
16
|
class << self
|
13
17
|
def decode(attested_credential_data)
|
14
18
|
length = (
|
@@ -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,97 @@ 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 packed 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
|
+
'eyJjaGFsbGVuZ2UiOiJjbUZ1Wkc5dExYTjBjbWx1WnkxblpXNWxjbUYwWldRdFlua3RjbkF0YzJWeWRtVnkiLCJvcmlnaW4iOiJodHRwczovL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0'
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when self-attestation' do
|
59
|
+
let(:attestation_object) do
|
60
|
+
'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEYwRAIgOprGUE_GZMIbRBAPLPw6IiNdSk4dxFb4cRbqDgVfFXQCIHnRdm64FfnShyIhq1Z2qfn3ygp0auT32gy-eL35Uo6YaGF1dGhEYXRhWMQyy4DcrMPDUkYssB87_jAt5vNxLzD9IOzRnDuluFiUlUVb2_sTAAAAAAAAAAAAAAAAAAAAAABAAKUVEhUfjXl7S9MbcWXRfXltc39Spl6yuLxOuUtQJ-y-5DkR61Ge8riwY7dRXZFNSaWhsw9LfsknL57eZEB1gKUBAgMmIAEhWCCfkZcOMoafdVwFi4cNNPQlJS1JNUkq34sJ5fKhDODsfyJYIKD89fXxjNhcX6gDxsTwH3VL_TG7HAHdKFgUjAFumfmr'
|
61
|
+
end
|
62
|
+
|
63
|
+
it do
|
64
|
+
expect do
|
65
|
+
subject
|
66
|
+
end.not_to raise_error
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when client_data_json is invalid' do
|
70
|
+
let(:client_data_json) do
|
71
|
+
Base64.urlsafe_encode64({
|
72
|
+
type: "webauthn.create",
|
73
|
+
challenge: "cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy",
|
74
|
+
origin: "https://web-authn.self-issued.app",
|
75
|
+
malformed: 'malformed'
|
76
|
+
}.to_json, padding: false)
|
77
|
+
end
|
78
|
+
|
79
|
+
it do
|
80
|
+
expect do
|
81
|
+
subject
|
82
|
+
end.to raise_error WebAuthn::InvalidAttestation, 'Invalid Packed Self Attestation: signature'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'otherwise' do
|
88
|
+
let(:attestation_object) do
|
89
|
+
'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhANsy4jAv4_BLPmM1pua45Pqo1gfIMA3KDgG-22P0eSH1AiEA3vRxM21j1nKLYyWTdgigzjZHG81IU3JXt2hh0Hr-P_tjeDVjgVkCwjCCAr4wggGmoAMCAQICBHSG_cIwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NTUwMDM4NDIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVXfOt9yR9MXXv_ZzE8xpOh4664YEJVmFQ-ziLLl9lJ79XQJqlgaUNCsUvGERcChNUihNTyKTlmnBOUjvATevto2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBADFcSIDmmlJ-OGaJvWn9CqhvSeueToVFQVVvqtALOgCKHdwB-Wx29mg2GpHiMsgQp5xjB0ybbnpG6x212FxESJ-GinZD0ipchi7APwPlhIvjgH16zVX44a4e4hOsc6tLIOP71SaMsHuHgCcdH0vg5d2sc006WJe9TXO6fzV-ogjJnYpNKQLmCXoAXE3JBNwKGBIOCvfQDPyWmiiG5bGxYfPty8Z3pnjX-1MDnM2hhr40ulMxlSNDnX_ZSnDyMGIbk8TOQmjTF02UO8auP8k3wt5D1rROIRU9-FCSX5WQYi68RuDrGMZB8P5-byoJqbKQdxn2LmE1oZAyohPAmLcoPO5oYXV0aERhdGFYxDLLgNysw8NSRiywHzv-MC3m83EvMP0g7NGcO6W4WJSVQQAAAAv4oBHzjApNFYAGFxEfntx9AEA02xXaLwowZrcHlY4sjukQfJOcMH6ulShKwJM5F4ScjEZHw5pzBzgX2Us_FsAqBP4D3f7rnJ3khIHK7bwY6vtYpQECAyYgASFYIJCdNP17MO609HucLCpQWeeCqIDtipNu2yK0PZHMPh1KIlggRfqxNbCUPKGPZn_NLUdXP2Jsf2ErcCoLEq9O7yEpZvQ'
|
90
|
+
end
|
91
|
+
|
92
|
+
it do
|
93
|
+
expect do
|
94
|
+
subject
|
95
|
+
end.to raise_error WebAuthn::NotImplementedError, 'Certificate Chain Verification Not Implemented Yet: packed'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when android-safetynet attestation given' do
|
101
|
+
let(:context) do
|
102
|
+
{
|
103
|
+
origin: 'https://web-authn.self-issued.app',
|
104
|
+
challenge: 'random-string-generated-by-rp-server'
|
105
|
+
}
|
106
|
+
end
|
107
|
+
let(:client_data_json) do
|
108
|
+
'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRMWE4wY21sdVp5MW5aVzVsY21GMFpXUXRZbmt0Y25BdGMyVnlkbVZ5Iiwib3JpZ2luIjoiaHR0cHM6XC9cL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uY2hyb21lLmNhbmFyeSJ9'
|
109
|
+
end
|
110
|
+
let(:attestation_object) do
|
111
|
+
'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDEyODc0MDQxaHJlc3BvbnNlWRMHZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxGYVdwRFEwRXpTMmRCZDBsQ1FXZEpTVmxyV1c4MVJqQm5PRFpyZDBSUldVcExiMXBKYUhaalRrRlJSVXhDVVVGM1ZrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaElha0ZqUW1kT1ZrSkJiMVJHVldSMllqSmtjMXBUUWxWamJsWjZaRU5DVkZwWVNqSmhWMDVzWTNwRmJFMURUVWRCTVZWRlFYaE5ZMUl5T1haYU1uaHNTVVZzZFdSSFZubGliVll3U1VWR01XUkhhSFpqYld3d1pWTkNTRTE2UVdWR2R6QjRUbnBGZVUxRVVYaE5la1UwVGtST1lVWjNNSGhQUkVWNVRVUk5kMDFFUVhkTlJFSmhUVWQzZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOVWsxM1JWRlpSRlpSVVVsRVFYQkVXVmQ0Y0ZwdE9YbGliV3hvVFZKWmQwWkJXVVJXVVZGSVJFRXhUbUl6Vm5Wa1IwWndZbWxDVjJGWFZqTk5VazEzUlZGWlJGWlJVVXRFUVhCSVlqSTVibUpIVldkVFZ6VnFUVkp6ZDBkUldVUldVVkZFUkVKS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVU5WYWpoM1dXOVFhWGhMWW1KV09ITm5XV2QyVFZSbVdDdGtTWE5HVkU5clowdFBiR2hVTUdrd1ltTkVSbHBMTW5KUGVFcGFNblZUVEZOV2FGbDJhWEJhVGtVelNFcFJXWFYxV1hkR2FtbDVLM2xyWm1GMFFVZFRhbEo2UmpGaU16RjFORE12TjI5SE5XcE5hRE5UTXpkaGJIZHFWV0k0UTFkcFZIaHZhWEJXVDFsM1MwdDZkVlY1YTNGRlEzUnFiR2hLTkVGclYyRkVVeXRhZUV0RmNVOWhaVGwwYmtOblpVaHNiRnBGTDA5U1oyVk5ZWGd5V0U1RGIwZzJjM0pVUlZKamEzTnFlbHBhY2tGWGVFdHpaR1oyVm5KWVRucERVamxFZUZaQlUzVkpOa3g2ZDJnNFJGTnNNa1ZQYjJ0aWMyRnVXaXNyTDBweFRXVkJRa1ptVUhkcWVYZHlZakJ3Y2tWVmVUQndZV1ZXYzNWa0t6QndaV1Y0U3k4MUswVTJhM0JaUjBzMFdrc3libXR2Vmt4MVowVTFkR0ZJY2tGcU9ETlJLMUJQWW1KMlQzcFhZMFpyY0c1V1MzbHFielpMVVVGdFdEWlhTa0ZuVFVKQlFVZHFaMmRHUjAxSlNVSlJha0ZVUW1kT1ZraFRWVVZFUkVGTFFtZG5ja0puUlVaQ1VXTkVRVlJCWkVKblRsWklVa1ZGUm1wQlZXZG9TbWhrU0ZKc1l6TlJkVmxYTld0amJUbHdXa00xYW1JeU1IZGhRVmxKUzNkWlFrSlJWVWhCVVVWRldFUkNZVTFETUVkRFEzTkhRVkZWUmtKNlFVTm9hVVp2WkVoU2QwOXBPSFpqUjNSd1RHMWtkbUl5WTNaYU0wNTVUV2s1U0ZaR1RraFRWVVpJVFhrMWFtTnVVWGRMVVZsSlMzZFpRa0pSVlVoTlFVZEhTRmRvTUdSSVFUWk1lVGwyV1ROT2QweHVRbkpoVXpWdVlqSTVia3d3WkZWVk1HUktVVlZqZWsxQ01FZEJNVlZrUkdkUlYwSkNVVWM0U1hKUmRFWlNOa05WVTJ0cGEySXpZV2x0YzIweU5tTkNWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDT0VkQk1WVmtTWGRSV1UxQ1lVRkdTR1pEZFVaRFlWb3pXakp6VXpORGFIUkRSRzlJTm0xbWNuQk1UVU5GUjBFeFZXUkpRVkZoVFVKbmQwUkJXVXRMZDFsQ1FrRklWMlZSU1VaQmVrRkpRbWRhYm1kUmQwSkJaMGwzVFZGWlJGWlNNR1pDUTI5M1MwUkJiVzlEVTJkSmIxbG5ZVWhTTUdORWIzWk1NazU1WWtNMWQyRXlhM1ZhTWpsMlduazVTRlpHVGtoVFZVWklUWGsxYW1OdGQzZEVVVmxLUzI5YVNXaDJZMDVCVVVWTVFsRkJSR2RuUlVKQlJpOVNlazV1UXpWRWVrSlZRblJ1YURKdWRFcE1WMFZSYURsNlJXVkdXbVpRVERsUmIydHliRUZ2V0dkcVYyZE9PSEJUVWxVeGJGWkhTWEIwZWsxNFIyaDVNeTlQVWxKYVZHRTJSREpFZVRob2RrTkVja1pKTXl0c1Exa3dNVTFNTlZFMldFNUZOVkp6TW1ReFVtbGFjRTF6ZWtRMFMxRmFUa2N6YUZvd1FrWk9VUzlqYW5KRGJVeENUMGRMYTBWVk1XUnRRVmh6UmtwWVNtbFBjakpEVGxSQ1QxUjFPVVZpVEZkb1VXWmtRMFl4WW5kNmVYVXJWelppVVZOMk9GRkVialZQWkUxVEwxQnhSVEZrUldkbGRDODJSVWxTUWpjMk1VdG1XbEVyTDBSRk5reHdNMVJ5V2xSd1QwWkVSR2RZYUN0TVowZFBjM2RvUld4cU9XTXpkbHBJUjBwdWFHcHdkRGh5YTJKcGNpOHlkVXhIWm5oc1ZsbzBTekY0TlVSU1RqQlFWVXhrT1hsUVUyMXFaeXRoYWpFcmRFaDNTVEZ0VVcxYVZsazNjWFpQTlVSbmFFOTRhRXBOUjJ4Nk5teE1hVnB0ZW05blBTSXNJazFKU1VWWVJFTkRRVEJUWjBGM1NVSkJaMGxPUVdWUGNFMUNlamhqWjFrMFVEVndWRWhVUVU1Q1oydHhhR3RwUnpsM01FSkJVWE5HUVVSQ1RVMVRRWGRJWjFsRVZsRlJURVY0WkVoaVJ6bHBXVmQ0VkdGWFpIVkpSa3AyWWpOUloxRXdSV2RNVTBKVFRXcEZWRTFDUlVkQk1WVkZRMmhOUzFJeWVIWlpiVVp6VlRKc2JtSnFSVlJOUWtWSFFURlZSVUY0VFV0U01uaDJXVzFHYzFVeWJHNWlha0ZsUm5jd2VFNTZRVEpOVkZWM1RVUkJkMDVFU21GR2R6QjVUVlJGZVUxVVZYZE5SRUYzVGtSS1lVMUdVWGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlVUVkkwZDBoQldVUldVVkZMUlhoV1NHSXlPVzVpUjFWblZraEtNV016VVdkVk1sWjVaRzFzYWxwWVRYaEtWRUZxUW1kT1ZrSkJUVlJJUldSMllqSmtjMXBUUWtwaWJsSnNZMjAxYkdSRFFrSmtXRkp2WWpOS2NHUklhMmRTZWsxM1oyZEZhVTFCTUVkRFUzRkhVMGxpTTBSUlJVSkJVVlZCUVRSSlFrUjNRWGRuWjBWTFFXOUpRa0ZSUkV0VmEzWnhTSFl2VDBwSGRXOHlia2xaWVU1V1YxaFJOVWxYYVRBeFExaGFZWG8yVkVsSVRFZHdMMnhQU2lzMk1EQXZOR2hpYmpkMmJqWkJRVUl6UkZaNlpGRlBkSE0zUnpWd1NEQnlTbTV1VDBaVlFVczNNVWMwYm5wTFRXWklRMGRWYTNOWEwyMXZibUVyV1RKbGJVcFJNazRyWVdsamQwcExaWFJRUzFKVFNXZEJkVkJQUWpaQllXaG9PRWhpTWxoUE0yZzVVbFZyTWxRd1NFNXZkVUl5Vm5wNGIwMVliR3Q1VnpkWVZWSTFiWGMyU210TVNHNUJOVEpZUkZadlVsUlhhMDUwZVRWdlEwbE9USFpIYlc1U2Mwb3hlbTkxUVhGWlIxWlJUV012TjNONUt5OUZXV2hCVEhKV1NrVkJPRXRpZEhsWUszSTRjMjUzVlRWRE1XaFZjbmRoVnpaTlYwOUJVbUU0Y1VKd1RsRmpWMVJyWVVsbGIxbDJlUzl6UjBsS1JXMXFVakIyUmtWM1NHUndNV05UWVZkSmNqWXZOR2MzTW00M1QzRllkMlpwYm5VM1dsbFhPVGRGWm05UFUxRktaVUY2UVdkTlFrRkJSMnBuWjBWNlRVbEpRa3g2UVU5Q1owNVdTRkU0UWtGbU9FVkNRVTFEUVZsWmQwaFJXVVJXVWpCc1FrSlpkMFpCV1VsTGQxbENRbEZWU0VGM1JVZERRM05IUVZGVlJrSjNUVU5OUWtsSFFURlZaRVYzUlVJdmQxRkpUVUZaUWtGbU9FTkJVVUYzU0ZGWlJGWlNNRTlDUWxsRlJraG1RM1ZHUTJGYU0xb3ljMU16UTJoMFEwUnZTRFp0Wm5Kd1RFMUNPRWRCTVZWa1NYZFJXVTFDWVVGR1NuWnBRakZrYmtoQ04wRmhaMkpsVjJKVFlVeGtMMk5IV1ZsMVRVUlZSME5EYzBkQlVWVkdRbmRGUWtKRGEzZEtla0ZzUW1kbmNrSm5SVVpDVVdOM1FWbFpXbUZJVWpCalJHOTJUREk1YW1NelFYVmpSM1J3VEcxa2RtSXlZM1phTTA1NVRXcEJlVUpuVGxaSVVqaEZTM3BCY0UxRFpXZEtZVUZxYUdsR2IyUklVbmRQYVRoMldUTktjMHh1UW5KaFV6VnVZakk1Ymt3eVpIcGpha2wyV2pOT2VVMXBOV3BqYlhkM1VIZFpSRlpTTUdkQ1JHZDNUbXBCTUVKbldtNW5VWGRDUVdkSmQwdHFRVzlDWjJkeVFtZEZSa0pSWTBOQlVsbGpZVWhTTUdOSVRUWk1lVGwzWVRKcmRWb3lPWFphZVRsNVdsaENkbU15YkRCaU0wbzFUSHBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpCUVU5RFFWRkZRVWhNWlVwc2RWSlVOMkoyY3pJMlozbEJXamh6YnpneGRISlZTVk5rTjA4ME5YTnJSRlZ0UVdkbE1XTnVlR2hITVZBeVkwNXRVM2hpVjNOdmFVTjBNbVYxZURsTVUwUXJVRUZxTWt4SldWSkdTRmN6TVM4MmVHOXBZekZyTkhSaVYxaHJSRU5xYVhJek4zaFVWRTV4VWtGTlVGVjVSbEpYVTJSMmRDdHViRkJ4ZDI1aU9FOWhNa2t2YldGVFNuVnJZM2hFYWs1VFpuQkVhQzlDWkRGc1drNW5aR1F2T0dOTVpITkZNeXQzZVhCMVprbzVkVmhQTVdsUmNHNW9PWHBpZFVaSmQzTkpUMDVIYkRGd00wRTRRMmQ0YTNGSkwxVkJhV2d6U21GSFQzRmpjR05rWVVOSmVtdENZVkk1ZFZsUk1WZzBhekpXWnpWQlVGSk1iM1Y2Vm5rM1lUaEpWbXMyZDNWNU5uQnRLMVEzU0ZRMFRGazRhV0pUTlVaRldteG1RVVpNVTFjNFRuZHpWbm81VTBKTE1sWnhiakZPTUZCSlRXNDFlRUUyVGxwV1l6ZHZPRE0xUkV4QlJuTm9SVmRtUXpkVVNXVXpaejA5SWwxOS5leUp1YjI1alpTSTZJaTlYVG1SVFZXOHpiRUl4ZWt0Mk5UVlpNV1EyY2psR1pXdFJkbE5rZERjNWNHaDRjMmhuTjNsMVIxazlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFNelUyT1Rjd056TTFOVEFzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJako1TXpBNFJ6Y3hMM2RaUmtZMk9XRnVSVzlKT1VGYWNrTmFPREZFV0dSMk1YaEhNMjg0UVZkUlNITTlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5QVW9fT0h0dW96SWUxZGZFNlctSUVlYkZtN0R0Qll6M2NmZ0UzRlB5X3dVQlFCMjUwUE1WWjNVbk1NVjYwb0Q2U3d0ajZtQ0lQYnNYZnBULXFuY184eHlTUURndXZQUmp1b191b1JtUWF4aV9FSEZVaTZrZFV0akhaNkY1bWpYcVF4LWRiaFlONU00dG01WlM2bUxaMkdlbGlVcE1UVG9nU2FQUVZiVnl1Y3g0aGpfT1VDLWVPM1lCRmRldmw0d0pmSy15QVJxaGtmOHN6NEgwa1E5TU9IT2U0N0x2a0h3RnI2MElQSjNaQ3QwSklKYnJWVDZnMHJJal9iSlo3aXFrTDFOWUhrbURvNUhnSkgyTXA3QnYtZlJfMHcydzJyWkIzZk5zVGhxRm9UbUI4RU5XUmJjQ3lWQXBncVdIbFU5bUh5SlJGWVVsbnVDcGRGSEwwUjFaX1FoYXV0aERhdGFYxTLLgNysw8NSRiywHzv-MC3m83EvMP0g7NGcO6W4WJSVRAAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBKg96NvDCk5gmyyLqM0zXE0ZZnSkTarHzUfYU2PHWwdQhjjWLXayf0-jYLazWjpSr-N8DM1Zhls4jfmQCqa50X6UBAgMmIAEhWCAGD72C5VXE3mwMjzc_X0_7wUgIOA6kt2KoDIn1-1PHdSJYIAoZmBXyEJkjvlu251BDb8VoOtIketAD1VvYc3WUJJrZ'
|
112
|
+
end
|
113
|
+
|
114
|
+
# it do
|
115
|
+
# expect do
|
116
|
+
# subject
|
117
|
+
# end.not_to raise_error
|
118
|
+
# end
|
119
|
+
it 'TODO: handle time-dependent certificate verification error (timecop can\'t modify time in openssl world)'
|
120
|
+
|
121
|
+
context 'when client_data_json is invalid' do
|
122
|
+
let(:client_data_json) do
|
123
|
+
Base64.urlsafe_encode64({
|
124
|
+
type: "webauthn.create",
|
125
|
+
challenge: "cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy",
|
126
|
+
origin: "https://web-authn.self-issued.app",
|
127
|
+
androidPackageName: "com.chrome.canary.malformed"
|
128
|
+
}.to_json, padding: false)
|
129
|
+
end
|
130
|
+
|
131
|
+
it do
|
132
|
+
expect do
|
133
|
+
subject
|
134
|
+
end.to raise_error WebAuthn::InvalidAttestation, 'Invalid Android Safetynet Response: nonce'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
46
138
|
end
|
47
139
|
end
|
data/web_authn.gemspec
CHANGED
@@ -9,12 +9,13 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.license = 'MIT'
|
10
10
|
gem.files = `git ls-files`.split("\n")
|
11
11
|
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
12
|
-
gem.executables = `git ls-files --
|
12
|
+
gem.executables = `git ls-files -- exe/*`.split("\n").map{ |f| File.basename(f) }
|
13
13
|
gem.require_paths = ['lib']
|
14
14
|
gem.required_ruby_version = '>= 2.3'
|
15
15
|
gem.add_runtime_dependency 'activesupport'
|
16
16
|
gem.add_runtime_dependency 'cbor'
|
17
|
-
gem.add_runtime_dependency 'cose-key'
|
17
|
+
gem.add_runtime_dependency 'cose-key', '>= 0.2.0'
|
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.5.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:
|
11
|
+
date: 2020-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -40,6 +40,20 @@ dependencies:
|
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: cose-key
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.2.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.2.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: json-jwt
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - ">="
|
@@ -112,9 +126,7 @@ description: W3C Web Authentication API (a.k.a. WebAuthN / FIDO 2.0) RP library
|
|
112
126
|
Ruby
|
113
127
|
email:
|
114
128
|
- nov@matake.jp
|
115
|
-
executables:
|
116
|
-
- console
|
117
|
-
- setup
|
129
|
+
executables: []
|
118
130
|
extensions: []
|
119
131
|
extra_rdoc_files: []
|
120
132
|
files:
|
@@ -122,6 +134,7 @@ files:
|
|
122
134
|
- ".rspec"
|
123
135
|
- ".travis.yml"
|
124
136
|
- Gemfile
|
137
|
+
- Gemfile.lock
|
125
138
|
- LICENSE.txt
|
126
139
|
- README.md
|
127
140
|
- Rakefile
|
@@ -130,6 +143,10 @@ files:
|
|
130
143
|
- bin/setup
|
131
144
|
- lib/web_authn.rb
|
132
145
|
- lib/web_authn/attestation_object.rb
|
146
|
+
- lib/web_authn/attestation_statement.rb
|
147
|
+
- lib/web_authn/attestation_statement/android_safetynet.rb
|
148
|
+
- lib/web_authn/attestation_statement/apple.rb
|
149
|
+
- lib/web_authn/attestation_statement/packed.rb
|
133
150
|
- lib/web_authn/attested_credential_data.rb
|
134
151
|
- lib/web_authn/authenticator_data.rb
|
135
152
|
- lib/web_authn/authenticator_data/flags.rb
|
@@ -166,8 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
183
|
- !ruby/object:Gem::Version
|
167
184
|
version: '0'
|
168
185
|
requirements: []
|
169
|
-
|
170
|
-
rubygems_version: 2.7.6
|
186
|
+
rubygems_version: 3.0.3
|
171
187
|
signing_key:
|
172
188
|
specification_version: 4
|
173
189
|
summary: WebAuthn RP library
|