Add Digest header to requests with body, handle acct and URI keyId (#4565)
parent
4e1bf082ce
commit
fdea173237
|
@ -31,7 +31,7 @@ module SignatureVerification
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
|
account = account_from_key_id(signature_params['keyId'])
|
||||||
|
|
||||||
if account.nil?
|
if account.nil?
|
||||||
@signed_request_account = nil
|
@signed_request_account = nil
|
||||||
|
@ -49,6 +49,10 @@ module SignatureVerification
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def request_body
|
||||||
|
@request_body ||= request.raw_post
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_signed_string(signed_headers)
|
def build_signed_string(signed_headers)
|
||||||
|
@ -57,6 +61,8 @@ module SignatureVerification
|
||||||
signed_headers.split(' ').map do |signed_header|
|
signed_headers.split(' ').map do |signed_header|
|
||||||
if signed_header == Request::REQUEST_TARGET
|
if signed_header == Request::REQUEST_TARGET
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
|
elsif signed_header == 'digest'
|
||||||
|
"digest: #{body_digest}"
|
||||||
else
|
else
|
||||||
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
|
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
|
||||||
end
|
end
|
||||||
|
@ -73,6 +79,10 @@ module SignatureVerification
|
||||||
(Time.now.utc - time_sent).abs <= 30
|
(Time.now.utc - time_sent).abs <= 30
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def body_digest
|
||||||
|
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
|
||||||
|
end
|
||||||
|
|
||||||
def to_header_name(name)
|
def to_header_name(name)
|
||||||
name.split(/-/).map(&:capitalize).join('-')
|
name.split(/-/).map(&:capitalize).join('-')
|
||||||
end
|
end
|
||||||
|
@ -81,7 +91,14 @@ module SignatureVerification
|
||||||
signature_params['keyId'].blank? ||
|
signature_params['keyId'].blank? ||
|
||||||
signature_params['signature'].blank? ||
|
signature_params['signature'].blank? ||
|
||||||
signature_params['algorithm'].blank? ||
|
signature_params['algorithm'].blank? ||
|
||||||
signature_params['algorithm'] != 'rsa-sha256' ||
|
signature_params['algorithm'] != 'rsa-sha256'
|
||||||
!signature_params['keyId'].start_with?('acct:')
|
end
|
||||||
|
|
||||||
|
def account_from_key_id(key_id)
|
||||||
|
if key_id.start_with?('acct:')
|
||||||
|
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
|
||||||
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
|
ActivityPub::FetchRemoteAccountService.new.call(key_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,15 +12,21 @@ class Request
|
||||||
@headers = {}
|
@headers = {}
|
||||||
|
|
||||||
set_common_headers!
|
set_common_headers!
|
||||||
|
set_digest! if options.key?(:body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_behalf_of(account)
|
def on_behalf_of(account, key_id_format = :acct)
|
||||||
raise ArgumentError unless account.local?
|
raise ArgumentError unless account.local?
|
||||||
@account = account
|
|
||||||
|
@account = account
|
||||||
|
@key_id_format = key_id_format
|
||||||
|
|
||||||
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_headers(new_headers)
|
def add_headers(new_headers)
|
||||||
@headers.merge!(new_headers)
|
@headers.merge!(new_headers)
|
||||||
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
@ -40,8 +46,11 @@ class Request
|
||||||
@headers['Date'] = Time.now.utc.httpdate
|
@headers['Date'] = Time.now.utc.httpdate
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_digest!
|
||||||
|
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
|
||||||
|
end
|
||||||
|
|
||||||
def signature
|
def signature
|
||||||
key_id = @account.to_webfinger_s
|
|
||||||
algorithm = 'rsa-sha256'
|
algorithm = 'rsa-sha256'
|
||||||
signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
|
signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
|
||||||
|
|
||||||
|
@ -60,6 +69,15 @@ class Request
|
||||||
@user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})"
|
@user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def key_id
|
||||||
|
case @key_id_format
|
||||||
|
when :acct
|
||||||
|
@account.to_webfinger_s
|
||||||
|
when :uri
|
||||||
|
[ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def timeout
|
def timeout
|
||||||
{ write: 10, connect: 10, read: 10 }
|
{ write: 10, connect: 10, read: 10 }
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe ApplicationController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
routes.draw { get 'success' => 'anonymous#success' }
|
routes.draw { match via: [:get, :post], 'success' => 'anonymous#success' }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without signature header' do
|
context 'without signature header' do
|
||||||
|
@ -40,34 +40,74 @@ describe ApplicationController, type: :controller do
|
||||||
context 'with signature header' do
|
context 'with signature header' do
|
||||||
let!(:author) { Fabricate(:account) }
|
let!(:author) { Fabricate(:account) }
|
||||||
|
|
||||||
before do
|
context 'without body' do
|
||||||
get :success
|
before do
|
||||||
|
get :success
|
||||||
|
|
||||||
fake_request = Request.new(:get, request.url)
|
fake_request = Request.new(:get, request.url)
|
||||||
fake_request.on_behalf_of(author)
|
fake_request.on_behalf_of(author)
|
||||||
|
|
||||||
request.headers.merge!(fake_request.headers)
|
request.headers.merge!(fake_request.headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#signed_request?' do
|
describe '#signed_request?' do
|
||||||
it 'returns true' do
|
it 'returns true' do
|
||||||
expect(controller.signed_request?).to be true
|
expect(controller.signed_request?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#signed_request_account' do
|
||||||
|
it 'returns an account' do
|
||||||
|
expect(controller.signed_request_account).to eq author
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when path does not match' do
|
||||||
|
request.path = '/alternative-path'
|
||||||
|
expect(controller.signed_request_account).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when method does not match' do
|
||||||
|
post :success
|
||||||
|
expect(controller.signed_request_account).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#signed_request_account' do
|
context 'with body' do
|
||||||
it 'returns an account' do
|
before do
|
||||||
expect(controller.signed_request_account).to eq author
|
post :success, body: 'Hello world'
|
||||||
|
|
||||||
|
fake_request = Request.new(:post, request.url, body: 'Hello world')
|
||||||
|
fake_request.on_behalf_of(author)
|
||||||
|
|
||||||
|
request.headers.merge!(fake_request.headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns nil when path does not match' do
|
describe '#signed_request?' do
|
||||||
request.path = '/alternative-path'
|
it 'returns true' do
|
||||||
expect(controller.signed_request_account).to be_nil
|
expect(controller.signed_request?).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns nil when method does not match' do
|
describe '#signed_request_account' do
|
||||||
post :success
|
it 'returns an account' do
|
||||||
expect(controller.signed_request_account).to be_nil
|
expect(controller.signed_request_account).to eq author
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when path does not match' do
|
||||||
|
request.path = '/alternative-path'
|
||||||
|
expect(controller.signed_request_account).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when method does not match' do
|
||||||
|
get :success
|
||||||
|
expect(controller.signed_request_account).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when body has been tampered' do
|
||||||
|
request.headers['RAW_POST_DATA'] = 'doo doo doo'
|
||||||
|
expect(controller.signed_request_account).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Reference in New Issue