webauthn 2.1.0 → 3.4.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/build.yml +50 -0
  4. data/.github/workflows/git.yml +21 -0
  5. data/.rubocop.yml +121 -13
  6. data/CHANGELOG.md +169 -0
  7. data/CONTRIBUTING.md +0 -5
  8. data/README.md +80 -14
  9. data/SECURITY.md +7 -4
  10. data/docs/advanced_configuration.md +174 -0
  11. data/docs/u2f_migration.md +14 -20
  12. data/lib/cose/rsapkcs1_algorithm.rb +50 -0
  13. data/lib/webauthn/attestation_object.rb +47 -0
  14. data/lib/webauthn/attestation_statement/android_key.rb +27 -33
  15. data/lib/webauthn/attestation_statement/android_safetynet.rb +27 -11
  16. data/lib/webauthn/attestation_statement/apple.rb +65 -0
  17. data/lib/webauthn/attestation_statement/base.rb +114 -21
  18. data/lib/webauthn/attestation_statement/fido_u2f.rb +8 -6
  19. data/lib/webauthn/attestation_statement/none.rb +7 -1
  20. data/lib/webauthn/attestation_statement/packed.rb +14 -42
  21. data/lib/webauthn/attestation_statement/tpm.rb +38 -75
  22. data/lib/webauthn/attestation_statement.rb +24 -21
  23. data/lib/webauthn/authenticator_assertion_response.rb +22 -11
  24. data/lib/webauthn/authenticator_attestation_response.rb +31 -92
  25. data/lib/webauthn/authenticator_data/attested_credential_data.rb +33 -49
  26. data/lib/webauthn/authenticator_data.rb +59 -51
  27. data/lib/webauthn/authenticator_response.rb +24 -11
  28. data/lib/webauthn/client_data.rb +4 -6
  29. data/lib/webauthn/configuration.rb +38 -40
  30. data/lib/webauthn/credential.rb +4 -4
  31. data/lib/webauthn/credential_creation_options.rb +2 -0
  32. data/lib/webauthn/credential_request_options.rb +2 -0
  33. data/lib/webauthn/encoder.rb +13 -4
  34. data/lib/webauthn/fake_authenticator/attestation_object.rb +25 -4
  35. data/lib/webauthn/fake_authenticator/authenticator_data.rb +25 -10
  36. data/lib/webauthn/fake_authenticator.rb +49 -8
  37. data/lib/webauthn/fake_client.rb +41 -8
  38. data/lib/webauthn/json_serializer.rb +45 -0
  39. data/lib/webauthn/public_key.rb +21 -2
  40. data/lib/webauthn/public_key_credential/creation_options.rb +3 -3
  41. data/lib/webauthn/public_key_credential/entity.rb +5 -28
  42. data/lib/webauthn/public_key_credential/options.rb +11 -32
  43. data/lib/webauthn/public_key_credential/request_options.rb +11 -1
  44. data/lib/webauthn/public_key_credential.rb +52 -8
  45. data/lib/webauthn/public_key_credential_with_assertion.rb +16 -2
  46. data/lib/webauthn/public_key_credential_with_attestation.rb +2 -2
  47. data/lib/webauthn/relying_party.rb +137 -0
  48. data/lib/webauthn/u2f_migrator.rb +8 -4
  49. data/lib/webauthn/version.rb +1 -1
  50. data/lib/webauthn.rb +1 -0
  51. data/webauthn.gemspec +15 -12
  52. metadata +56 -60
  53. data/.travis.yml +0 -36
  54. data/Appraisals +0 -17
  55. data/gemfiles/cose_head.gemfile +0 -7
  56. data/gemfiles/openssl_2_0.gemfile +0 -7
  57. data/gemfiles/openssl_2_1.gemfile +0 -7
  58. data/gemfiles/openssl_head.gemfile +0 -7
  59. data/lib/android_safetynet/attestation_response.rb +0 -116
  60. data/lib/cose/rsassa_algorithm.rb +0 -10
  61. data/lib/tpm/constants.rb +0 -44
  62. data/lib/tpm/s_attest/s_certify_info.rb +0 -14
  63. data/lib/tpm/s_attest.rb +0 -26
  64. data/lib/tpm/sized_buffer.rb +0 -13
  65. data/lib/tpm/t_public/s_ecc_parms.rb +0 -17
  66. data/lib/tpm/t_public/s_rsa_parms.rb +0 -17
  67. data/lib/tpm/t_public.rb +0 -32
  68. data/lib/webauthn/attestation_statement/android_key/authorization_list.rb +0 -39
  69. data/lib/webauthn/attestation_statement/android_key/key_description.rb +0 -37
  70. data/lib/webauthn/attestation_statement/tpm/cert_info.rb +0 -44
  71. data/lib/webauthn/attestation_statement/tpm/pub_area.rb +0 -85
  72. data/lib/webauthn/security_utils.rb +0 -20
  73. data/lib/webauthn/signature_verifier.rb +0 -77
@@ -1,61 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cose/algorithm"
4
+ require "cose/error"
5
+ require "cose/rsapkcs1_algorithm"
3
6
  require "openssl"
4
7
  require "webauthn/authenticator_data/attested_credential_data"
5
8
  require "webauthn/error"
6
9
 
7
10
  module WebAuthn
8
11
  module AttestationStatement
12
+ class UnsupportedAlgorithm < Error; end
13
+
9
14
  ATTESTATION_TYPE_NONE = "None"
10
15
  ATTESTATION_TYPE_BASIC = "Basic"
11
16
  ATTESTATION_TYPE_SELF = "Self"
12
17
  ATTESTATION_TYPE_ATTCA = "AttCA"
13
- ATTESTATION_TYPE_ECDAA = "ECDAA"
14
18
  ATTESTATION_TYPE_BASIC_OR_ATTCA = "Basic_or_AttCA"
19
+ ATTESTATION_TYPE_ANONCA = "AnonCA"
15
20
 
16
- class Base
17
- class NotSupportedError < Error; end
21
+ ATTESTATION_TYPES_WITH_ROOT = [
22
+ ATTESTATION_TYPE_BASIC,
23
+ ATTESTATION_TYPE_BASIC_OR_ATTCA,
24
+ ATTESTATION_TYPE_ATTCA,
25
+ ATTESTATION_TYPE_ANONCA
26
+ ].freeze
18
27
 
28
+ class Base
19
29
  AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
20
30
 
21
- def initialize(statement)
31
+ def initialize(statement, relying_party = WebAuthn.configuration.relying_party)
22
32
  @statement = statement
33
+ @relying_party = relying_party
23
34
  end
24
35
 
25
36
  def valid?(_authenticator_data, _client_data_hash)
26
37
  raise NotImplementedError
27
38
  end
28
39
 
40
+ def format
41
+ WebAuthn::AttestationStatement::FORMAT_TO_CLASS.key(self.class)
42
+ end
43
+
29
44
  def attestation_certificate
30
45
  certificates&.first
31
46
  end
32
47
 
33
- def certificate_chain
34
- if certificates
35
- certificates[1..-1]
36
- end
48
+ def attestation_certificate_key_id
49
+ attestation_certificate.subject_key_identifier&.unpack("H*")&.[](0)
37
50
  end
38
51
 
39
52
  private
40
53
 
41
- attr_reader :statement
54
+ attr_reader :statement, :relying_party
42
55
 
43
56
  def matching_aaguid?(attested_credential_data_aaguid)
44
- extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
57
+ extension = attestation_certificate&.find_extension(AAGUID_EXTENSION_OID)
45
58
  if extension
46
- # `extension.value` mangles data into ASCII, so we must manually compare bytes
47
- # see https://github.com/ruby/openssl/pull/234
48
- extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] ==
49
- attested_credential_data_aaguid
59
+ aaguid_value = OpenSSL::ASN1.decode(extension.value_der).value
60
+ aaguid_value == attested_credential_data_aaguid
50
61
  else
51
62
  true
52
63
  end
53
64
  end
54
65
 
66
+ def matching_public_key?(authenticator_data)
67
+ attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
68
+ end
69
+
55
70
  def certificates
56
- @certificates ||= raw_certificates&.map do |raw_certificate|
57
- OpenSSL::X509::Certificate.new(raw_certificate)
58
- end
71
+ @certificates ||=
72
+ raw_certificates&.map do |raw_certificate|
73
+ OpenSSL::X509::Certificate.new(raw_certificate)
74
+ end
59
75
  end
60
76
 
61
77
  def algorithm
@@ -66,10 +82,6 @@ module WebAuthn
66
82
  statement["x5c"]
67
83
  end
68
84
 
69
- def raw_ecdaa_key_id
70
- statement["ecdaaKeyId"]
71
- end
72
-
73
85
  def signature
74
86
  statement["sig"]
75
87
  end
@@ -79,6 +91,87 @@ module WebAuthn
79
91
  certificates
80
92
  end
81
93
  end
94
+
95
+ def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
96
+ if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
97
+ relying_party.acceptable_attestation_types.include?(attestation_type) &&
98
+ valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id)
99
+ else
100
+ relying_party.acceptable_attestation_types.include?(attestation_type)
101
+ end
102
+ end
103
+
104
+ def valid_certificate_chain?(aaguid: nil, attestation_certificate_key_id: nil)
105
+ root_certificates = root_certificates(
106
+ aaguid: aaguid,
107
+ attestation_certificate_key_id: attestation_certificate_key_id
108
+ )
109
+
110
+ if certificates&.one? && root_certificates.include?(attestation_certificate)
111
+ return true
112
+ end
113
+
114
+ attestation_root_certificates_store(
115
+ aaguid: aaguid,
116
+ attestation_certificate_key_id: attestation_certificate_key_id
117
+ ).verify(attestation_certificate, attestation_trust_path)
118
+ end
119
+
120
+ def attestation_root_certificates_store(aaguid: nil, attestation_certificate_key_id: nil)
121
+ OpenSSL::X509::Store.new.tap do |store|
122
+ root_certificates(
123
+ aaguid: aaguid,
124
+ attestation_certificate_key_id: attestation_certificate_key_id
125
+ ).each do |cert|
126
+ store.add_cert(cert)
127
+ end
128
+ end
129
+ end
130
+
131
+ def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
132
+ root_certificates =
133
+ relying_party.attestation_root_certificates_finders.reduce([]) do |certs, finder|
134
+ if certs.empty?
135
+ finder.find(
136
+ attestation_format: format,
137
+ aaguid: aaguid,
138
+ attestation_certificate_key_id: attestation_certificate_key_id
139
+ ) || []
140
+ else
141
+ certs
142
+ end
143
+ end
144
+
145
+ if root_certificates.empty? && respond_to?(:default_root_certificates, true)
146
+ default_root_certificates
147
+ else
148
+ root_certificates
149
+ end
150
+ end
151
+
152
+ def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
153
+ raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
154
+
155
+ cose_algorithm.verify(
156
+ public_key,
157
+ signature,
158
+ verification_data(authenticator_data, client_data_hash)
159
+ )
160
+ rescue COSE::Error
161
+ false
162
+ end
163
+
164
+ def verification_data(authenticator_data, client_data_hash)
165
+ authenticator_data.data + client_data_hash
166
+ end
167
+
168
+ def cose_algorithm
169
+ @cose_algorithm ||=
170
+ COSE::Algorithm.find(algorithm).tap do |alg|
171
+ alg && relying_party.algorithms.include?(alg.name) ||
172
+ raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
173
+ end
174
+ end
82
175
  end
83
176
  end
84
177
  end
@@ -4,7 +4,6 @@ require "cose"
4
4
  require "openssl"
5
5
  require "webauthn/attestation_statement/base"
6
6
  require "webauthn/attestation_statement/fido_u2f/public_key"
7
- require "webauthn/signature_verifier"
8
7
 
9
8
  module WebAuthn
10
9
  module AttestationStatement
@@ -19,7 +18,8 @@ module WebAuthn
19
18
  valid_credential_public_key?(authenticator_data.credential.public_key) &&
20
19
  valid_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
21
20
  valid_signature?(authenticator_data, client_data_hash) &&
22
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, attestation_trust_path]
21
+ trustworthy?(attestation_certificate_key_id: attestation_certificate_key_id) &&
22
+ [attestation_type, attestation_trust_path]
23
23
  end
24
24
 
25
25
  private
@@ -47,10 +47,8 @@ module WebAuthn
47
47
  attested_credential_data_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
48
48
  end
49
49
 
50
- def valid_signature?(authenticator_data, client_data_hash)
51
- WebAuthn::SignatureVerifier
52
- .new(VALID_ATTESTATION_CERTIFICATE_ALGORITHM, certificate_public_key)
53
- .verify(signature, verification_data(authenticator_data, client_data_hash))
50
+ def algorithm
51
+ VALID_ATTESTATION_CERTIFICATE_ALGORITHM.id
54
52
  end
55
53
 
56
54
  def verification_data(authenticator_data, client_data_hash)
@@ -64,6 +62,10 @@ module WebAuthn
64
62
  def public_key_u2f(cose_key_data)
65
63
  PublicKey.new(cose_key_data)
66
64
  end
65
+
66
+ def attestation_type
67
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA
68
+ end
67
69
  end
68
70
  end
69
71
  end
@@ -6,12 +6,18 @@ module WebAuthn
6
6
  module AttestationStatement
7
7
  class None < Base
8
8
  def valid?(*_args)
9
- if statement == {}
9
+ if statement == {} && trustworthy?
10
10
  [WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE, nil]
11
11
  else
12
12
  false
13
13
  end
14
14
  end
15
+
16
+ private
17
+
18
+ def attestation_type
19
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE
20
+ end
15
21
  end
16
22
  end
17
23
  end
@@ -2,25 +2,21 @@
2
2
 
3
3
  require "openssl"
4
4
  require "webauthn/attestation_statement/base"
5
- require "webauthn/signature_verifier"
6
5
 
7
6
  module WebAuthn
8
7
  # Implements https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation
9
- # ECDAA attestation is unsupported.
10
8
  module AttestationStatement
11
9
  class Packed < Base
12
10
  # Follows "Verification procedure"
13
11
  def valid?(authenticator_data, client_data_hash)
14
- check_unsupported_feature
15
-
16
12
  valid_format? &&
17
13
  valid_algorithm?(authenticator_data.credential) &&
18
- valid_certificate_chain? &&
19
14
  valid_ec_public_keys?(authenticator_data.credential) &&
20
15
  meet_certificate_requirement? &&
21
16
  matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
22
17
  valid_signature?(authenticator_data, client_data_hash) &&
23
- attestation_type_and_trust_path
18
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
19
+ [attestation_type, attestation_trust_path]
24
20
  end
25
21
 
26
22
  private
@@ -30,27 +26,11 @@ module WebAuthn
30
26
  end
31
27
 
32
28
  def self_attestation?
33
- !raw_certificates && !raw_ecdaa_key_id
29
+ !raw_certificates
34
30
  end
35
31
 
36
32
  def valid_format?
37
- algorithm && signature && (
38
- [raw_certificates, raw_ecdaa_key_id].compact.size < 2
39
- )
40
- end
41
-
42
- def check_unsupported_feature
43
- if raw_ecdaa_key_id
44
- raise NotSupportedError, "ecdaaKeyId of the packed attestation format is not implemented yet"
45
- end
46
- end
47
-
48
- def valid_certificate_chain?
49
- if certificate_chain
50
- certificate_chain.all? { |c| certificate_in_use?(c) }
51
- else
52
- true
53
- end
33
+ algorithm && signature
54
34
  end
55
35
 
56
36
  def valid_ec_public_keys?(credential)
@@ -65,35 +45,27 @@ module WebAuthn
65
45
  subject = attestation_certificate.subject.to_a
66
46
 
67
47
  attestation_certificate.version == 2 &&
68
- certificate_in_use?(attestation_certificate) &&
69
48
  subject.assoc('OU')&.at(1) == "Authenticator Attestation" &&
70
- attestation_certificate.extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == 'CA:FALSE'
49
+ attestation_certificate.find_extension('basicConstraints')&.value == 'CA:FALSE'
71
50
  else
72
51
  true
73
52
  end
74
53
  end
75
54
 
76
- def certificate_in_use?(certificate)
77
- now = Time.now
78
-
79
- certificate.not_before < now && now < certificate.not_after
55
+ def attestation_type
56
+ if attestation_trust_path
57
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA # FIXME: use metadata if available
58
+ else
59
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF
60
+ end
80
61
  end
81
62
 
82
63
  def valid_signature?(authenticator_data, client_data_hash)
83
- signature_verifier = WebAuthn::SignatureVerifier.new(
84
- algorithm,
64
+ super(
65
+ authenticator_data,
66
+ client_data_hash,
85
67
  attestation_certificate&.public_key || authenticator_data.credential.public_key_object
86
68
  )
87
-
88
- signature_verifier.verify(signature, authenticator_data.data + client_data_hash)
89
- end
90
-
91
- def attestation_type_and_trust_path
92
- if attestation_trust_path
93
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA, attestation_trust_path]
94
- else
95
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF, nil]
96
- end
97
69
  end
98
70
  end
99
71
  end
@@ -2,98 +2,63 @@
2
2
 
3
3
  require "cose/algorithm"
4
4
  require "openssl"
5
- require "tpm/constants"
5
+ require "tpm/key_attestation"
6
6
  require "webauthn/attestation_statement/base"
7
- require "webauthn/attestation_statement/tpm/cert_info"
8
- require "webauthn/attestation_statement/tpm/pub_area"
9
- require "webauthn/signature_verifier"
10
7
 
11
8
  module WebAuthn
12
9
  module AttestationStatement
13
10
  class TPM < Base
14
- CERTIFICATE_V3 = 2
15
- CERTIFICATE_EMPTY_NAME = OpenSSL::X509::Name.new([]).freeze
16
- CERTIFICATE_SAN_DIRECTORY_NAME = 4
17
- OID_TCG_AT_TPM_MANUFACTURER = "2.23.133.2.1"
18
- OID_TCG_AT_TPM_MODEL = "2.23.133.2.2"
19
- OID_TCG_AT_TPM_VERSION = "2.23.133.2.3"
20
- OID_TCG_KP_AIK_CERTIFICATE = "2.23.133.8.3"
21
11
  TPM_V2 = "2.0"
22
12
 
23
- def valid?(authenticator_data, client_data_hash)
24
- case attestation_type
25
- when ATTESTATION_TYPE_ATTCA
26
- att_to_be_signed = authenticator_data.data + client_data_hash
13
+ COSE_ALG_TO_TPM = {
14
+ "RS1" => { signature: ::TPM::ALG_RSASSA, hash: ::TPM::ALG_SHA1 },
15
+ "RS256" => { signature: ::TPM::ALG_RSASSA, hash: ::TPM::ALG_SHA256 },
16
+ "PS256" => { signature: ::TPM::ALG_RSAPSS, hash: ::TPM::ALG_SHA256 },
17
+ "ES256" => { signature: ::TPM::ALG_ECDSA, hash: ::TPM::ALG_SHA256 },
18
+ }.freeze
27
19
 
