webauthn 2.2.0 → 2.5.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 (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