mirror of
https://github.com/glitch-soc/mastodon
synced 2025-04-24 21:14:51 +00:00
Refactoring: Move SignatureVerificationError
into Mastodon
namespace (#34342)
This commit is contained in:
parent
324acff572
commit
e2ef173b82
4 changed files with 32 additions and 33 deletions
|
@ -10,8 +10,6 @@ module SignatureVerification
|
||||||
EXPIRATION_WINDOW_LIMIT = 12.hours
|
EXPIRATION_WINDOW_LIMIT = 12.hours
|
||||||
CLOCK_SKEW_MARGIN = 1.hour
|
CLOCK_SKEW_MARGIN = 1.hour
|
||||||
|
|
||||||
class SignatureVerificationError < StandardError; end
|
|
||||||
|
|
||||||
def require_account_signature!
|
def require_account_signature!
|
||||||
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||||
end
|
end
|
||||||
|
@ -34,7 +32,7 @@ module SignatureVerification
|
||||||
|
|
||||||
def signature_key_id
|
def signature_key_id
|
||||||
signature_params['keyId']
|
signature_params['keyId']
|
||||||
rescue SignatureVerificationError
|
rescue Mastodon::SignatureVerificationError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -45,17 +43,17 @@ module SignatureVerification
|
||||||
def signed_request_actor
|
def signed_request_actor
|
||||||
return @signed_request_actor if defined?(@signed_request_actor)
|
return @signed_request_actor if defined?(@signed_request_actor)
|
||||||
|
|
||||||
raise SignatureVerificationError, 'Request not signed' unless signed_request?
|
raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request?
|
||||||
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
|
raise Mastodon::SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
|
||||||
raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
|
raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
|
||||||
raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
|
raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
|
||||||
|
|
||||||
verify_signature_strength!
|
verify_signature_strength!
|
||||||
verify_body_digest!
|
verify_body_digest!
|
||||||
|
|
||||||
actor = actor_from_key_id(signature_params['keyId'])
|
actor = actor_from_key_id(signature_params['keyId'])
|
||||||
|
|
||||||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
raise Mastodon::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(include_query_string: true)
|
compare_signed_string = build_signed_string(include_query_string: true)
|
||||||
|
@ -68,7 +66,7 @@ module SignatureVerification
|
||||||
|
|
||||||
actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
|
actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
|
||||||
|
|
||||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||||
|
|
||||||
compare_signed_string = build_signed_string(include_query_string: true)
|
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?
|
||||||
|
@ -78,7 +76,7 @@ module SignatureVerification
|
||||||
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']
|
||||||
rescue SignatureVerificationError => e
|
rescue Mastodon::SignatureVerificationError => e
|
||||||
fail_with! e.message
|
fail_with! e.message
|
||||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||||
fail_with! "Failed to fetch remote data: #{e.message}"
|
fail_with! "Failed to fetch remote data: #{e.message}"
|
||||||
|
@ -104,7 +102,7 @@ module SignatureVerification
|
||||||
def signature_params
|
def signature_params
|
||||||
@signature_params ||= SignatureParser.parse(request.headers['Signature'])
|
@signature_params ||= SignatureParser.parse(request.headers['Signature'])
|
||||||
rescue SignatureParser::ParsingError
|
rescue SignatureParser::ParsingError
|
||||||
raise SignatureVerificationError, 'Error parsing signature parameters'
|
raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters'
|
||||||
end
|
end
|
||||||
|
|
||||||
def signature_algorithm
|
def signature_algorithm
|
||||||
|
@ -116,31 +114,31 @@ module SignatureVerification
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_signature_strength!
|
def verify_signature_strength!
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
|
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_body_digest!
|
def verify_body_digest!
|
||||||
return unless signed_headers.include?('digest')
|
return unless signed_headers.include?('digest')
|
||||||
raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
|
raise Mastodon::SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
|
||||||
|
|
||||||
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
||||||
sha256 = digests.assoc('sha-256')
|
sha256 = digests.assoc('sha-256')
|
||||||
raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
|
raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
|
||||||
|
|
||||||
return if body_digest == sha256[1]
|
return if body_digest == sha256[1]
|
||||||
|
|
||||||
digest_size = begin
|
digest_size = begin
|
||||||
Base64.strict_decode64(sha256[1].strip).length
|
Base64.strict_decode64(sha256[1].strip).length
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
|
raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
|
raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
|
||||||
|
|
||||||
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
|
raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_signature(actor, signature, compare_signed_string)
|
def verify_signature(actor, signature, compare_signed_string)
|
||||||
|
@ -165,13 +163,13 @@ module SignatureVerification
|
||||||
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
end
|
end
|
||||||
when '(created)'
|
when '(created)'
|
||||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
raise Mastodon::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 Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||||
|
|
||||||
"(created): #{signature_params['created']}"
|
"(created): #{signature_params['created']}"
|
||||||
when '(expires)'
|
when '(expires)'
|
||||||
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
|
raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
|
||||||
|
|
||||||
"(expires): #{signature_params['expires']}"
|
"(expires): #{signature_params['expires']}"
|
||||||
else
|
else
|
||||||
|
@ -193,7 +191,7 @@ module SignatureVerification
|
||||||
|
|
||||||
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
|
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
|
||||||
rescue ArgumentError => e
|
rescue ArgumentError => e
|
||||||
raise SignatureVerificationError, "Invalid Date header: #{e.message}"
|
raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
expires_time ||= created_time + 5.minutes unless created_time.nil?
|
expires_time ||= created_time + 5.minutes unless created_time.nil?
|
||||||
|
@ -233,9 +231,9 @@ module SignatureVerification
|
||||||
account
|
account
|
||||||
end
|
end
|
||||||
rescue Mastodon::PrivateNetworkAddressError => e
|
rescue Mastodon::PrivateNetworkAddressError => e
|
||||||
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
|
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
|
||||||
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
|
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
|
||||||
raise SignatureVerificationError, e.message
|
raise Mastodon::SignatureVerificationError, e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
def stoplight_wrapper
|
def stoplight_wrapper
|
||||||
|
@ -251,8 +249,8 @@ module SignatureVerification
|
||||||
|
|
||||||
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
|
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
|
||||||
rescue Mastodon::PrivateNetworkAddressError => e
|
rescue Mastodon::PrivateNetworkAddressError => e
|
||||||
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
|
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
|
||||||
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
|
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
|
||||||
raise SignatureVerificationError, e.message
|
raise Mastodon::SignatureVerificationError, e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,11 +53,11 @@ class Fasp::Request
|
||||||
|
|
||||||
def validate!(response)
|
def validate!(response)
|
||||||
content_digest_header = response.headers['content-digest']
|
content_digest_header = response.headers['content-digest']
|
||||||
raise SignatureVerification::SignatureVerificationError, 'content-digest missing' if content_digest_header.blank?
|
raise Mastodon::SignatureVerificationError, 'content-digest missing' if content_digest_header.blank?
|
||||||
raise SignatureVerification::SignatureVerificationError, 'content-digest does not match' if content_digest_header != content_digest(response.body)
|
raise Mastodon::SignatureVerificationError, 'content-digest does not match' if content_digest_header != content_digest(response.body)
|
||||||
|
|
||||||
signature_input = response.headers['signature-input']&.encode('UTF-8')
|
signature_input = response.headers['signature-input']&.encode('UTF-8')
|
||||||
raise SignatureVerification::SignatureVerificationError, 'signature-input is missing' if signature_input.blank?
|
raise Mastodon::SignatureVerificationError, 'signature-input is missing' if signature_input.blank?
|
||||||
|
|
||||||
linzer_response = Linzer.new_response(
|
linzer_response = Linzer.new_response(
|
||||||
response.body,
|
response.body,
|
||||||
|
|
|
@ -12,6 +12,7 @@ module Mastodon
|
||||||
class RateLimitExceededError < Error; end
|
class RateLimitExceededError < Error; end
|
||||||
class SyntaxError < Error; end
|
class SyntaxError < Error; end
|
||||||
class InvalidParameterError < Error; end
|
class InvalidParameterError < Error; end
|
||||||
|
class SignatureVerificationError < Error; end
|
||||||
|
|
||||||
class UnexpectedResponseError < Error
|
class UnexpectedResponseError < Error
|
||||||
attr_reader :response
|
attr_reader :response
|
||||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe Fasp::Request do
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
expect do
|
expect do
|
||||||
subject.send(method, '/test_path')
|
subject.send(method, '/test_path')
|
||||||
end.to raise_error(SignatureVerification::SignatureVerificationError)
|
end.to raise_error(Mastodon::SignatureVerificationError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue