webauthn 1.13.0 → 1.14.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: 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