HTTP signatures (#4146)
* Add Request class with HTTP signature generator Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06 * Add HTTP signature verification concern * Add test for SignatureVerification concern * Add basic test for Request class * Make PuSH subscribe/unsubscribe requests use new Request class Accidentally fix lease_seconds not being set and sent properly, and change the new minimum subscription duration to 1 day * Make all PuSH workers use new Request class * Make Salmon sender use new Request class * Make FetchLinkService use new Request class * Make FetchAtomService use the new Request class * Make Remotable use the new Request class * Make ResolveRemoteAccountService use the new Request class * Add more tests * Allow +-30 seconds window for signed request to remain valid * Disable time window validation for signed requests, restore 7 days as PuSH subscription duration (which was previous default due to a bug)
This commit is contained in:
parent
c1f201c49a
commit
1618b68bfa
23 changed files with 379 additions and 90 deletions
|
@ -2,6 +2,7 @@
|
|||
|
||||
class AccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
|
|
|
@ -42,7 +42,7 @@ class Api::SubscriptionsController < Api::BaseController
|
|||
end
|
||||
|
||||
def lease_seconds_or_default
|
||||
(params['hub.lease_seconds'] || 86_400).to_i.seconds
|
||||
(params['hub.lease_seconds'] || 1.day).to_i.seconds
|
||||
end
|
||||
|
||||
def set_account
|
||||
|
|
87
app/controllers/concerns/signature_verification.rb
Normal file
87
app/controllers/concerns/signature_verification.rb
Normal file
|
@ -0,0 +1,87 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Implemented according to HTTP signatures (Draft 6)
|
||||
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
|
||||
module SignatureVerification
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def signed_request?
|
||||
request.headers['Signature'].present?
|
||||
end
|
||||
|
||||
def signed_request_account
|
||||
return @signed_request_account if defined?(@signed_request_account)
|
||||
|
||||
unless signed_request?
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
raw_signature = request.headers['Signature']
|
||||
signature_params = {}
|
||||
|
||||
raw_signature.split(',').each do |part|
|
||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
||||
end
|
||||
|
||||
if incompatible_signature?(signature_params)
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
|
||||
|
||||
if account.nil?
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string(signature_params['headers'])
|
||||
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
else
|
||||
@signed_request_account = nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_signed_string(signed_headers)
|
||||
signed_headers = 'date' if signed_headers.blank?
|
||||
|
||||
signed_headers.split(' ').map do |signed_header|
|
||||
if signed_header == Request::REQUEST_TARGET
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
else
|
||||
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
|
||||
end
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def matches_time_window?
|
||||
begin
|
||||
time_sent = DateTime.httpdate(request.headers['Date'])
|
||||
rescue ArgumentError
|
||||
return false
|
||||
end
|
||||
|
||||
(Time.now.utc - time_sent).abs <= 30
|
||||
end
|
||||
|
||||
def to_header_name(name)
|
||||
name.split(/-/).map(&:capitalize).join('-')
|
||||
end
|
||||
|
||||
def incompatible_signature?(signature_params)
|
||||
signature_params['keyId'].blank? ||
|
||||
signature_params['signature'].blank? ||
|
||||
signature_params['algorithm'].blank? ||
|
||||
signature_params['algorithm'] != 'rsa-sha256' ||
|
||||
!signature_params['keyId'].start_with?('acct:')
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class StreamEntriesController < ApplicationController
|
||||
include Authorization
|
||||
include SignatureVerification
|
||||
|
||||
layout 'public'
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue