web_authn 0.2.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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