webauthn 1.13.0 → 1.14.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: f9b0ee13d67d762afc44c92d76d99a7a7f3500c31658a0248a6dfb4d986f3f18
4
- data.tar.gz: 888a0fb232facc8e5d02458bc1c94f926ed4b8541cac4b3937cf7bb4098ed0f4
3
+ metadata.gz: 812396778f9d74667ca7be0273e1ac38c0f0275aae8eb32b35f4d7aa5a52b6ec
4
+ data.tar.gz: 66539bc1f99c17b31f51e91df71b4423b059519eb0d8ccb7e4d637e6db8c588b
5
5
  SHA512:
6
- metadata.gz: a8f7821dbbc3ce730216ade21b37a23d6c202e1e060b90ed1bfb419c2abeed1ff6867d4ba6730de07d7b0f0eef54306358c894cac95faf90cb09af397f60f49e
7
- data.tar.gz: ec06219c2f23352fbeec12c6bdf550b5120403c2dbbc249eb577e1eeb90f16ee1425b8ca857cdcdef884d7e2cd385e1d4e5dfb3cc9173aabc511088cf0627760
6
+ metadata.gz: 875d8b449345498f08caf64f7a9b7cf01d537e85ac188d398ba337b2e13c24a2dd07c8013c201d0b00deb0eb191c8328abfed212e806afee287c91c63c6f8a46
7
+ data.tar.gz: fa8d32da1d05d15d9a74868c343cf2f907f31d9eab4830c1a3b4eead8317440e4ade8695ff19f463d494ee356a02486ffe37b5c52bf5beff3e70a78d4f4ccbd8
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.14.0] - 2019-04-25
4
+
5
+ ### Added
6
+
7
+ - Support 'tpm' attestation statement
8
+ - Support RS256 credential public key
9
+
3
10
  ## [v1.13.0] - 2019-04-09
4
11
 
5
12
  ### Added
@@ -166,6 +173,7 @@ Note: Both additions should help making it compatible with Chrome for Android 70
166
173
  - `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser
167
174
  - Works with ruby 2.5
168
175
 
176
+ [v1.14.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.13.0...v1.14.0/
169
177
  [v1.13.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.12.0...v1.13.0/
170
178
  [v1.12.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.11.0...v1.12.0/
171
179
  [v1.11.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.10.0...v1.11.0/
data/README.md CHANGED
@@ -93,15 +93,15 @@ attestation_response = WebAuthn::AuthenticatorAttestationResponse.new(
93
93
 
94
94
  # This value needs to match `window.location.origin` evaluated by
95
95
  # the User Agent as part of the verification phase.
96
- original_origin = "https://www.example.com"
96
+ expected_origin = "https://www.example.com"
97
97
 
98
- # In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `original_origin` was used on
98
+ # In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `expected_origin` was used on
99
99
  # `navigator.credentials.create`, it needs to specified for verification.
100
100
  # Otherwise, you can ignore passing in this value to the `verify` method below.
101
101
  rp_id = "example.com"
102
102
 
103
103
  begin
104
- attestation_response.verify(original_challenge, original_origin, rp_id: rp_id)
104
+ attestation_response.verify(expected_challenge, expected_origin, rp_id: rp_id)
105
105
 
106
106
  # 1. Register the new user and
107
107
  # 2. Keep Credential ID and Credential Public Key under storage
@@ -160,9 +160,9 @@ assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
160
160
 
161
161
  # This value needs to match `window.location.origin` evaluated by
162
162
  # the User Agent as part of the verification phase.
163
- original_origin = "https://www.example.com"
163
+ expected_origin = "https://www.example.com"
164
164
 
165
- # In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `original_origin` was used on
165
+ # In the case that a Relying Party ID (https://www.w3.org/TR/webauthn/#relying-party-identifier) different from `expected_origin` was used on
166
166
  # `navigator.credentials.get`, it needs to be specified for verification.
167
167
  # Otherwise, you can ignore passing in this value to the `verify` method below.`
168
168
  rp_id = "example.com"
@@ -175,7 +175,7 @@ allowed_credential = {
175
175
  }
176
176
 
177
177
  begin
178
- assertion_response.verify(original_challenge, original_origin, allowed_credentials: [allowed_credential], rp_id: rp_id)
178
+ assertion_response.verify(expected_challenge, expected_origin, allowed_credentials: [allowed_credential], rp_id: rp_id)
179
179
 
180
180
  # Sign in the user
181
181
  rescue WebAuthn::VerificationError => e
@@ -190,7 +190,7 @@ end
190
190
  | packed (self attestation) | Yes |
191
191
  | packed (x5c attestation) | Yes |
192
192
  | packed (ECDAA attestation) | No |
193
- | tpm (x5c attestation) | No |
193
+ | tpm (x5c attestation) | Yes |
194
194
  | tpm (ECDAA attestation) | No |
195
195
  | android-key | Yes |
