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.

115 lines
3.2 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. elsif account.possibly_stale?
  37. account = account.refresh!
  38. if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
  39. @signed_request_account = account
  40. @signed_request_account
  41. else
  42. @signed_request_account = nil
  43. end
  44. else
  45. @signed_request_account = nil
  46. end
  47. end
  48. def request_body
  49. @request_body ||= request.raw_post
  50. end
  51. private
  52. def build_signed_string(signed_headers)
  53. signed_headers = 'date' if signed_headers.blank?
  54. signed_headers.split(' ').map do |signed_header|
  55. if signed_header == Request::REQUEST_TARGET
  56. "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
  57. elsif signed_header == 'digest'
  58. "digest: #{body_digest}"
  59. else
  60. "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
  61. end
  62. end.join("\n")
  63. end
  64. def matches_time_window?
  65. begin
  66. time_sent = DateTime.httpdate(request.headers['Date'])
  67. rescue ArgumentError
  68. return false
  69. end
  70. (Time.now.utc - time_sent).abs <= 30
  71. end
  72. def body_digest
  73. "SHA-256=#{Digest::SHA256.base64digest(request_body)}"
  74. end
  75. def to_header_name(name)
  76. name.split(/-/).map(&:capitalize).join('-')
  77. end
  78. def incompatible_signature?(signature_params)
  79. signature_params['keyId'].blank? ||
  80. signature_params['signature'].blank? ||
  81. signature_params['algorithm'].blank? ||
  82. signature_params['algorithm'] != 'rsa-sha256'
  83. end
  84. def account_from_key_id(key_id)
  85. if key_id.start_with?('acct:')
  86. ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
  87. elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
  88. account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
  89. account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
  90. account
  91. end
  92. end
  93. end