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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d1fff7e52035a4866627bc77ac38905ba8f702d1da958073824a48a27f53a9e
4
- data.tar.gz: 68c0adc1b2f4a3722a137ac63657868af12c90cd40654ffc9476d757bf02c235
3
+ metadata.gz: 5a95868432d9b0378f32f89fcb8ebc3c64f30b2c881c52ec1e6cc43a1b94c48b
4
+ data.tar.gz: ed86036716a75ea287888670cc0d3e2555e2c18dad65aed820d067019aabafe4
5
5
  SHA512:
6
- metadata.gz: 977f77cf15519cac46231aa2bfe829783a4b773e4709ee550fec8265bfff73ed39e91418143e50b3ec34e37af608b601cc03196eb37a6ac8fbbe51c51f95e318
7
- data.tar.gz: 72277d134728c8efa97a950fac7a3c2b8c8d678bfe7f2f7a8424625a3fa54d6fb426da85da4ff68cbf8d0adf7f97fc67e360475d7996e41c9d0d1002eda4dfbe
6
+ metadata.gz: f47a3020a2a9e54840eedcc38a816ef65503a90798b67cc15c684f02b9f6eeb4b5b7afa8b08ed6f4c629626a4a117cbdff8b8bdaab687c9c3a2d30a8561a98c5
7
+ data.tar.gz: 8b6fd8bbecfb96b24ab1359011d5868b6d1098645f74389221bd9b9861451a41aa279ca9f6b51e00022284db9b13cd25930a5c064291980872b53ecffe65e359
@@ -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.2.1
1
+ 0.5.0
@@ -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(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'
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 attrs[:authData]
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
- new CBOR.decode(
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,8 @@
1
+ module WebAuthn
2
+ class AttestationStatement
3
+ end
4
+ end
5
+
6
+ require 'web_authn/attestation_statement/android_safetynet'
7
+ require 'web_authn/attestation_statement/apple'
8
+ require 'web_authn/attestation_statement/packed'
@@ -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(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,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
@@ -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 -- bin/*`.split("\n").map{ |f| File.basename(f) }
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.2.1
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: 2018-09-09 00:00:00.000000000 Z
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
- rubyforge_project:
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