Fix Mastodon not correctly processing HTTP Signatures with query strings (#28476)
This commit is contained in:
		
							parent
							
								
									1998c561b2
								
							
						
					
					
						commit
						3837ec2227
					
				| @ -91,14 +91,23 @@ module SignatureVerification | |||||||
|     raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? |     raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? | ||||||
| 
 | 
 | ||||||
|     signature             = Base64.decode64(signature_params['signature']) |     signature             = Base64.decode64(signature_params['signature']) | ||||||
|     compare_signed_string = build_signed_string |     compare_signed_string = build_signed_string(include_query_string: true) | ||||||
| 
 | 
 | ||||||
|     return actor unless verify_signature(actor, signature, compare_signed_string).nil? |     return actor unless verify_signature(actor, signature, compare_signed_string).nil? | ||||||
| 
 | 
 | ||||||
|  |     # Compatibility quirk with older Mastodon versions | ||||||
|  |     compare_signed_string = build_signed_string(include_query_string: false) | ||||||
|  |     return actor unless verify_signature(actor, signature, compare_signed_string).nil? | ||||||
|  | 
 | ||||||
|     actor = stoplight_wrap_request { actor_refresh_key!(actor) } |     actor = stoplight_wrap_request { actor_refresh_key!(actor) } | ||||||
| 
 | 
 | ||||||
|     raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? |     raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? | ||||||
| 
 | 
 | ||||||
|  |     compare_signed_string = build_signed_string(include_query_string: true) | ||||||
|  |     return actor unless verify_signature(actor, signature, compare_signed_string).nil? | ||||||
|  | 
 | ||||||
|  |     # Compatibility quirk with older Mastodon versions | ||||||
|  |     compare_signed_string = build_signed_string(include_query_string: false) | ||||||
|     return actor unless verify_signature(actor, signature, compare_signed_string).nil? |     return actor unless verify_signature(actor, signature, compare_signed_string).nil? | ||||||
| 
 | 
 | ||||||
|     fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] |     fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] | ||||||
| @ -180,11 +189,18 @@ module SignatureVerification | |||||||
|     nil |     nil | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def build_signed_string |   def build_signed_string(include_query_string: true) | ||||||
|     signed_headers.map do |signed_header| |     signed_headers.map do |signed_header| | ||||||
|       case signed_header |       case signed_header | ||||||
|       when Request::REQUEST_TARGET |       when Request::REQUEST_TARGET | ||||||
|         "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" |         if include_query_string | ||||||
|  |           "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" | ||||||
|  |         else | ||||||
|  |           # Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header. | ||||||
|  |           # Therefore, temporarily support such incorrect signatures for compatibility. | ||||||
|  |           # TODO: remove eventually some time after release of the fixed version | ||||||
|  |           "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" | ||||||
|  |         end | ||||||
|       when '(created)' |       when '(created)' | ||||||
|         raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' |         raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' | ||||||
|         raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? |         raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? | ||||||
|  | |||||||
| @ -77,6 +77,7 @@ class Request | |||||||
|     @url         = Addressable::URI.parse(url).normalize |     @url         = Addressable::URI.parse(url).normalize | ||||||
|     @http_client = options.delete(:http_client) |     @http_client = options.delete(:http_client) | ||||||
|     @allow_local = options.delete(:allow_local) |     @allow_local = options.delete(:allow_local) | ||||||
|  |     @full_path   = options.delete(:with_query_string) | ||||||
|     @options     = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket) |     @options     = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket) | ||||||
|     @options     = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT) |     @options     = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT) | ||||||
|     @options     = @options.merge(proxy_url) if use_proxy? |     @options     = @options.merge(proxy_url) if use_proxy? | ||||||
| @ -146,7 +147,7 @@ class Request | |||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def set_common_headers! |   def set_common_headers! | ||||||
|     @headers[REQUEST_TARGET]    = "#{@verb} #{@url.path}" |     @headers[REQUEST_TARGET]    = request_target | ||||||
|     @headers['User-Agent']      = Mastodon::Version.user_agent |     @headers['User-Agent']      = Mastodon::Version.user_agent | ||||||
|     @headers['Host']            = @url.host |     @headers['Host']            = @url.host | ||||||
|     @headers['Date']            = Time.now.utc.httpdate |     @headers['Date']            = Time.now.utc.httpdate | ||||||
| @ -157,6 +158,14 @@ class Request | |||||||
|     @headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}" |     @headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}" | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def request_target | ||||||
|  |     if @url.query.nil? || !@full_path | ||||||
|  |       "#{@verb} #{@url.path}" | ||||||
|  |     else | ||||||
|  |       "#{@verb} #{@url.path}?#{@url.query}" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def signature |   def signature | ||||||
|     algorithm = 'rsa-sha256' |     algorithm = 'rsa-sha256' | ||||||
|     signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) |     signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) | ||||||
|  | |||||||
| @ -94,6 +94,72 @@ describe 'signature verification concern' do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'with a valid signature on a GET request that has a query string' do | ||||||
|  |       let(:signature_header) do | ||||||
|  |         'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'successfuly verifies signature', :aggregate_failures do | ||||||
|  |         expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) | ||||||
|  | 
 | ||||||
|  |         get '/activitypub/success?foo=42', headers: { | ||||||
|  |           'Host' => 'www.example.com', | ||||||
|  |           'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', | ||||||
|  |           'Signature' => signature_header, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  |         expect(body_as_json).to match( | ||||||
|  |           signed_request: true, | ||||||
|  |           signature_actor_id: actor.id.to_s | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when the query string is missing from the signature verification (compatibility quirk)' do | ||||||
|  |       let(:signature_header) do | ||||||
|  |         'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'successfuly verifies signature', :aggregate_failures do | ||||||
|  |         expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) | ||||||
|  | 
 | ||||||
|  |         get '/activitypub/success?foo=42', headers: { | ||||||
|  |           'Host' => 'www.example.com', | ||||||
|  |           'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', | ||||||
|  |           'Signature' => signature_header, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  |         expect(body_as_json).to match( | ||||||
|  |           signed_request: true, | ||||||
|  |           signature_actor_id: actor.id.to_s | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with mismatching query string' do | ||||||
|  |       let(:signature_header) do | ||||||
|  |         'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'fails to verify signature', :aggregate_failures do | ||||||
|  |         expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) | ||||||
|  | 
 | ||||||
|  |         get '/activitypub/success?foo=43', headers: { | ||||||
|  |           'Host' => 'www.example.com', | ||||||
|  |           'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', | ||||||
|  |           'Signature' => signature_header, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         expect(body_as_json).to match( | ||||||
|  |           signed_request: true, | ||||||
|  |           signature_actor_id: nil, | ||||||
|  |           error: anything | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'with a mismatching path' do |     context 'with a mismatching path' do | ||||||
|       it 'fails to verify signature', :aggregate_failures do |       it 'fails to verify signature', :aggregate_failures do | ||||||
|         get '/activitypub/alternative-path', headers: { |         get '/activitypub/alternative-path', headers: { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user