20
+ def valid?(authenticator_data, client_data_hash)
21
+ attestation_type == ATTESTATION_TYPE_ATTCA &&
28
22
  ver == TPM_V2 &&
29
- valid_signature? &&
30
- valid_attestation_certificate? &&
31
- pub_area.valid?(authenticator_data.credential.public_key) &&
32
- cert_info.valid?(statement["pubArea"],
33
- OpenSSL::Digest.digest(cose_algorithm.hash_function, att_to_be_signed)) &&
34
- matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
35
- [attestation_type, attestation_trust_path]
36
- when ATTESTATION_TYPE_ECDAA
37
- raise(
38
- WebAuthn::AttestationStatement::Base::NotSupportedError,
39
- "Attestation type ECDAA is not supported"
40
- )
41
- end
23
+ valid_key_attestation?(
24
+ authenticator_data.data + client_data_hash,
25
+ authenticator_data.credential.public_key_object,
26
+ authenticator_data.aaguid
27
+ ) &&
28
+ matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
29
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
30
+ [attestation_type, attestation_trust_path]
42
31
  end
43
32
 
44
33
  private
45
34
 
46
- def valid_signature?
47
- WebAuthn::SignatureVerifier
48
- .new(algorithm, attestation_certificate.public_key)
49
- .verify(signature, verification_data, rsa_pss_salt_length: :auto)
50
- end
51
-
52
- def valid_attestation_certificate?
53
- extensions = attestation_certificate.extensions
54
-
55
- attestation_certificate.version == CERTIFICATE_V3 &&
56
- attestation_certificate.subject.eql?(CERTIFICATE_EMPTY_NAME) &&
57
- valid_subject_alternative_name? &&
58
- certificate_in_use?(attestation_certificate) &&
59
- extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == "CA:FALSE" &&
60
- extensions.find { |ext| ext.oid == "extendedKeyUsage" }&.value == OID_TCG_KP_AIK_CERTIFICATE
61
- end
62
-
63
- def valid_subject_alternative_name?
64
- extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectAltName" }
65
- return unless extension&.critical?
66
-
67
- san_asn1 = OpenSSL::ASN1.decode(extension).find do |val|
68
- val.tag_class == :UNIVERSAL && val.tag == OpenSSL::ASN1::OCTET_STRING
69
- end
70
- directory_name = OpenSSL::ASN1.decode(san_asn1.value).find do |val|
71
- val.tag_class == :CONTEXT_SPECIFIC && val.tag == CERTIFICATE_SAN_DIRECTORY_NAME
72
- end
73
- name = OpenSSL::X509::Name.new(directory_name.value.first).to_a
74
- manufacturer = name.assoc(OID_TCG_AT_TPM_MANUFACTURER).at(1)
75
- model = name.assoc(OID_TCG_AT_TPM_MODEL).at(1)
76
- version = name.assoc(OID_TCG_AT_TPM_VERSION).at(1)
77
-
78
- ::TPM::VENDOR_IDS[manufacturer] && !model.empty? && !version.empty?
79
- end
80
-
81
- def certificate_in_use?(certificate)
82
- now = Time.now
35
+ def valid_key_attestation?(certified_extra_data, key, aaguid)
36
+ key_attestation =
37
+ ::TPM::KeyAttestation.new(
38
+ statement["certInfo"],
39
+ signature,
40
+ statement["pubArea"],
41
+ certificates,
42
+ OpenSSL::Digest.digest(cose_algorithm.hash_function, certified_extra_data),
43
+ signature_algorithm: tpm_algorithm[:signature],
44
+ hash_algorithm: tpm_algorithm[:hash],
45
+ trusted_certificates: root_certificates(aaguid: aaguid)
46
+ )
83
47
 
84
- certificate.not_before < now && now < certificate.not_after
48
+ key_attestation.valid? && key_attestation.key && key_attestation.key.to_pem == key.to_pem
85
49
  end
86
50
 
87
- def verification_data
88
- statement["certInfo"]
51
+ def valid_certificate_chain?(**_)
52
+ # Already performed as part of #valid_key_attestation?
53
+ true
89
54
  end
90
55
 
91
- def cert_info
92
- @cert_info ||= CertInfo.new(statement["certInfo"])
56
+ def default_root_certificates
57
+ ::TPM::KeyAttestation::TRUSTED_CERTIFICATES
93
58
  end
94
59
 
95
- def pub_area
96
- @pub_area ||= PubArea.new(statement["pubArea"])
60
+ def tpm_algorithm
61
+ COSE_ALG_TO_TPM[cose_algorithm.name] || raise("Unsupported algorithm #{cose_algorithm.name}")
97
62
  end
98
63
 
99
64
  def ver
@@ -105,10 +70,8 @@ module WebAuthn
105
70
  end
106
71
 
107
72
  def attestation_type
108
- if raw_certificates && !raw_ecdaa_key_id
73
+ if raw_certificates
109
74
  ATTESTATION_TYPE_ATTCA
110
- elsif raw_ecdaa_key_id && !raw_certificates
111
- ATTESTATION_TYPE_ECDAA
112
75
  else
113
76
  raise "Attestation type invalid"
114
77
  end
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "webauthn/attestation_statement/android_key"
4
+ require "webauthn/attestation_statement/android_safetynet"
5
+ require "webauthn/attestation_statement/apple"
6
+ require "webauthn/attestation_statement/fido_u2f"
7
+ require "webauthn/attestation_statement/none"
8
+ require "webauthn/attestation_statement/packed"
9
+ require "webauthn/attestation_statement/tpm"
3
10
  require "webauthn/error"
4
11
 
5
12
  module WebAuthn
@@ -12,29 +19,25 @@ module WebAuthn
12
19
  ATTESTATION_FORMAT_ANDROID_SAFETYNET = "android-safetynet"
13
20
  ATTESTATION_FORMAT_ANDROID_KEY = "android-key"
14
21
  ATTESTATION_FORMAT_TPM = "tpm"
22
+ ATTESTATION_FORMAT_APPLE = "apple"
15
23
 
16
- def self.from(format, statement)
17
- case format
18
- when ATTESTATION_FORMAT_NONE
19
- require "webauthn/attestation_statement/none"
20
- WebAuthn::AttestationStatement::None.new(statement)
21
- when ATTESTATION_FORMAT_FIDO_U2F
22
- require "webauthn/attestation_statement/fido_u2f"
23
- WebAuthn::AttestationStatement::FidoU2f.new(statement)
24
- when ATTESTATION_FORMAT_PACKED
25
- require "webauthn/attestation_statement/packed"
26
- WebAuthn::AttestationStatement::Packed.new(statement)
27
- when ATTESTATION_FORMAT_ANDROID_SAFETYNET
28
- require "webauthn/attestation_statement/android_safetynet"
29
- WebAuthn::AttestationStatement::AndroidSafetynet.new(statement)
30
- when ATTESTATION_FORMAT_ANDROID_KEY
31
- require "webauthn/attestation_statement/android_key"
32
- WebAuthn::AttestationStatement::AndroidKey.new(statement)
33
- when ATTESTATION_FORMAT_TPM
34
- require "webauthn/attestation_statement/tpm"
35
- WebAuthn::AttestationStatement::TPM.new(statement)
24
+ FORMAT_TO_CLASS = {
25
+ ATTESTATION_FORMAT_NONE => WebAuthn::AttestationStatement::None,
26
+ ATTESTATION_FORMAT_FIDO_U2F => WebAuthn::AttestationStatement::FidoU2f,
27
+ ATTESTATION_FORMAT_PACKED => WebAuthn::AttestationStatement::Packed,
28
+ ATTESTATION_FORMAT_ANDROID_SAFETYNET => WebAuthn::AttestationStatement::AndroidSafetynet,
29
+ ATTESTATION_FORMAT_ANDROID_KEY => WebAuthn::AttestationStatement::AndroidKey,
30
+ ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM,
31
+ ATTESTATION_FORMAT_APPLE => WebAuthn::AttestationStatement::Apple
32
+ }.freeze
33
+
34
+ def self.from(format, statement, relying_party: WebAuthn.configuration.relying_party)
35
+ klass = FORMAT_TO_CLASS[format]
36
+
37
+ if klass
38
+ klass.new(statement, relying_party)
36
39
  else
37
- raise FormatNotSupportedError, "Unsupported attestation format '#{format}'"
40
+ raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
38
41
  end
39
42
  end
40
43
  end
@@ -3,7 +3,6 @@
3
3
  require "webauthn/authenticator_data"
4
4
  require "webauthn/authenticator_response"
5
5
  require "webauthn/encoder"
6
- require "webauthn/signature_verifier"
7
6
  require "webauthn/public_key"
8
7
 
9
8
  module WebAuthn
@@ -11,8 +10,8 @@ module WebAuthn
11
10
  class SignCountVerificationError < VerificationError; end
12
11
 
13
12
  class AuthenticatorAssertionResponse < AuthenticatorResponse
14
- def self.from_client(response)
15
- encoder = WebAuthn.configuration.encoder
13
+ def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
14
+ encoder = relying_party.encoder
16
15
 
17
16
  user_handle =
18
17
  if response["userHandle"]
@@ -23,7 +22,8 @@ module WebAuthn
23
22
  authenticator_data: encoder.decode(response["authenticatorData"]),
24
23
  client_data_json: encoder.decode(response["clientDataJSON"]),
25
24
  signature: encoder.decode(response["signature"]),
26
- user_handle: user_handle
25
+ user_handle: user_handle,
26
+ relying_party: relying_party
27
27
  )
28
28
  end
29
29
 
@@ -37,9 +37,22 @@ module WebAuthn
37
37
  @user_handle = user_handle
38
38
  end
39
39
 
40
- def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil,
41
- rp_id: nil)
42
- super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id)
40
+ def verify(
41
+ expected_challenge,
42
+ expected_origin = nil,
43
+ public_key:,
44
+ sign_count:,
45
+ user_presence: nil,
46
+ user_verification: nil,
47
+ rp_id: nil
48
+ )
49
+ super(
50
+ expected_challenge,
51
+ expected_origin,
52
+ user_presence: user_presence,
53
+ user_verification: user_verification,
54
+ rp_id: rp_id
55
+ )
43
56
  verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key))
44
57
  verify_item(:sign_count, sign_count)
45
58
 
@@ -47,7 +60,7 @@ module WebAuthn
47
60
  end
48
61
 
49
62
  def authenticator_data
50
- @authenticator_data ||= WebAuthn::AuthenticatorData.new(authenticator_data_bytes)
63
+ @authenticator_data ||= WebAuthn::AuthenticatorData.deserialize(authenticator_data_bytes)
51
64
  end
52
65
 
53
66
  private
@@ -55,9 +68,7 @@ module WebAuthn
55
68
  attr_reader :authenticator_data_bytes, :signature
56
69
 
57
70
  def valid_signature?(webauthn_public_key)
58
- WebAuthn::SignatureVerifier
59
- .new(webauthn_public_key.alg, webauthn_public_key.pkey)
60
- .verify(signature, authenticator_data_bytes + client_data.hash)
71
+ webauthn_public_key.verify(signature, authenticator_data_bytes + client_data.hash)
61
72
  end
62
73
 
63
74
  def valid_sign_count?(stored_sign_count)