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.

87 lines
2.4 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 = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
  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. private
  41. def build_signed_string(signed_headers)
  42. signed_headers = 'date' if signed_headers.blank?
  43. signed_headers.split(' ').map do |signed_header|
  44. if signed_header == Request::REQUEST_TARGET
  45. "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
  46. else
  47. "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
  48. end
  49. end.join("\n")
  50. end
  51. def matches_time_window?
  52. begin
  53. time_sent = DateTime.httpdate(request.headers['Date'])
  54. rescue ArgumentError
  55. return false
  56. end
  57. (Time.now.utc - time_sent).abs <= 30
  58. end
  59. def to_header_name(name)
  60. name.split(/-/).map(&:capitalize).join('-')
  61. end
  62. def incompatible_signature?(signature_params)
  63. signature_params['keyId'].blank? ||
  64. signature_params['signature'].blank? ||
  65. signature_params['algorithm'].blank? ||
  66. signature_params['algorithm'] != 'rsa-sha256' ||
  67. !signature_params['keyId'].start_with?('acct:')
  68. end
  69. end