webauthn 2.2.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +36 -0
  3. data/.rubocop.yml +60 -0
  4. data/Appraisals +2 -10
  5. data/CHANGELOG.md +53 -0
  6. data/README.md +71 -9
  7. data/SECURITY.md +6 -3
  8. data/gemfiles/{openssl_2_0.gemfile → openssl_2_2.gemfile} +1 -1
  9. data/lib/cose/rsapkcs1_algorithm.rb +11 -0
  10. data/lib/webauthn/attestation_object.rb +2 -2
  11. data/lib/webauthn/attestation_statement.rb +4 -1
  12. data/lib/webauthn/attestation_statement/android_key.rb +0 -11
  13. data/lib/webauthn/attestation_statement/android_safetynet.rb +1 -5
  14. data/lib/webauthn/attestation_statement/apple.rb +65 -0
  15. data/lib/webauthn/attestation_statement/base.rb +36 -14
  16. data/lib/webauthn/attestation_statement/fido_u2f.rb +2 -5
  17. data/lib/webauthn/attestation_statement/none.rb +7 -1
  18. data/lib/webauthn/attestation_statement/packed.rb +10 -23
  19. data/lib/webauthn/attestation_statement/tpm.rb +10 -20
  20. data/lib/webauthn/authenticator_assertion_response.rb +1 -4
  21. data/lib/webauthn/authenticator_attestation_response.rb +2 -2
  22. data/lib/webauthn/configuration.rb +2 -6
  23. data/lib/webauthn/credential_creation_options.rb +2 -0
  24. data/lib/webauthn/credential_request_options.rb +2 -0
  25. data/lib/webauthn/fake_authenticator.rb +16 -4
  26. data/lib/webauthn/fake_authenticator/attestation_object.rb +7 -3
  27. data/lib/webauthn/fake_client.rb +21 -4
  28. data/lib/webauthn/public_key.rb +21 -2
  29. data/lib/webauthn/public_key_credential.rb +13 -3
  30. data/lib/webauthn/public_key_credential/entity.rb +3 -4
  31. data/lib/webauthn/version.rb +1 -1
  32. data/webauthn.gemspec +7 -6
  33. metadata +34 -22
  34. data/.travis.yml +0 -26
  35. data/gemfiles/cose_head.gemfile +0 -7
  36. data/gemfiles/openssl_head.gemfile +0 -7
  37. data/lib/webauthn/signature_verifier.rb +0 -52
@@ -3,7 +3,6 @@
3
3
  require "android_key_attestation"
4
4
  require "openssl"
5
5
  require "webauthn/attestation_statement/base"
6
- require "webauthn/signature_verifier"
7
6
 
8
7
  module WebAuthn
9
8
  module AttestationStatement
@@ -21,16 +20,6 @@ module WebAuthn
21
20
 
22
21
  private
23
22
 
24
- def valid_signature?(authenticator_data, client_data_hash)
25
- WebAuthn::SignatureVerifier
26
- .new(algorithm, attestation_certificate.public_key)
27
- .verify(signature, authenticator_data.data + client_data_hash)
28
- end
29
-
30
- def matching_public_key?(authenticator_data)
31
- attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
32
- end
33
-
34
23
  def valid_attestation_challenge?(client_data_hash)
35
24
  android_key_attestation.verify_challenge(client_data_hash)
36
25
  rescue AndroidKeyAttestation::ChallengeMismatchError
@@ -16,10 +16,6 @@ module WebAuthn
16
16
  [attestation_type, attestation_trust_path]
17
17
  end
18
18
 
19
- def attestation_certificate
20
- attestation_trust_path.first
21
- end
22
-
23
19
  private
24
20
 
25
21
  def valid_response?(authenticator_data, client_data_hash)
@@ -52,7 +48,7 @@ module WebAuthn
52
48
  end
53
49
 
54
50
  # SafetyNetAttestation returns full chain including root, WebAuthn expects only the x5c certificates
55
- def attestation_trust_path
51
+ def certificates
56
52
  attestation_response.certificate_chain[0..-2]
57
53
  end
58
54
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "webauthn/attestation_statement/base"
5
+
6
+ module WebAuthn
7
+ module AttestationStatement
8
+ class Apple < Base
9
+ # Source: https://www.apple.com/certificateauthority/private/
10
+ ROOT_CERTIFICATE =
11
+ OpenSSL::X509::Certificate.new(<<~PEM)
12
+ -----BEGIN CERTIFICATE-----
13
+ MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
14
+ HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
15
+ bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
16
+ NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
17
+ A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
18
+ AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
19
+ xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
20
+ pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
21
+ 2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
22
+ MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
23
+ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
24
+ 1bWeT0vT
25
+ -----END CERTIFICATE-----
26
+ PEM
27
+
28
+ NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2"
29
+
30
+ def valid?(authenticator_data, client_data_hash)
31
+ valid_nonce?(authenticator_data, client_data_hash) &&
32
+ matching_public_key?(authenticator_data) &&
33
+ trustworthy? &&
34
+ [attestation_type, attestation_trust_path]
35
+ end
36
+
37
+ private
38
+
39
+ def valid_nonce?(authenticator_data, client_data_hash)
40
+ extension = cred_cert&.extensions&.detect { |ext| ext.oid == NONCE_EXTENSION_OID }
41
+
42
+ if extension
43
+ sequence = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(extension.to_der).value[1].value)
44
+
45
+ sequence.tag == OpenSSL::ASN1::SEQUENCE &&
46
+ sequence.value.size == 1 &&
47
+ sequence.value[0].value[0].value ==
48
+ OpenSSL::Digest::SHA256.digest(authenticator_data.data + client_data_hash)
49
+ end
50
+ end
51
+
52
+ def attestation_type
53
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_ANONCA
54
+ end
55
+
56
+ def cred_cert
57
+ attestation_certificate
58
+ end
59
+
60
+ def default_root_certificates
61
+ [ROOT_CERTIFICATE]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,27 +1,31 @@
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
21
  ATTESTATION_TYPES_WITH_ROOT = [
17
22
  ATTESTATION_TYPE_BASIC,
18
23
  ATTESTATION_TYPE_BASIC_OR_ATTCA,
19
- ATTESTATION_TYPE_ATTCA
24
+ ATTESTATION_TYPE_ATTCA,
25
+ ATTESTATION_TYPE_ANONCA
20
26
  ].freeze
21
27
 
22
28
  class Base
23
- class NotSupportedError < Error; end
24
-
25
29
  AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"
26
30
 
27
31
  def initialize(statement)
@@ -40,12 +44,6 @@ module WebAuthn
40
44
  certificates&.first
41
45
  end
42
46
 
43
- def certificate_chain
44
- if certificates
45
- certificates[1..-1]
46
- end
47
- end
48
-
49
47
  def attestation_certificate_key_id
50
48
  raw_subject_key_identifier&.unpack("H*")&.[](0)
51
49
  end
@@ -66,6 +64,10 @@ module WebAuthn
66
64
  end
67
65
  end
68
66
 
67
+ def matching_public_key?(authenticator_data)
68
+ attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
69
+ end
70
+
69
71
  def certificates
70
72
  @certificates ||=
71
73
  raw_certificates&.map do |raw_certificate|
@@ -81,10 +83,6 @@ module WebAuthn
81
83
  statement["x5c"]
82
84
  end
83
85
 
84
- def raw_ecdaa_key_id
85
- statement["ecdaaKeyId"]
86
- end
87
-
88
86
  def signature
89
87
  statement["sig"]
90
88
  end
@@ -152,6 +150,30 @@ module WebAuthn
152
150
  OpenSSL::ASN1.decode(ext_value.value).value
153
151
  end
154
152
 
153
+ def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key)
154
+ raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key)
155
+
156
+ cose_algorithm.verify(
157
+ public_key,
158
+ signature,
159
+ verification_data(authenticator_data, client_data_hash)
160
+ )
161
+ rescue COSE::Error
162
+ false
163
+ end
164
+
165
+ def verification_data(authenticator_data, client_data_hash)
166
+ authenticator_data.data + client_data_hash
167
+ end
168
+
169
+ def cose_algorithm
170
+ @cose_algorithm ||=
171
+ COSE::Algorithm.find(algorithm).tap do |alg|
172
+ alg && configuration.algorithms.include?(alg.name) ||
173
+ raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
174
+ end
175
+ end
176
+
155
177
  def configuration
156
178
  WebAuthn.configuration
157
179
  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
@@ -48,10 +47,8 @@ module WebAuthn
48
47
  attested_credential_data_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID
49
48
  end
50
49
 
51
- def valid_signature?(authenticator_data, client_data_hash)
52
- WebAuthn::SignatureVerifier
53
- .new(VALID_ATTESTATION_CERTIFICATE_ALGORITHM, certificate_public_key)
54
- .verify(signature, verification_data(authenticator_data, client_data_hash))
50
+ def algorithm
51
+ VALID_ATTESTATION_CERTIFICATE_ALGORITHM.id
55
52
  end
56
53
 
57
54
  def verification_data(authenticator_data, client_data_hash)
@@ -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,17 +2,13 @@
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
14
  valid_ec_public_keys?(authenticator_data.credential) &&
@@ -30,19 +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
33
+ algorithm && signature
46
34
  end
47
35
 
48
36
  def valid_ec_public_keys?(credential)
@@ -64,15 +52,6 @@ module WebAuthn
64
52
  end
65
53
  end
66
54
 
67
- def valid_signature?(authenticator_data, client_data_hash)
68
- signature_verifier = WebAuthn::SignatureVerifier.new(
69
- algorithm,
70
- attestation_certificate&.public_key || authenticator_data.credential.public_key_object
71
- )
72
-
73
- signature_verifier.verify(signature, authenticator_data.data + client_data_hash)
74
- end
75
-
76
55
  def attestation_type
77
56
  if attestation_trust_path
78
57
  WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA # FIXME: use metadata if available
@@ -80,6 +59,14 @@ module WebAuthn
80
59
  WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF
81
60
  end
82
61
  end
62
+
63
+ def valid_signature?(authenticator_data, client_data_hash)
64
+ super(
65
+ authenticator_data,
66
+ client_data_hash,
67
+ attestation_certificate&.public_key || authenticator_data.credential.public_key_object
68
+ )
69
+ end
83
70
  end
84
71
  end
85
72
  end
@@ -4,7 +4,6 @@ require "cose/algorithm"
4
4
  require "openssl"
5
5
  require "tpm/key_attestation"
6
6
  require "webauthn/attestation_statement/base"
7
- require "webauthn/signature_verifier"
8
7
 
9
8
  module WebAuthn
10
9
  module AttestationStatement
@@ -19,23 +18,16 @@ module WebAuthn
19
18
  }.freeze
20
19
 
21
20
  def valid?(authenticator_data, client_data_hash)
22
- case attestation_type
23
- when ATTESTATION_TYPE_ATTCA
21
+ attestation_type == ATTESTATION_TYPE_ATTCA &&
24
22
  ver == TPM_V2 &&
25
- valid_key_attestation?(
26
- authenticator_data.data + client_data_hash,
27
- authenticator_data.credential.public_key_object,
28
- authenticator_data.aaguid
29
- ) &&
30
- matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) &&
31
- trustworthy?(aaguid: authenticator_data.aaguid) &&
32
- [attestation_type, attestation_trust_path]
33
- when ATTESTATION_TYPE_ECDAA
34
- raise(
35
- WebAuthn::AttestationStatement::Base::NotSupportedError,
36
- "Attestation type ECDAA is not supported"
37
- )
38
- 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]
39
31
  end
40
32
 
41
33
  private
@@ -78,10 +70,8 @@ module WebAuthn
78
70
  end
79
71
 
80
72
  def attestation_type
81
- if raw_certificates && !raw_ecdaa_key_id
73
+ if raw_certificates
82
74
  ATTESTATION_TYPE_ATTCA
83
- elsif raw_ecdaa_key_id && !raw_certificates
84
- ATTESTATION_TYPE_ECDAA
85
75
  else
86
76
  raise "Attestation type invalid"
87
77
  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
@@ -54,9 +53,7 @@ module WebAuthn
54
53
  attr_reader :authenticator_data_bytes, :signature
55
54
 
56
55
  def valid_signature?(webauthn_public_key)
57
- WebAuthn::SignatureVerifier
58
- .new(webauthn_public_key.alg, webauthn_public_key.pkey)
59
- .verify(signature, authenticator_data_bytes + client_data.hash)
56
+ webauthn_public_key.verify(signature, authenticator_data_bytes + client_data.hash)
60
57
  end
61
58
 
62
59
  def valid_sign_count?(stored_sign_count)
@@ -16,6 +16,8 @@ module WebAuthn
16
16
  class AttestedCredentialVerificationError < VerificationError; end
17
17
 
18
18
  class AuthenticatorAttestationResponse < AuthenticatorResponse
19
+ extend Forwardable
20
+
19
21
  def self.from_client(response)
20
22
  encoder = WebAuthn.configuration.encoder
21
23
 
@@ -48,8 +50,6 @@ module WebAuthn
48
50
  @attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes)
49
51
  end
50
52
 
51
- extend Forwardable
52
-
53
53
  def_delegators(
54
54
  :attestation_object,
55
55
  :aaguid,
@@ -16,11 +16,7 @@ module WebAuthn
16
16
  class RootCertificateFinderNotSupportedError < Error; end
17
17
 
18
18
  class Configuration
19
- def self.if_pss_supported(algorithm)
20
- OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
21
- end
22
-
23
- DEFAULT_ALGORITHMS = ["ES256", if_pss_supported("PS256"), "RS256"].compact.freeze
19
+ DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze
24
20
 
25
21
  attr_accessor :algorithms
26
22
  attr_accessor :encoding
@@ -39,7 +35,7 @@ module WebAuthn
39
35
  @verify_attestation_statement = true
40
36
  @credential_options_timeout = 120000
41
37
  @silent_authentication = false
42
- @acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']
38
+ @acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA', 'AnonCA']
43
39
  @attestation_root_certificates_finders = []
44
40
  end
45
41
 
@@ -32,6 +32,8 @@ module WebAuthn
32
32
  user_display_name: nil,
33
33
  rp_name: nil
34
34
  )
35
+ super()
36
+
35
37
  @attestation = attestation
36
38
  @authenticator_selection = authenticator_selection
37
39
  @exclude_credentials = exclude_credentials