196
196
  | android-safetynet | Yes |
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+
5
+ module AndroidSafetynet
6
+ # Decoupled from WebAuthn, candidate for extraction
7
+ # Reference: https://developer.android.com/training/safetynet/attestation.html
8
+ class AttestationResponse
9
+ class VerificationError < StandardError; end
10
+ class LeafCertificateSubjectError < VerificationError; end
11
+ class NonceMismatchError < VerificationError; end
12
+ class SignatureError < VerificationError; end
13
+ class ResponseMissingError < VerificationError; end
14
+
15
+ CERTIRICATE_CHAIN_HEADER = "x5c"
16
+ VALID_SUBJECT_HOSTNAME = "attest.android.com"
17
+ HEADERS_POSITION = 1
18
+ PAYLOAD_POSITION = 0
19
+
20
+ attr_reader :response
21
+
22
+ def initialize(response)
23
+ @response = response
24
+ end
25
+
26
+ def verify(nonce)
27
+ if response
28
+ valid_nonce?(nonce) || raise(NonceMismatchError)
29
+ valid_attestation_domain? || raise(LeafCertificateSubjectError)
30
+ valid_signature? || raise(SignatureError)
31
+ else
32
+ raise(ResponseMissingError)
33
+ end
34
+ end
35
+
36
+ def cts_profile_match?
37
+ payload["ctsProfileMatch"]
38
+ end
39
+
40
+ def certificate_chain
41
+ @certificate_chain ||= headers[CERTIRICATE_CHAIN_HEADER].map do |cert|
42
+ OpenSSL::X509::Certificate.new(Base64.strict_decode64(cert))
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def valid_nonce?(nonce)
49
+ payload["nonce"] == nonce
50
+ end
51
+
52
+ def valid_attestation_domain?
53
+ common_name = leaf_certificate&.subject&.to_a&.assoc('CN')
54
+
55
+ if common_name
56
+ common_name[1] == VALID_SUBJECT_HOSTNAME
57
+ end
58
+ end
59
+
60
+ def valid_signature?
61
+ JWT.decode(response, leaf_certificate.public_key, true, algorithms: algorithm_for(leaf_certificate.public_key))
62
+ rescue JWT::VerificationError
63
+ false
64
+ end
65
+
66
+ def algorithm_for(public_key)
67
+ case public_key
68
+ when OpenSSL::PKey::RSA
69
+ "RS256"
70
+ when OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point
71
+ "ES256"
72
+ else
73
+ raise "Unsupported algorithm"
74
+ end
75
+ end
76
+
77
+ def leaf_certificate
78
+ certificate_chain[0]
79
+ end
80
+
81
+ def headers
82
+ jws_parts[HEADERS_POSITION]
83
+ end
84
+
85
+ def payload
86
+ jws_parts[PAYLOAD_POSITION]
87
+ end
88
+
89
+ def jws_parts
90
+ @jws_parts ||= JWT.decode(response, nil, false)
91
+ end
92
+ end
93
+ end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cose/key"
4
+
3
5
  # TODO: Move this to cose gem
4
6
  module COSE
5
7
  # https://tools.ietf.org/html/rfc8152#section-8.1
6
- Algorithm = Struct.new(:id, :name, :hash, :key_curve) do
8
+ Algorithm = Struct.new(:id, :name, :hash, :kty, :key_curve) do
7
9
  @registered = {}
8
10
 
9
- def self.register(id, name, hash, key_curve)
10
- @registered[id] = COSE::Algorithm.new(id, name, hash, key_curve)
11
+ def self.register(id, name, hash, kty, key_curve = nil)
12
+ @registered[id] = COSE::Algorithm.new(id, name, hash, kty, key_curve)
11
13
  end
12
14
 
13
15
  def self.find(id)
@@ -24,4 +26,5 @@ module COSE
24
26
  end
25
27
  end
26
28
 
27
- COSE::Algorithm.register(-7, "ES256", "SHA256", "prime256v1")
29
+ COSE::Algorithm.register(-7, "ES256", "SHA256", COSE::Key::EC2::KTY_EC2, "prime256v1")
30
+ COSE::Algorithm.register(-257, "RS256", "SHA256", COSE::Key::RSA::KTY_RSA)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TPM
4
+ # Section 6 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
5
+
6
+ GENERATED_VALUE = 0xFF544347
7
+
8
+ ST_ATTEST_CERTIFY = 0x8017
9
+
10
+ # Algorithms
11
+ ALG_RSA = 0x0001
12
+ ALG_SHA1 = 0x0004
13
+ ALG_SHA256 = 0x000B
14
+ ALG_NULL = 0x0010
15
+ ALG_RSASSA = 0x0014
16
+ ALG_ECDSA = 0x0018
17
+ ALG_ECC = 0x0023
18
+
19
+ # ECC curves
20
+ ECC_NIST_P256 = 0x0003
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "tpm/constants"
5
+ require "tpm/sized_buffer"
6
+ require "tpm/s_attest/s_certify_info"
7
+
8
+ module TPM
9
+ # Section 10.12.8 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
10
+ class SAttest < BinData::Record
11
+ endian :big
12
+
13
+ uint32 :magic
14
+ uint16 :attested_type
15
+ sized_buffer :qualified_signer
16
+ sized_buffer :extra_data
17
+
18
+ # s_clock_info :clock_info
19
+ # uint64 :firmware_version
20
+ skip length: 25
21
+
22
+ choice :attested, selection: :attested_type do
23
+ s_certify_info TPM::ST_ATTEST_CERTIFY
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "tpm/sized_buffer"
5
+
6
+ module TPM
7
+ class SAttest < BinData::Record
8
+ # Section 10.12.3 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
9
+ class SCertifyInfo < BinData::Record
10
+ sized_buffer :name
11
+ sized_buffer :qualified_name
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module TPM
6
+ # Section 10.4 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
7
+ class SizedBuffer < BinData::Record
8
+ endian :big
9
+
10
+ uint16 :buffer_size, value: lambda { buffer.size }
11
+ string :buffer, read_length: :buffer_size
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "tpm/constants"
5
+ require "tpm/sized_buffer"
6
+ require "tpm/t_public/s_ecc_parms"
7
+ require "tpm/t_public/s_rsa_parms"
8
+
9
+ module TPM
10
+ # Section 12.2.4 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
11
+ class TPublic < BinData::Record
12
+ endian :big
13
+
14
+ uint16 :alg_type
15
+ uint16 :name_alg
16
+
17
+ # :object_attributes
18
+ skip length: 4
19
+
20
+ sized_buffer :auth_policy
21
+
22
+ choice :parameters, selection: :alg_type do
23
+ s_ecc_parms TPM::ALG_ECC
24
+ s_rsa_parms TPM::ALG_RSA
25
+ end
26
+
27
+ choice :unique, selection: :alg_type do
28
+ sized_buffer TPM::ALG_ECC
29
+ sized_buffer TPM::ALG_RSA
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module TPM
6
+ class TPublic < BinData::Record
7
+ # Section 12.2.3.6 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
8
+ class SEccParms < BinData::Record
9
+ endian :big
10
+
11
+ uint16 :symmetric
12
+ uint16 :scheme
13
+ uint16 :curve_id
14
+ uint16 :kdf
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module TPM
6
+ class TPublic < BinData::Record
7
+ # Section 12.2.3.5 in https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
8
+ class SRsaParms < BinData::Record
9
+ endian :big
10
+
11
+ uint16 :symmetric
12
+ uint16 :scheme
13
+ uint16 :key_bits
14
+ uint32 :exponent
15
+ end
16
+ end
17
+ end
@@ -11,7 +11,12 @@ require "securerandom"
11
11
  require "json"
12
12
 
13
13
  module WebAuthn
14
- CRED_PARAM_ES256 = { type: "public-key", alg: COSE::Algorithm.by_name("ES256").id }.freeze
14
+ DEFAULT_ALGORITHMS = ["ES256", "RS256"].freeze
15
+
16
+ DEFAULT_PUB_KEY_CRED_PARAMS = DEFAULT_ALGORITHMS.map do |alg_name|
17
+ { type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id }
18
+ end.freeze
19
+
15
20
  TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
16
21
 
17
22
  # TODO: make keyword arguments mandatory in next major version
@@ -23,7 +28,7 @@ module WebAuthn
23
28
  )
24
29
  {
25
30
  challenge: challenge,
26
- pubKeyCredParams: [CRED_PARAM_ES256],
31
+ pubKeyCredParams: DEFAULT_PUB_KEY_CRED_PARAMS,
27
32
  rp: { name: rp_name },
28
33
  user: { name: user_name, displayName: display_name, id: user_id }
29
34
  }
@@ -11,6 +11,7 @@ module WebAuthn
11
11
  ATTESTATION_FORMAT_PACKED = 'packed'
12
12
  ATTESTATION_FORMAT_ANDROID_SAFETYNET = "android-safetynet"
13
13
  ATTESTATION_FORMAT_ANDROID_KEY = "android-key"
14
+ ATTESTATION_FORMAT_TPM = "tpm"
14
15
 
15
16
  ATTESTATION_TYPE_NONE = "None"
16
17
  ATTESTATION_TYPE_BASIC = "Basic"
@@ -36,6 +37,9 @@ module WebAuthn
36
37
  when ATTESTATION_FORMAT_ANDROID_KEY
37
38
  require "webauthn/attestation_statement/android_key"
38
39
  WebAuthn::AttestationStatement::AndroidKey.new(statement)
40
+ when ATTESTATION_FORMAT_TPM
41
+ require "webauthn/attestation_statement/tpm"
42
+ WebAuthn::AttestationStatement::TPM.new(statement)
39
43
  else
40
44
  raise FormatNotSupportedError, "Unsupported attestation format '#{format}'"
41
45
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cose/algorithm"
4
3
  require "openssl"
5
4
  require "webauthn/attestation_statement/android_key/key_description"
6
5
  require "webauthn/attestation_statement/base"
6
+ require "webauthn/signature_verifier"
7
7
 
8
8
  module WebAuthn
9
9
  module AttestationStatement
@@ -27,17 +27,9 @@ module WebAuthn
27
27
  private
28
28
 
29
29
  def valid_signature?(authenticator_data, client_data_hash)
30
- cose_algorithm = COSE::Algorithm.find(algorithm)
31
-
32
- if cose_algorithm
33
- attestation_certificate.public_key.verify(
34
- cose_algorithm.hash,
35
- signature,
36
- authenticator_data.data + client_data_hash
37
- )
38
- else
39
- raise "Unsupported algorithm #{algorithm}"
40
- end
30
+ WebAuthn::SignatureVerifier
31
+ .new(algorithm, attestation_certificate.public_key)
32
+ .verify(signature, authenticator_data.data + client_data_hash)
41
33
  end
42
34
 
43
35
  def matching_public_key?(authenticator_data)
@@ -76,28 +68,6 @@ module WebAuthn
76
68
  KeyDescription.new(OpenSSL::ASN1.decode(raw_key_description.value).value)
77
69
  end
78
70
  end
79
-
80
- def attestation_certificate
81
- attestation_certificate_chain[0]
82
- end
83
-
84
- def attestation_certificate_chain
85
- @attestation_certificate_chain ||= raw_attestation_certificates.map do |cert|
86
- OpenSSL::X509::Certificate.new(cert)
87
- end
88
- end
89
-
90
- def raw_attestation_certificates
91
- statement["x5c"]
92
- end
93
-
94
- def signature
95
- statement["sig"]
96
- end
97
-
98
- def algorithm
99
- statement["alg"]
100
- end
101
71
  end
102
72
  end
103
73
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "jwt"
3
+ require "android_safetynet/attestation_response"
4
4
  require "openssl"
5
5
  require "webauthn/attestation_statement/base"
6
6
 
@@ -14,10 +14,8 @@ module WebAuthn
14
14
 
15
15
  def valid?(authenticator_data, client_data_hash, trust_store: self.class.default_trust_store)
16
16
  trusted_attestation_certificate?(trust_store) &&
17
- valid_signature? &&
18
- valid_attestation_domain? &&
17
+ valid_response?(authenticator_data, client_data_hash) &&
19
18
  valid_version? &&
20
- valid_nonce?(authenticator_data, client_data_hash) &&
21
19
  cts_profile_match? &&
22
20
  [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_certificate]
23
21
  end
@@ -31,15 +29,14 @@ module WebAuthn
31
29
  trust_store.verify(attestation_certificate)
32
30
  end
33
31
 
34
- def valid_signature?
35
- signed_payload, _, base64_signature = statement["response"].rpartition(".")
36
- signature = Base64.urlsafe_decode64(base64_signature)
37
- attestation_certificate.public_key.verify(OpenSSL::Digest::SHA256.new, signature, signed_payload)
38
- end
32
+ def valid_response?(authenticator_data, client_data_hash)
33
+ nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
39
34
 
40
- def valid_attestation_domain?
41
- subject = attestation_certificate.subject.to_a
42
- subject.assoc('CN')[1] == "attest.android.com"
35
+ begin
36
+ attestation_response.verify(nonce)
37
+ rescue ::AndroidSafetynet::AttestationResponse::VerificationError
38
+ false
39
+ end
43
40
  end
44
41
 
45
42
  # TODO: improve once the spec has clarifications https://github.com/w3c/webauthn/issues/968
@@ -47,35 +44,20 @@ module WebAuthn
47
44
  !statement["ver"].empty?
48
45
  end
49
46
 
50
- def valid_nonce?(authenticator_data, client_data_hash)
51
- nonce = unverified_jws_result[0]["nonce"]
52
- nonce == verification_data(authenticator_data, client_data_hash)
53
- end
54
-
55
47
  def cts_profile_match?
56
- unverified_jws_result[0]["ctsProfileMatch"]
57
- end
58
-
59
- def verification_data(authenticator_data, client_data_hash)
60
- Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
48
+ attestation_response.cts_profile_match?
61
49
  end
62
50
 
63
51
  def attestation_certificate
64
- attestation_certificate_chain[0]
52
+ attestation_response.certificate_chain[0]
65
53
  end
66
54
 
67
55
  def signing_certificates
68
- attestation_certificate_chain[1..-1]
69
- end
70
-
71
- def attestation_certificate_chain
72
- @attestation_certificate_chain ||= unverified_jws_result[1]["x5c"].map do |cert|
73
- OpenSSL::X509::Certificate.new(Base64.strict_decode64(cert))
74
- end
56
+ attestation_response.certificate_chain[1..-1]
75
57
  end
76
58
 
77
- def unverified_jws_result
78
- @unverified_jws_result ||= JWT.decode(statement["response"], nil, false)
59
+ def attestation_response
60
+ @attestation_response ||= ::AndroidSafetynet::AttestationResponse.new(statement["response"])
79
61
  end
80
62
  end
81
63
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openssl"
3
4
  require "webauthn/error"
4
5
 
5
6
  module WebAuthn
@@ -20,6 +21,44 @@ module WebAuthn
20
21
  private
21
22
 
22
23
  attr_reader :statement
24
+
25
+ def matching_aaguid?(attested_credential_data_aaguid)
26
+ extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
27
+ if extension
28
+ # `extension.value` mangles data into ASCII, so we must manually compare bytes
29
+ # see https://github.com/ruby/openssl/pull/234
30
+ extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
31
+ attested_credential_data_aaguid
32
+ else
33
+ true
34
+ end
35
+ end
36
+
37
+ def attestation_certificate
38
+ attestation_certificate_chain&.first
39
+ end
40
+
41
+ def attestation_certificate_chain
42
+ @attestation_certificate_chain ||= raw_attestation_certificates&.map do |raw_certificate|
43
+ OpenSSL::X509::Certificate.new(raw_certificate)
44
+ end
45
+ end
46
+
47
+ def algorithm
48
+ statement["alg"]
49
+ end
50
+
51
+ def raw_attestation_certificates
52
+ statement["x5c"]
53
+ end
54
+
55
+ def raw_ecdaa_key_id
56
+ statement["ecdaaKeyId"]
57
+ end
58
+
59
+ def signature
60
+ statement["sig"]
61
+ end
23
62
  end
24
63
  end
25
64
  end
@@ -3,6 +3,7 @@
3
3
  require "openssl"
4
4
  require "webauthn/attestation_statement/base"
5
5
  require "webauthn/attestation_statement/fido_u2f/public_key"
6
+ require "webauthn/signature_verifier"
6
7
 
7
8
  module WebAuthn
8
9
  module AttestationStatement
@@ -22,10 +23,6 @@ module WebAuthn
22
23
 
23
24
  private
24
25
 
25
- def signature
26
- statement["sig"]
27
- end
28
-
29
26
  def valid_format?
30
27
  !!(raw_attestation_certificates && signature) &&
31
28
  raw_attestation_certificates.length == VALID_ATTESTATION_CERTIFICATE_COUNT
@@ -45,24 +42,14 @@ module WebAuthn
45
42
  attestation_certificate.public_key
46
43
  end
47
44
 
48
- def attestation_certificate
49
- @attestation_certificate ||= OpenSSL::X509::Certificate.new(raw_attestation_certificates[0])
50
- end
51
-
52
- def raw_attestation_certificates
53
- statement["x5c"]
54
- end
55
-
56
45
  def valid_aaguid?(attested_credential_data_aaguid)
57
46
  attested_credential_data_aaguid == VALID_ATTESTED_AAGUID
58
47
  end
59
48
 
60
49
  def valid_signature?(authenticator_data, client_data_hash)
61
- certificate_public_key.verify(
62
- VALID_ATTESTATION_CERTIFICATE_ALGORITHM.hash,
63
- signature,
64
- verification_data(authenticator_data, client_data_hash)
65
- )
50
+ WebAuthn::SignatureVerifier
51
+ .new(VALID_ATTESTATION_CERTIFICATE_ALGORITHM, certificate_public_key)
52
+ .verify(signature, verification_data(authenticator_data, client_data_hash))
66
53
  end
67
54
 
68
55
  def verification_data(authenticator_data, client_data_hash)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cose/algorithm"
4
3
  require "openssl"
5
4
  require "webauthn/attestation_statement/base"
5
+ require "webauthn/signature_verifier"
6
6
 
7
7
  module WebAuthn
8
8
  # Implements https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation
@@ -16,7 +16,7 @@ module WebAuthn
16
16
  valid_format? &&
17
17
  valid_algorithm?(authenticator_data.credential) &&
18
18
  valid_certificate_chain? &&
19
- valid_public_keys?(authenticator_data.credential) &&
19
+ valid_ec_public_keys?(authenticator_data.credential) &&
20
20
  meet_certificate_requirement? &&
21
21
  matching_aaguid?(authenticator_data.attested_credential_data.aaguid) &&
22
22
  valid_signature?(authenticator_data, client_data_hash) &&
@@ -33,22 +33,6 @@ module WebAuthn
33
33
  !raw_attestation_certificates && !raw_ecdaa_key_id
34
34
  end
35
35
 
36
- def algorithm
37
- statement["alg"]
38
- end
39
-
40
- def signature
41
- statement["sig"]
42
- end
43
-
44
- def raw_attestation_certificates
45
- statement["x5c"]
46
- end
47
-
48
- def raw_ecdaa_key_id
49
- statement["ecdaaKeyId"]
50
- end
51
-
52
36
  def valid_format?
53
37
  algorithm && signature && (
54
38
  [raw_attestation_certificates, raw_ecdaa_key_id].compact.size < 2
@@ -61,16 +45,6 @@ module WebAuthn
61
45
  end
62
46
  end
63
47
 
64
- def attestation_certificate_chain
65
- @attestation_certificate_chain ||= raw_attestation_certificates&.map do |cert|
66
- OpenSSL::X509::Certificate.new(cert)
67
- end
68
- end
69
-
70
- def attestation_certificate
71
- attestation_certificate_chain&.first
72
- end
73
-
74
48
  def valid_certificate_chain?
75
49
  if attestation_certificate_chain
76
50
  attestation_certificate_chain[1..-1].all? { |c| certificate_in_use?(c) }
@@ -79,12 +53,10 @@ module WebAuthn
79
53
  end
80
54
  end
81
55
 
82
- # TODO: Reevaluate this check
83
- def valid_public_keys?(credential)
84
- public_keys = attestation_certificate_chain&.map(&:public_key) || [credential.public_key_object]
85
- public_keys.all? do |public_key|
86
- public_key.is_a?(OpenSSL::PKey::EC) && public_key.check_key
87
- end
56
+ def valid_ec_public_keys?(credential)
57
+ (attestation_certificate_chain&.map(&:public_key) || [credential.public_key_object])
58
+ .select { |pkey| pkey.is_a?(OpenSSL::PKey::EC) }
59
+ .all? { |pkey| pkey.check_key }
88
60
  end
89
61
 
90
62
  # Check https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation-cert-requirements
@@ -101,18 +73,6 @@ module WebAuthn
101
73
  end
102
74
  end
103
75
 
104
- def matching_aaguid?(attested_credential_data_aaguid)
105
- extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
106
- if extension
107
- # `extension.value` mangles data into ASCII, so we must manually compare bytes
108
- # see https://github.com/ruby/openssl/pull/234
109
- extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
110
- attested_credential_data_aaguid
111
- else
112
- true
113
- end
114
- end
115
-
116
76
  def certificate_in_use?(certificate)
117
77
  now = Time.now
118
78
 
@@ -120,21 +80,12 @@ module WebAuthn
120
80
  end
121
81
 
122
82
  def valid_signature?(authenticator_data, client_data_hash)
123
- cose_algorithm = COSE::Algorithm.find(algorithm)
124
-
125
- if cose_algorithm
126
- (attestation_certificate&.public_key || authenticator_data.credential.public_key_object).verify(
127
- cose_algorithm.hash,
128
- signature,
129
- verification_data(authenticator_data, client_data_hash)
130
- )
131
- else
132
- raise "Unsupported algorithm #{algorithm}"
133
- end
134
- end
83
+ signature_verifier = WebAuthn::SignatureVerifier.new(
84
+ algorithm,
85
+ attestation_certificate&.public_key || authenticator_data.credential.public_key_object
86
+ )
135
87
 
136
- def verification_data(authenticator_data, client_data_hash)
137
- authenticator_data.data + client_data_hash
88
+ signature_verifier.verify(signature, authenticator_data.data + client_data_hash)
138
89
  end
139
90
 
140
91
  def attestation_type_and_trust_path
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose/algorithm"
4
+ require "openssl"
5
+ require "webauthn/attestation_statement/base"
6
+ require "webauthn/attestation_statement/tpm/cert_info"
7
+ require "webauthn/attestation_statement/tpm/pub_area"
8
+ require "webauthn/signature_verifier"
9
+
10
+ module WebAuthn
11
+ module AttestationStatement
12
+ class TPM < Base
13
+ CERTIFICATE_V3 = 2
14
+ CERTIFICATE_EMPTY_NAME = OpenSSL::X509::Name.new([]).freeze
15
+ OID_TCG_KP_AIK_CERTIFICATE = "2.23.133.8.3"
16
+ TPM_V2 = "2.0"
17
+
18
+ def valid?(authenticator_data, client_data_hash)
19
+ case attestation_type
20
+ when ATTESTATION_TYPE_ATTCA
21
+ att_to_be_signed = authenticator_data.data + client_data_hash
22
+
23
+ ver == TPM_V2 &&
24
+ valid_signature? &&
25
+ valid_attestation_certificate? &&
26
+ pub_area.valid?(authenticator_data.credential.public_key) &&
27
+ cert_info.valid?(statement["pubArea"], OpenSSL::Digest.digest(cose_algorithm.hash, att_to_be_signed)) &&
28
+ matching_aaguid?(authenticator_data.attested_credential_data.aaguid) &&
29
+ [attestation_type, attestation_trust_path]
30
+ when ATTESTATION_TYPE_ECDAA
31
+ raise(
32
+ WebAuthn::AttestationStatement::Base::NotSupportedError,
33
+ "Attestation type ECDAA is not supported"
34
+ )
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def valid_signature?
41
+ WebAuthn::SignatureVerifier
42
+ .new(algorithm, attestation_certificate.public_key)
43
+ .verify(signature, verification_data)
44
+ end
45
+
46
+ def valid_attestation_certificate?
47
+ extensions = attestation_certificate.extensions
48
+
49
+ attestation_certificate.version == CERTIFICATE_V3 &&
50
+ attestation_certificate.subject.eql?(CERTIFICATE_EMPTY_NAME) &&
51
+ certificate_in_use?(attestation_certificate) &&
52
+ extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == "CA:FALSE" &&
53
+ extensions.find { |ext| ext.oid == "extendedKeyUsage" }&.value == OID_TCG_KP_AIK_CERTIFICATE
54
+ end
55
+
56
+ def certificate_in_use?(certificate)
57
+ now = Time.now
58
+
59
+ certificate.not_before < now && now < certificate.not_after
60
+ end
61
+
62
+ def verification_data
63
+ statement["certInfo"]
64
+ end
65
+
66
+ def cert_info
67
+ @cert_info ||= CertInfo.new(statement["certInfo"])
68
+ end
69
+
70
+ def pub_area
71
+ @pub_area ||= PubArea.new(statement["pubArea"])
72
+ end
73
+
74
+ def ver
75
+ statement["ver"]
76
+ end
77
+
78
+ def cose_algorithm
79
+ @cose_algorithm ||= COSE::Algorithm.find(algorithm)
80
+ end
81
+
82
+ def attestation_type
83
+ if raw_attestation_certificates && !raw_ecdaa_key_id
84
+ ATTESTATION_TYPE_ATTCA
85
+ elsif raw_ecdaa_key_id && !raw_attestation_certificates
86
+ ATTESTATION_TYPE_ECDAA
87
+ else
88
+ raise "Attestation type invalid"
89
+ end
90
+ end
91
+
92
+ def attestation_trust_path
93
+ attestation_certificate_chain
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tpm/constants"
4
+ require "tpm/s_attest"
5
+
6
+ module WebAuthn
7
+ module AttestationStatement
8
+ class TPM < Base
9
+ class CertInfo
10
+ TPM_TO_OPENSSL_HASH_ALG = {
11
+ ::TPM::ALG_SHA1 => "SHA1",
12
+ ::TPM::ALG_SHA256 => "SHA256"
13
+ }.freeze
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ end
18
+
19
+ def valid?(attested_data, extra_data)
20
+ s_attest.magic == ::TPM::GENERATED_VALUE &&
21
+ valid_name?(attested_data) &&
22
+ s_attest.extra_data.buffer == extra_data
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :data
28
+
29
+ def valid_name?(attested_data)
30
+ name_hash_alg = s_attest.attested.name.buffer[0..1].unpack("n")[0]
31
+ name = s_attest.attested.name.buffer[2..-1]
32
+
33
+ name == OpenSSL::Digest.digest(TPM_TO_OPENSSL_HASH_ALG[name_hash_alg], attested_data)
34
+ end
35
+
36
+ def s_attest
37
+ @s_attest ||= ::TPM::SAttest.read(data)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose/key"
4
+ require "tpm/constants"
5
+ require "tpm/t_public"
6
+
7
+ module WebAuthn
8
+ module AttestationStatement
9
+ class TPM < Base
10
+ class PubArea
11
+ BYTE_LENGTH = 8
12
+
13
+ COSE_ECC_TO_TPM_ALG = {
14
+ COSE::Algorithm.by_name("ES256").id => ::TPM::ALG_ECDSA,
15
+ }.freeze
16
+
17
+ COSE_RSA_TO_TPM_ALG = {
18
+ COSE::Algorithm.by_name("RS256").id => ::TPM::ALG_RSASSA
19
+ }.freeze
20
+
21
+ COSE_TO_TPM_CURVE = {
22
+ COSE::Key::EC2::CRV_P256 => ::TPM::ECC_NIST_P256
23
+ }.freeze
24
+
25
+ def initialize(data)
26
+ @data = data
27
+ end
28
+
29
+ def valid?(public_key)
30
+ cose_key = COSE::Key.deserialize(public_key)
31
+
32
+ case cose_key
33
+ when COSE::Key::EC2
34
+ valid_ecc_key?(cose_key)
35
+ when COSE::Key::RSA
36
+ valid_rsa_key?(cose_key)
37
+ else
38
+ raise "Unsupported or unknown TPM key type"
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :data
45
+
46
+ def valid_ecc_key?(cose_key)
47
+ valid_symmetric? &&
48
+ valid_scheme?(COSE_ECC_TO_TPM_ALG[cose_key.alg]) &&
49
+ parameters.curve_id == COSE_TO_TPM_CURVE[cose_key.crv] &&
50
+ unique == cose_key.x + cose_key.y
51
+ end
52
+
53
+ def valid_rsa_key?(cose_key)
54
+ valid_symmetric? &&
55
+ valid_scheme?(COSE_RSA_TO_TPM_ALG[cose_key.alg]) &&
56
+ parameters.key_bits == cose_key.n.size * BYTE_LENGTH &&
57
+ unique == cose_key.n
58
+ end
59
+
60
+ def valid_symmetric?
61
+ parameters.symmetric == ::TPM::ALG_NULL
62
+ end
63
+
64
+ def valid_scheme?(scheme)
65
+ parameters.scheme == ::TPM::ALG_NULL || parameters.scheme == scheme
66
+ end
67
+
68
+ def unique
69
+ t_public.unique.buffer
70
+ end
71
+
72
+ def parameters
73
+ t_public.parameters
74
+ end
75
+
76
+ def t_public
77
+ @t_public = ::TPM::TPublic.read(data)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -4,6 +4,7 @@ require "cose/algorithm"
4
4
  require "cose/key"
5
5
  require "webauthn/attestation_statement/fido_u2f/public_key"
6
6
  require "webauthn/authenticator_response"
7
+ require "webauthn/signature_verifier"
7
8
 
8
9
  module WebAuthn
9
10
  class CredentialVerificationError < VerificationError; end
@@ -18,8 +19,8 @@ module WebAuthn
18
19
  @signature = signature
19
20
  end
20
21
 
21
- def verify(original_challenge, original_origin, allowed_credentials:, rp_id: nil)
22
- super(original_challenge, original_origin, rp_id: rp_id)
22
+ def verify(expected_challenge, expected_origin, allowed_credentials:, rp_id: nil)
23
+ super(expected_challenge, expected_origin, rp_id: rp_id)
23
24
 
24
25
  verify_item(:credential, allowed_credentials)
25
26
  verify_item(:signature, credential_cose_key(allowed_credentials))
@@ -36,17 +37,9 @@ module WebAuthn
36
37
  attr_reader :credential_id, :authenticator_data_bytes, :signature
37
38
 
38
39
  def valid_signature?(credential_cose_key)
39
- cose_algorithm = COSE::Algorithm.find(credential_cose_key.alg)
40
-
41
- if cose_algorithm
42
- credential_cose_key.to_pkey.verify(
43
- cose_algorithm.hash,
44
- signature,
45
- authenticator_data_bytes + client_data.hash
46
- )
47
- else
48
- raise "Unsupported algorithm #{credential_cose_key.alg}"
49
- end
40
+ WebAuthn::SignatureVerifier
41
+ .new(credential_cose_key.alg, credential_cose_key.to_pkey)
42
+ .verify(signature, authenticator_data_bytes + client_data.hash)
50
43
  end
51
44
 
52
45
  def valid_credential?(allowed_credentials)
@@ -21,7 +21,7 @@ module WebAuthn
21
21
  @attestation_object = attestation_object
22
22
  end
23
23
 
24
- def verify(original_challenge, original_origin, rp_id: nil)
24
+ def verify(expected_challenge, expected_origin, rp_id: nil)
25
25
  super
26
26
 
27
27
  verify_item(:attestation_statement)
@@ -18,13 +18,13 @@ module WebAuthn
18
18
  @client_data_json = client_data_json
19
19
  end
20
20
 
21
- def verify(original_challenge, original_origin, rp_id: nil)
21
+ def verify(expected_challenge, expected_origin, rp_id: nil)
22
22
  verify_item(:type)
23
23
  verify_item(:token_binding)
24
- verify_item(:challenge, original_challenge)
25
- verify_item(:origin, original_origin)
24
+ verify_item(:challenge, expected_challenge)
25
+ verify_item(:origin, expected_origin)
26
26
  verify_item(:authenticator_data)
27
- verify_item(:rp_id, rp_id || rp_id_from_origin(original_origin))
27
+ verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
28
28
  verify_item(:user_presence)
29
29
 
30
30
  true
@@ -62,12 +62,12 @@ module WebAuthn
62
62
  client_data.valid_token_binding_format?
63
63
  end
64
64
 
65
- def valid_challenge?(original_challenge)
66
- WebAuthn::SecurityUtils.secure_compare(Base64.urlsafe_decode64(client_data.challenge), original_challenge)
65
+ def valid_challenge?(expected_challenge)
66
+ WebAuthn::SecurityUtils.secure_compare(Base64.urlsafe_decode64(client_data.challenge), expected_challenge)
67
67
  end
68
68
 
69
- def valid_origin?(original_origin)
70
- client_data.origin == original_origin
69
+ def valid_origin?(expected_origin)
70
+ client_data.origin == expected_origin
71
71
  end
72
72
 
73
73
  def valid_rp_id?(rp_id)
@@ -82,8 +82,8 @@ module WebAuthn
82
82
  authenticator_data.user_flagged?
83
83
  end
84
84
 
85
- def rp_id_from_origin(original_origin)
86
- URI.parse(original_origin).host
85
+ def rp_id_from_origin(expected_origin)
86
+ URI.parse(expected_origin).host
87
87
  end
88
88
 
89
89
  def type
@@ -93,16 +93,23 @@ module WebAuthn
93
93
  end
94
94
 
95
95
  def cose_credential_public_key
96
- alg = {
97
- COSE::Key::EC2::CRV_P256 => -7,
98
- COSE::Key::EC2::CRV_P384 => -35,
99
- COSE::Key::EC2::CRV_P521 => -36
100
- }
96
+ case credential[:public_key]
97
+ when OpenSSL::PKey::RSA
98
+ key = COSE::Key::RSA.from_pkey(credential[:public_key])
99
+ # FIXME: Remove once writer in cose
100
+ key.instance_variable_set(:@alg, -257)
101
+ when OpenSSL::PKey::EC::Point
102
+ alg = {
103
+ COSE::Key::EC2::CRV_P256 => -7,
104
+ COSE::Key::EC2::CRV_P384 => -35,
105
+ COSE::Key::EC2::CRV_P521 => -36
106
+ }
107
+
108
+ key = COSE::Key::EC2.from_pkey(credential[:public_key])
109
+ # FIXME: Remove once writer in cose
110
+ key.instance_variable_set(:@alg, alg[key.crv])
101
111
 
102
- key = COSE::Key::EC2.from_pkey(credential[:public_key])
103
-
104
- # FIXME: Remove once writer in cose
105
- key.instance_variable_set(:@alg, alg[key.crv])
112
+ end
106
113
 
107
114
  key.serialize
108
115
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose/algorithm"
4
+
5
+ module WebAuthn
6
+ class SignatureVerifier
7
+ class UnsupportedAlgorithm < Error; end
8
+
9
+ # This logic contained in this map constant is a candidate to be moved to cose gem domain
10
+ KTY_MAP = {
11
+ COSE::Key::EC2::KTY_EC2 => [OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point],
12
+ COSE::Key::RSA::KTY_RSA => [OpenSSL::PKey::RSA]
13
+ }.freeze
14
+
15
+ def initialize(algorithm, public_key)
16
+ @algorithm = algorithm
17
+ @public_key = public_key
18
+
19
+ validate
20
+ end
21
+
22
+ def verify(signature, verification_data)
23
+ public_key.verify(cose_algorithm.hash, signature, verification_data)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :algorithm, :public_key
29
+
30
+ def cose_algorithm
31
+ case algorithm
32
+ when COSE::Algorithm
33
+ algorithm
34
+ else
35
+ COSE::Algorithm.find(algorithm)
36
+ end
37
+ end
38
+
39
+ def validate
40
+ if cose_algorithm
41
+ if !KTY_MAP[cose_algorithm.kty].include?(public_key.class)
42
+ raise("Incompatible algorithm and key")
43
+ end
44
+ else
45
+ raise UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebAuthn
4
- VERSION = "1.13.0"
4
+ VERSION = "1.14.0"
5
5
  end
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.required_ruby_version = ">= 2.3"
32
32
 
33
+ spec.add_dependency "bindata", "~> 2.4"
33
34
  spec.add_dependency "cbor", "~> 0.5.9"
34
35
  spec.add_dependency "cose", "~> 0.6.0"
35
36
  spec.add_dependency "jwt", [">= 1.5", "< 3.0"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webauthn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gonzalo Rodriguez
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-04-09 00:00:00.000000000 Z
12
+ date: 2019-04-25 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bindata
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.4'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.4'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: cbor
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -200,7 +214,15 @@ files:
200
214
  - bin/setup
201
215
  - gemfiles/openssl_2_0.gemfile
202
216
  - gemfiles/openssl_2_1.gemfile
217
+ - lib/android_safetynet/attestation_response.rb
203
218
  - lib/cose/algorithm.rb
219
+ - lib/tpm/constants.rb
220
+ - lib/tpm/s_attest.rb
221
+ - lib/tpm/s_attest/s_certify_info.rb
222
+ - lib/tpm/sized_buffer.rb
223
+ - lib/tpm/t_public.rb
224
+ - lib/tpm/t_public/s_ecc_parms.rb
225
+ - lib/tpm/t_public/s_rsa_parms.rb
204
226
  - lib/webauthn.rb
205
227
  - lib/webauthn/attestation_statement.rb
206
228
  - lib/webauthn/attestation_statement/android_key.rb
@@ -212,6 +234,9 @@ files:
212
234
  - lib/webauthn/attestation_statement/fido_u2f/public_key.rb
213
235
  - lib/webauthn/attestation_statement/none.rb
214
236
  - lib/webauthn/attestation_statement/packed.rb
237
+ - lib/webauthn/attestation_statement/tpm.rb
238
+ - lib/webauthn/attestation_statement/tpm/cert_info.rb
239
+ - lib/webauthn/attestation_statement/tpm/pub_area.rb
215
240
  - lib/webauthn/authenticator_assertion_response.rb
216
241
  - lib/webauthn/authenticator_attestation_response.rb
217
242
  - lib/webauthn/authenticator_data.rb
@@ -224,6 +249,7 @@ files:
224
249
  - lib/webauthn/fake_authenticator/authenticator_data.rb
225
250
  - lib/webauthn/fake_client.rb
226
251
  - lib/webauthn/security_utils.rb
252
+ - lib/webauthn/signature_verifier.rb
227
253
  - lib/webauthn/version.rb
228
254
  - webauthn.gemspec
229
255
  homepage: https://github.com/cedarcode/webauthn-ruby