signature_verification.rb 3.8 KB

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