Skip to content
Extraits de code Groupes Projets
Valider fdea1732 rédigé par Eugen Rochko's avatar Eugen Rochko Validation de GitHub
Parcourir les fichiers

Add Digest header to requests with body, handle acct and URI keyId (#4565)

parent 4e1bf082
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -31,7 +31,7 @@ module SignatureVerification ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter