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
@@ -0,0 +1,174 @@
1
+ # Advanced Configuration
2
+
3
+ ## Global vs Instance Based Configuration
4
+
5
+ Which approach suits best your needs will depend on the architecture of your application and how do your users need to register and authenticate to it.
6
+
7
+ If you have a multi-tenant application, or any application segmenation, where your users register and authenticate to each of these tenants or segments individuallly using different hostnames, or with different security needs, you need to go through [Instance Based Configuration](#instance-based-configuration).
8
+
9
+ However, if your application is served for just one hostname, or else if your users authenticate to only one subdmain (e.g. your application serves www.example.com and admin.example.com but all you users authenticate through auth.example.com) you can still rely on one [Global Configuration](../README.md#configuration).
10
+
11
+ If you are still not sure, or want to keep your options open, be aware that [Instance Based Configuration](#instance-based-configuration) is also a valid way of defining a single instance configuration and how you share such configuration across your application, it's up to you.
12
+
13
+
14
+ ## Instance Based Configuration
15
+
16
+ Intead of the [Global Configuration](../README.md#configuration) you place in `config/initializers/webauthn.rb`,
17
+ you can now have an on-demand instance of `WebAuthn::RelyingParty` with the same configuration options, that
18
+ you can build anywhere in you application, in the following way:
19
+
20
+ ```ruby
21
+ relying_party = WebAuthn::RelyingParty.new(
22
+ # This value needs to match `window.location.origin` evaluated by
23
+ # the User Agent during registration and authentication ceremonies.
24
+ origin: "https://admin.example.com",
25
+
26
+ # Relying Party name for display purposes
27
+ name: "Admin Site for Example Inc."
28
+
29
+ # Optionally configure a client timeout hint, in milliseconds.
30
+ # This hint specifies how long the browser should wait for any
31
+ # interaction with the user.
32
+ # This hint may be overridden by the browser.
33
+ # https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
34
+ # credential_options_timeout: 120_000
35
+
36
+ # You can optionally specify a different Relying Party ID
37
+ # (https://www.w3.org/TR/webauthn/#relying-party-identifier)
38
+ # if it differs from the default one.
39
+ #
40
+ # In this case the default would be "admin.example.com", but you can set it to
41
+ # the suffix "example.com"
42
+ #
43
+ # id: "example.com"
44
+
45
+ # Configure preferred binary-to-text encoding scheme. This should match the encoding scheme
46
+ # used in your client-side (user agent) code before sending the credential to the server.
47
+ # Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding.
48
+ #
49
+ # encoding: :base64url
50
+
51
+ # Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1"
52
+ # Default: ["ES256", "PS256", "RS256"]
53
+ #
54
+ # algorithms: ["ES384"]
55
+ )
56
+ ```
57
+
58
+ ## Instance Based API
59
+
60
+ **DISCLAIMER: This API was released on version 3.0.0.alpha1 and is still under evaluation. Although it has been throughly tested and it is fully functional it might be changed until the final release of version 3.0.0.**
61
+
62
+ The explanation for each ceremony can be found in depth in [Credential Registration](../README.md#credential-registration) and [Credential Authentication](../README.md#credential-authentication) but if you choose this instance based approach to define your WebAuthn configurations and assuming `relying_party` is the result of an instance you get through `WebAuthn::RelyingParty.new(...)` the code in those explanations needs to be updated to:
63
+
64
+ ### Credential Registration
65
+
66
+ #### Initiation phase
67
+
68
+ ```ruby
69
+ # Generate and store the WebAuthn User ID the first time the user registers a credential
70
+ if !user.webauthn_id
71
+ user.update!(webauthn_id: WebAuthn.generate_user_id)
72
+ end
73
+
74
+ options = relying_party.options_for_registration(
75
+ user: { id: user.webauthn_id, name: user.name },
76
+ exclude: user.credentials.map { |c| c.external_id }
77
+ )
78
+
79
+ # Store the newly generated challenge somewhere so you can have it
80
+ # for the verification phase.
81
+ session[:creation_challenge] = options.challenge
82
+
83
+ # Send `options` back to the browser, so that they can be used
84
+ # to call `navigator.credentials.create({ "publicKey": options })`
85
+ #
86
+ # You can call `options.as_json` to get a ruby hash with a JSON representation if needed.
87
+
88
+ # If inside a Rails controller, `render json: options` will just work.
89
+ # I.e. it will encode and convert the options to JSON automatically.
90
+
91
+ # For your frontend code, you might find @github/webauthn-json npm package useful.
92
+ # Especially for handling the necessary decoding of the options, and sending the
93
+ # `PublicKeyCredential` object back to the server.
94
+ ```
95
+
96
+ #### Verification phase
97
+
98
+ ```ruby
99
+ # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back
100
+ # in params[:publicKeyCredential]:
101
+ begin
102
+ webauthn_credential = relying_party.verify_registration(
103
+ params[:publicKeyCredential],
104
+ params[:create_challenge]
105
+ )
106
+
107
+ # Store Credential ID, Credential Public Key and Sign Count for future authentications
108
+ user.credentials.create!(
109
+ external_id: webauthn_credential.id,
110
+ public_key: webauthn_credential.public_key,
111
+ sign_count: webauthn_credential.sign_count
112
+ )
113
+ rescue WebAuthn::Error => e
114
+ # Handle error
115
+ end
116
+ ```
117
+
118
+ ### Credential Authentication
119
+
120
+ #### Initiation phase
121
+
122
+ ```ruby
123
+ options = relying_party.options_for_authentication(allow: user.credentials.map { |c| c.webauthn_id })
124
+
125
+ # Store the newly generated challenge somewhere so you can have it
126
+ # for the verification phase.
127
+ session[:authentication_challenge] = options.challenge
128
+
129
+ # Send `options` back to the browser, so that they can be used
130
+ # to call `navigator.credentials.get({ "publicKey": options })`
131
+
132
+ # You can call `options.as_json` to get a ruby hash with a JSON representation if needed.
133
+
134
+ # If inside a Rails controller, `render json: options` will just work.
135
+ # I.e. it will encode and convert the options to JSON automatically.
136
+
137
+ # For your frontend code, you might find @github/webauthn-json npm package useful.
138
+ # Especially for handling the necessary decoding of the options, and sending the
139
+ # `PublicKeyCredential` object back to the server.
140
+ ```
141
+
142
+ #### Verification phase
143
+
144
+ ```ruby
145
+ begin
146
+ # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back
147
+ # in params[:publicKeyCredential]:
148
+ webauthn_credential, stored_credential = relying_party.verify_authentication(
149
+ params[:publicKeyCredential],
150
+ session[:authentication_challenge]
151
+ ) do |webauthn_credential|
152
+ # the returned object needs to respond to #public_key and #sign_count
153
+ user.credentials.find_by(external_id: webauthn_credential.id)
154
+ end
155
+
156
+ # Update the stored credential sign count with the value from `webauthn_credential.sign_count`
157
+ stored_credential.update!(sign_count: webauthn_credential.sign_count)
158
+
159
+ # Continue with successful sign in or 2FA verification...
160
+
161
+ rescue WebAuthn::SignCountVerificationError => e
162
+ # Cryptographic verification of the authenticator data succeeded, but the signature counter was less then or equal
163
+ # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or
164
+ # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter
165
+ rescue WebAuthn::Error => e
166
+ # Handle error
167
+ end
168
+ ```
169
+
170
+ ## Moving from Global to Instance Based Configuration
171
+
172
+ Adding a configuration for a new instance does not mean you need to get rid of your Global configuration. They can co-exist in your application and be both available for the different usages you might have. `WebAuthn.configuration.relying_party` will always return the global one while `WebAuthn::RelyingParty.new`, executed anywhere in your codebase, will allow you to create a different instance as you see the need. They will not collide and instead operate in isolation without any shared state.
173
+
174
+ The gem API described in the current [Usage](../README.md#usage) section for the [Global Configuration](../README.md#configuration) approach will still valid but the [Instance Based API](#instance-based-api) also works with the global `relying_party` that is maintain globally at `WebAuthn.configuration.relying_party`.
@@ -53,7 +53,7 @@ migrated_credential.authenticator_data.sign_count
53
53
 
54
54
  ## Authenticate migrated U2F credentials
55
55
 
56
- Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#authentication),
56
+ Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#initiation-phase-1),
57
57
  you need to specify the [FIDO AppID extension](https://www.w3.org/TR/webauthn/#sctn-appid-extension) for U2F migratedq
58
58
  credentials. The WebAuthn standard explains:
59
59
 
@@ -65,32 +65,26 @@ For the earlier given example `domain` this means:
65
65
  - FIDO AppID: `https://login.example.com`
66
66
  - Valid RP IDs: `login.example.com` (default) and `example.com`
67
67
 
68
+ You can request the use of the `appid` extension by setting the AppID in the configuration, like this:
69
+
70
+ ```ruby
71
+ WebAuthn.configure do |config|
72
+ config.legacy_u2f_appid = "https://login.example.com"
73
+ end
74
+ ```
75
+
76
+ By doing this, the `appid` extension will be automatically requested when generating the options for get:
77
+
68
78
  ```ruby
69
- credential_request_options = WebAuthn.credential_request_options
70
- credential_request_options[:extensions] = { appid: domain.to_s }
79
+ options = WebAuthn::Credential.options_for_get
71
80
  ```
72
81
 
73
82
  On the frontend, in the resolved value from `navigator.credentials.get({ "publicKey": credentialRequestOptions })` add
74
83
  a call to [getClientExtensionResults()](https://www.w3.org/TR/webauthn/#dom-publickeycredential-getclientextensionresults)
75
84
  and send its result to your backend alongside the `id`/`rawId` and `response` values. If the authenticator used the AppID
76
- extension, the returned value will contain `{ "appid": true }`. In the example below, we use `clientExtensionResults`.
85
+ extension, the returned value will contain `{ "appid": true }`.
77
86
 
78
- During authentication verification phase, you must pass either the original AppID or the RP ID as the `rp_id` argument:
87
+ During authentication verification phase, if you followed the [verification phase documentation](https://github.com/cedarcode/webauthn-ruby#verification-phase-1) and have set the AppID in the config, the method `PublicKeyCredentialWithAssertion#verify` will be smart enough to determine if it should use the AppID or the RP ID to verify the WebAuthn credential, depending on the output of the `appid` client extension:
79
88
 
80
89
  > If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the `rpIdHash` to be
81
90
  > the hash of the _AppID_, not the RP ID.
82
-
83
- ```ruby
84
- assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
85
- credential_id: params[:id],
86
- authenticator_data: params[:response][:authenticatorData],
87
- client_data_json: params[:response][:clientDataJSON],
88
- signature: params[:response][:signature],
89
- )
90
-
91
- assertion_response.verify(
92
- expected_challenge,
93
- allowed_credentials: [credential],
94
- rp_id: params[:clientExtensionResults][:appid] ? domain.to_s : domain.host,
95
- )
96
- ```
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose"
4
+ require "cose/algorithm/signature_algorithm"
5
+ require "cose/error"
6
+ require "cose/key/rsa"
7
+ require "openssl/signature_algorithm/rsapkcs1"
8
+
9
+ class RSAPKCS1Algorithm < COSE::Algorithm::SignatureAlgorithm
10
+ attr_reader :hash_function
11
+
12
+ def initialize(*args, hash_function:)
13
+ super(*args)
14
+
15
+ @hash_function = hash_function
16
+ end
17
+
18
+ private
19
+
20
+ def signature_algorithm_class
21
+ OpenSSL::SignatureAlgorithm::RSAPKCS1
22
+ end
23
+
24
+ def valid_key?(key)
25
+ to_cose_key(key).is_a?(COSE::Key::RSA)
26
+ end
27
+
28
+ def to_pkey(key)
29
+ case key
30
+ when COSE::Key::RSA
31
+ key.to_pkey
32
+ when OpenSSL::PKey::RSA
33
+ key
34
+ else
35
+ raise(COSE::Error, "Incompatible key for algorithm")
36
+ end
37
+ end
38
+ end
39
+
40
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-257, "RS256", hash_function: "SHA256"))
41
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-258, "RS384", hash_function: "SHA384"))
42
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-259, "RS512", hash_function: "SHA512"))
43
+
44
+ # Patch openssl-signature_algorithm gem to support discouraged/deprecated RSA-PKCS#1 with SHA-1
45
+ # (RS1 in JOSE/COSE terminology) algorithm needed for WebAuthn.
46
+ OpenSSL::SignatureAlgorithm::RSAPKCS1.const_set(
47
+ :ACCEPTED_HASH_FUNCTIONS,
48
+ OpenSSL::SignatureAlgorithm::RSAPKCS1::ACCEPTED_HASH_FUNCTIONS + ["SHA1"]
49
+ )
50
+ COSE::Algorithm.register(RSAPKCS1Algorithm.new(-65535, "RS1", hash_function: "SHA1"))
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cbor"
4
+ require "forwardable"
5
+ require "openssl"
6
+ require "webauthn/attestation_statement"
7
+ require "webauthn/authenticator_data"
8
+
9
+ module WebAuthn
10
+ class AttestationObject
11
+ extend Forwardable
12
+
13
+ def self.deserialize(attestation_object, relying_party)
14
+ from_map(CBOR.decode(attestation_object), relying_party)
15
+ end
16
+
17
+ def self.from_map(map, relying_party)
18
+ new(
19
+ authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]),
20
+ attestation_statement: WebAuthn::AttestationStatement.from(
21
+ map["fmt"],
22
+ map["attStmt"],
23
+ relying_party: relying_party
24
+ )
25
+ )
26
+ end
27
+
28
+ attr_reader :authenticator_data, :attestation_statement, :relying_party
29
+
30
+ def initialize(authenticator_data:, attestation_statement:)
31
+ @authenticator_data = authenticator_data
32
+ @attestation_statement = attestation_statement
33
+ end
34
+
35
+ def valid_attested_credential?
36
+ authenticator_data.attested_credential_data_included? &&
37
+ authenticator_data.attested_credential_data.valid?
38
+ end
39
+
40
+ def valid_attestation_statement?(client_data_hash)
41
+ attestation_statement.valid?(authenticator_data, client_data_hash)
42
+ end
43
+
44
+ def_delegators :authenticator_data, :credential, :aaguid
45
+ def_delegators :attestation_statement, :attestation_certificate_key_id
46
+ end
47
+ end
@@ -1,73 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "android_key_attestation"
3
4
  require "openssl"
4
- require "webauthn/attestation_statement/android_key/key_description"
5
5
  require "webauthn/attestation_statement/base"
6
- require "webauthn/security_utils"
7
- require "webauthn/signature_verifier"
8
6
 
9
7
  module WebAuthn
10
8
  module AttestationStatement
11
9
  class AndroidKey < Base
12
- EXTENSION_DATA_OID = "1.3.6.1.4.1.11129.2.1.17"
13
-
14
- # https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/keymaster_defs.h
15
- KM_ORIGIN_GENERATED = 0
16
- KM_PURPOSE_SIGN = 2
17
-
18
10
  def valid?(authenticator_data, client_data_hash)
19
11
  valid_signature?(authenticator_data, client_data_hash) &&
20
12
  matching_public_key?(authenticator_data) &&
21
13
  valid_attestation_challenge?(client_data_hash) &&
22
- all_applications_field_not_present? &&
14
+ all_applications_fields_not_set? &&
23
15
  valid_authorization_list_origin? &&
24
16
  valid_authorization_list_purpose? &&
25
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_trust_path]
17
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
18
+ [attestation_type, attestation_trust_path]
26
19
  end
27
20
 
28
21
  private
29
22
 
30
- def valid_signature?(authenticator_data, client_data_hash)
31
- WebAuthn::SignatureVerifier
32
- .new(algorithm, attestation_certificate.public_key)
33
- .verify(signature, authenticator_data.data + client_data_hash)
23
+ def valid_attestation_challenge?(client_data_hash)
24
+ android_key_attestation.verify_challenge(client_data_hash)
25
+ rescue AndroidKeyAttestation::ChallengeMismatchError
26
+ false
34
27
  end
35
28
 
36
- def matching_public_key?(authenticator_data)
37
- attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der
29
+ def valid_certificate_chain?(aaguid: nil, **_)
30
+ android_key_attestation.verify_certificate_chain(root_certificates: root_certificates(aaguid: aaguid))
31
+ rescue AndroidKeyAttestation::CertificateVerificationError
32
+ false
38
33
  end
39
34
 
40
- def valid_attestation_challenge?(client_data_hash)
41
- WebAuthn::SecurityUtils.secure_compare(key_description.attestation_challenge, client_data_hash)
42
- end
43
-
44
- def all_applications_field_not_present?
45
- tee_enforced.all_applications.nil? && software_enforced.all_applications.nil?
35
+ def all_applications_fields_not_set?
36
+ !tee_enforced.all_applications && !software_enforced.all_applications
46
37
  end
47
38
 
48
39
  def valid_authorization_list_origin?
49
- tee_enforced.origin == KM_ORIGIN_GENERATED || software_enforced.origin == KM_ORIGIN_GENERATED
40
+ tee_enforced.origin == :generated || software_enforced.origin == :generated
50
41
  end
51
42
 
52
43
  def valid_authorization_list_purpose?
53
- tee_enforced.purpose == KM_PURPOSE_SIGN || software_enforced.purpose == KM_PURPOSE_SIGN
44
+ tee_enforced.purpose == [:sign] || software_enforced.purpose == [:sign]
54
45
  end
55
46
 
56
47
  def tee_enforced
57
- key_description.tee_enforced
48
+ android_key_attestation.tee_enforced
58
49
  end
59
50
 
60
51
  def software_enforced
61
- key_description.software_enforced
52
+ android_key_attestation.software_enforced
53
+ end
54
+
55
+ def attestation_type
56
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC
62
57
  end
63
58
 
64
- def key_description
65
- @key_description ||= begin
66
- extension_data = attestation_certificate.extensions.detect { |ext| ext.oid == EXTENSION_DATA_OID }
67
- raw_key_description = OpenSSL::ASN1.decode(extension_data).value.last
59
+ def default_root_certificates
60
+ AndroidKeyAttestation::Statement::GOOGLE_ROOT_CERTIFICATES
61
+ end
68
62
 
69
- KeyDescription.new(OpenSSL::ASN1.decode(raw_key_description.value).value)
70
- end
63
+ def android_key_attestation
64
+ @android_key_attestation ||= AndroidKeyAttestation::Statement.new(*certificates)
71
65
  end
72
66
  end
73
67
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "android_safetynet/attestation_response"
3
+ require "safety_net_attestation"
4
4
  require "openssl"
5
5
  require "webauthn/attestation_statement/base"
6
6
 
@@ -12,11 +12,8 @@ module WebAuthn
12
12
  valid_response?(authenticator_data, client_data_hash) &&
13
13
  valid_version? &&
14
14
  cts_profile_match? &&
15
- [WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC, attestation_trust_path]
16
- end
17
-
18
- def attestation_certificate
19
- attestation_response.leaf_certificate
15
+ trustworthy?(aaguid: authenticator_data.aaguid) &&
16
+ [attestation_type, attestation_trust_path]
20
17
  end
21
18
 
22
19
  private
@@ -25,8 +22,9 @@ module WebAuthn
25
22
  nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash)
26
23
 
27
24
  begin
28
- attestation_response.verify(nonce, trustworthiness: false)
29
- rescue ::AndroidSafetynet::AttestationResponse::VerificationError
25
+ attestation_response
26
+ .verify(nonce, trusted_certificates: root_certificates(aaguid: authenticator_data.aaguid), time: time)
27
+ rescue SafetyNetAttestation::Error
30
28
  false
31
29
  end
32
30
  end
@@ -40,12 +38,30 @@ module WebAuthn
40
38
  attestation_response.cts_profile_match?
41
39
  end
42
40
 
43
- def attestation_trust_path
44
- attestation_response.certificate_chain
41
+ def valid_certificate_chain?(**_)
42
+ # Already performed as part of #valid_response?
43
+ true
44
+ end
45
+
46
+ def attestation_type
47
+ WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC
48
+ end
49
+
50
+ # SafetyNetAttestation returns full chain including root, WebAuthn expects only the x5c certificates
51
+ def certificates
52
+ attestation_response.certificate_chain[0..-2]
45
53
  end
46
54
 
47
55
  def attestation_response
48
- @attestation_response ||= ::AndroidSafetynet::AttestationResponse.new(statement["response"])
56
+ @attestation_response ||= SafetyNetAttestation::Statement.new(statement["response"])
57
+ end
58
+
59
+ def default_root_certificates
60
+ SafetyNetAttestation::Statement::GOOGLE_ROOT_CERTIFICATES
61
+ end
62
+
63
+ def time
64
+ Time.now
49
65
  end
50
66
  end
51
67
  end
@@ -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&.find_extension(NONCE_EXTENSION_OID)
41
+
42
+ if extension
43
+ sequence = OpenSSL::ASN1.decode(extension.value_der)
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