闭社主体 forked from https://github.com/tootsuite/mastodon
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.

106 lines
2.9 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. def signed_request?
  7. request.headers['Signature'].present?
  8. end
  9. def signed_request_account
  10. return @signed_request_account if defined?(@signed_request_account)
  11. unless signed_request?
  12. @signed_request_account = nil
  13. return
  14. end
  15. raw_signature = request.headers['Signature']
  16. signature_params = {}
  17. raw_signature.split(',').each do |part|
  18. parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
  19. next if parsed_parts.nil? || parsed_parts.size != 3
  20. signature_params[parsed_parts[1]] = parsed_parts[2]
  21. end
  22. if incompatible_signature?(signature_params)
  23. @signed_request_account = nil
  24. return
  25. end
  26. account = account_from_key_id(signature_params['keyId'])
  27. if account.nil?
  28. @signed_request_account = nil
  29. return
  30. end
  31. signature = Base64.decode64(signature_params['signature'])
  32. compare_signed_string = build_signed_string(signature_params['headers'])
  33. if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
  34. @signed_request_account = account
  35. @signed_request_account
  36. else
  37. @signed_request_account = nil
  38. end
  39. end
  40. def request_body
  41. @request_body ||= request.raw_post
  42. end
  43. private
  44. def build_signed_string(signed_headers)
  45. signed_headers = 'date' if signed_headers.blank?
  46. signed_headers.split(' ').map do |signed_header|
  47. if signed_header == Request::REQUEST_TARGET
  48. "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
  49. elsif signed_header == 'digest'
  50. "digest: #{body_digest}"
  51. else
  52. "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
  53. end
  54. end.join("\n")
  55. end
  56. def matches_time_window?
  57. begin
  58. time_sent = DateTime.httpdate(request.headers['Date'])
  59. rescue ArgumentError
  60. return false
  61. end
  62. (Time.now.utc - time_sent).abs <= 30
  63. end
  64. def body_digest
  65. "SHA-256=#{Digest::SHA256.base64digest(request_body)}"
  66. end
  67. def to_header_name(name)
  68. name.split(/-/).map(&:capitalize).join('-')
  69. end
  70. def incompatible_signature?(signature_params)
  71. signature_params['keyId'].blank? ||
  72. signature_params['signature'].blank? ||
  73. signature_params['algorithm'].blank? ||
  74. signature_params['algorithm'] != 'rsa-sha256'
  75. end
  76. def account_from_key_id(key_id)
  77. if key_id.start_with?('acct:')
  78. ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
  79. elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
  80. account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
  81. account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
  82. account
  83. end
  84. end
  85. end