The code powering m.abunchtell.com https://m.abunchtell.com
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.
 
 
 
 

152 lines
4.8 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 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. if request.headers['Date'].present? && !matches_time_window?
  20. @signature_verification_failure_reason = 'Signed request date outside acceptable time window'
  21. @signed_request_account = nil
  22. return
  23. end
  24. raw_signature = request.headers['Signature']
  25. signature_params = {}
  26. raw_signature.split(',').each do |part|
  27. parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
  28. next if parsed_parts.nil? || parsed_parts.size != 3
  29. signature_params[parsed_parts[1]] = parsed_parts[2]
  30. end
  31. if incompatible_signature?(signature_params)
  32. @signature_verification_failure_reason = 'Incompatible request signature'
  33. @signed_request_account = nil
  34. return
  35. end
  36. account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) }
  37. .with_fallback { nil }
  38. .with_threshold(1)
  39. .with_cool_off_time(5.minutes.seconds)
  40. .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
  41. account = account_stoplight.run
  42. if account.nil?
  43. @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
  44. @signed_request_account = nil
  45. return
  46. end
  47. signature = Base64.decode64(signature_params['signature'])
  48. compare_signed_string = build_signed_string(signature_params['headers'])
  49. return account unless verify_signature(account, signature, compare_signed_string).nil?
  50. account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
  51. .with_fallback { nil }
  52. .with_threshold(1)
  53. .with_cool_off_time(5.minutes.seconds)
  54. .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
  55. account = account_stoplight.run
  56. if account.nil?
  57. @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
  58. @signed_request_account = nil
  59. return
  60. end
  61. return account unless verify_signature(account, signature, compare_signed_string).nil?
  62. @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
  63. @signed_request_account = nil
  64. end
  65. def request_body
  66. @request_body ||= request.raw_post
  67. end
  68. private
  69. def verify_signature(account, signature, compare_signed_string)
  70. if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
  71. @signed_request_account = account
  72. @signed_request_account
  73. end
  74. rescue OpenSSL::PKey::RSAError
  75. nil
  76. end
  77. def build_signed_string(signed_headers)
  78. signed_headers = 'date' if signed_headers.blank?
  79. signed_headers.downcase.split(' ').map do |signed_header|
  80. if signed_header == Request::REQUEST_TARGET
  81. "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
  82. elsif signed_header == 'digest'
  83. "digest: #{body_digest}"
  84. else
  85. "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
  86. end
  87. end.join("\n")
  88. end
  89. def matches_time_window?
  90. begin
  91. time_sent = Time.httpdate(request.headers['Date'])
  92. rescue ArgumentError
  93. return false
  94. end
  95. (Time.now.utc - time_sent).abs <= 12.hours
  96. end
  97. def body_digest
  98. "SHA-256=#{Digest::SHA256.base64digest(request_body)}"
  99. end
  100. def to_header_name(name)
  101. name.split(/-/).map(&:capitalize).join('-')
  102. end
  103. def incompatible_signature?(signature_params)
  104. signature_params['keyId'].blank? ||
  105. signature_params['signature'].blank?
  106. end
  107. def account_from_key_id(key_id)
  108. if key_id.start_with?('acct:')
  109. ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
  110. elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
  111. account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
  112. account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
  113. account
  114. end
  115. end
  116. def account_refresh_key(account)
  117. return if account.local? || !account.activitypub?
  118. ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
  119. end
  120. end