You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

180 lines
5.5 KiB

  1. # frozen_string_literal: true
  2. # Implemented according to HTTP signatures (Draft 6)
  3. # <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
  4. module SignatureVerification
  5. extend ActiveSupport::Concern
  6. include DomainControlHelper
  7. def require_signature!
  8. render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
  9. end
  10. def signed_request?
  11. request.headers['Signature'].present?
  12. end
  13. def signature_verification_failure_reason
  14. @signature_verification_failure_reason
  15. end
  16. def signature_verification_failure_code
  17. @signature_verification_failure_code || 401
  18. end
  19. def signature_key_id
  20. raw_signature = request.headers['Signature']
  21. signature_params = {}
  22. raw_signature.split(',').each do |part|
  23. parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
  24. next if parsed_parts.nil? || parsed_parts.size != 3
  25. signature_params[parsed_parts[1]] = parsed_parts[2]
  26. end
  27. signature_params['keyId']
  28. end
  29. def signed_request_account
  30. return @signed_request_account if defined?(@signed_request_account)
  31. unless signed_request?
  32. @signature_verification_failure_reason = 'Request not signed'
  33. @signed_request_account = nil
  34. return
  35. end
  36. if request.headers['Date'].present? && !matches_time_window?
  37. @signature_verification_failure_reason = 'Signed request date outside acceptable time window'
  38. @signed_request_account = nil
  39. return
  40. end
  41. raw_signature = request.headers['Signature']
  42. signature_params = {}
  43. raw_signature.split(',').each do |part|
  44. parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
  45. next if parsed_parts.nil? || parsed_parts.size != 3
  46. signature_params[parsed_parts[1]] = parsed_parts[2]
  47. end
  48. if incompatible_signature?(signature_params)
  49. @signature_verification_failure_reason = 'Incompatible request signature'
  50. @signed_request_account = nil
  51. return
  52. end
  53. account = account_from_key_id(signature_params['keyId'])
  54. if account.nil?
  55. @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
  56. @signed_request_account = nil
  57. return
  58. end
  59. signature = Base64.decode64(signature_params['signature'])
  60. compare_signed_string = build_signed_string(signature_params['headers'])
  61. return account unless verify_signature(account, signature, compare_signed_string).nil?
  62. account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
  63. if account.nil?
  64. @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
  65. @signed_request_account = nil
  66. return
  67. end
  68. return account unless verify_signature(account, signature, compare_signed_string).nil?
  69. @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
  70. @signed_request_account = nil
  71. end
  72. def request_body
  73. @request_body ||= request.raw_post
  74. end
  75. private
  76. def verify_signature(account, signature, compare_signed_string)
  77. if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
  78. @signed_request_account = account
  79. @signed_request_account
  80. end
  81. rescue OpenSSL::PKey::RSAError
  82. nil
  83. end
  84. def build_signed_string(signed_headers)
  85. signed_headers = 'date' if signed_headers.blank?
  86. signed_headers.downcase.split(' ').map do |signed_header|
  87. if signed_header == Request::REQUEST_TARGET
  88. "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
  89. elsif signed_header == 'digest'
  90. "digest: #{body_digest}"
  91. else
  92. "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
  93. end
  94. end.join("\n")
  95. end
  96. def matches_time_window?
  97. begin
  98. time_sent = Time.httpdate(request.headers['Date'])
  99. rescue ArgumentError
  100. return false
  101. end
  102. (Time.now.utc - time_sent).abs <= 12.hours
  103. end
  104. def body_digest
  105. "SHA-256=#{Digest::SHA256.base64digest(request_body)}"
  106. end
  107. def to_header_name(name)
  108. name.split(/-/).map(&:capitalize).join('-')
  109. end
  110. def incompatible_signature?(signature_params)
  111. signature_params['keyId'].blank? ||
  112. signature_params['signature'].blank?
  113. end
  114. def account_from_key_id(key_id)
  115. domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
  116. if domain_not_allowed?(domain)
  117. @signature_verification_failure_code = 403
  118. return
  119. end
  120. if key_id.start_with?('acct:')
  121. stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
  122. elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
  123. account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
  124. account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
  125. account
  126. end
  127. rescue Mastodon::HostValidationError
  128. nil
  129. end
  130. def stoplight_wrap_request(&block)
  131. Stoplight("source:#{request.remote_ip}", &block)
  132. .with_fallback { nil }
  133. .with_threshold(1)
  134. .with_cool_off_time(5.minutes.seconds)
  135. .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
  136. .run
  137. end
  138. def account_refresh_key(account)
  139. return if account.local? || !account.activitypub?
  140. ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
  141. end
  142. end