123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- # frozen_string_literal: true
- require 'rails_helper'
- describe SignatureVerification do
- let(:wrapped_actor_class) do
- Class.new do
- attr_reader :wrapped_account
- def initialize(wrapped_account)
- @wrapped_account = wrapped_account
- end
- delegate :uri, :keypair, to: :wrapped_account
- end
- end
- controller(ApplicationController) do
- include SignatureVerification
- before_action :require_actor_signature!, only: [:signature_required]
- def success
- head 200
- end
- def alternative_success
- head 200
- end
- def signature_required
- head 200
- end
- end
- before do
- routes.draw do
- match :via => [:get, :post], 'success' => 'anonymous#success'
- match :via => [:get, :post], 'signature_required' => 'anonymous#signature_required'
- end
- end
- context 'without signature header' do
- before do
- get :success
- end
- describe '#signed_request?' do
- it 'returns false' do
- expect(controller.signed_request?).to be false
- end
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- end
- context 'with signature header' do
- let!(:author) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') }
- context 'without body' do
- before do
- get :success
- fake_request = Request.new(:get, request.url)
- fake_request.on_behalf_of(author)
- request.headers.merge!(fake_request.headers)
- end
- describe '#signed_request?' do
- it 'returns true' do
- 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
- context 'with a valid actor that is not an Account' do
- let(:actor) { wrapped_actor_class.new(author) }
- before do
- get :success
- fake_request = Request.new(:get, request.url)
- fake_request.on_behalf_of(author)
- request.headers.merge!(fake_request.headers)
- allow(ActivityPub::TagManager.instance).to receive(:uri_to_actor).with(anything) do
- actor
- end
- end
- describe '#signed_request?' do
- it 'returns true' do
- expect(controller.signed_request?).to be true
- end
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- describe '#signed_request_actor' do
- it 'returns the expected actor' do
- expect(controller.signed_request_actor).to eq actor
- end
- end
- end
- context 'with non-normalized URL' do
- before do
- get :success
- fake_request = Request.new(:get, 'http://test.host/subdir/../success')
- fake_request.on_behalf_of(author)
- request.headers.merge!(fake_request.headers)
- allow(controller).to receive(:actor_refresh_key!).and_return(author)
- end
- describe '#build_signed_string' do
- it 'includes the normalized request path' do
- expect(controller.send(:build_signed_string)).to start_with "(request-target): get /success\n"
- end
- end
- describe '#signed_request?' do
- it 'returns true' do
- expect(controller.signed_request?).to be true
- end
- end
- describe '#signed_request_actor' do
- it 'returns an account' do
- expect(controller.signed_request_account).to eq author
- end
- end
- end
- context 'with request with unparsable Date header' do
- before do
- get :success
- fake_request = Request.new(:get, request.url)
- fake_request.add_headers({ 'Date' => 'wrong date' })
- fake_request.on_behalf_of(author)
- request.headers.merge!(fake_request.headers)
- end
- describe '#signed_request?' do
- it 'returns true' do
- expect(controller.signed_request?).to be true
- end
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- describe '#signature_verification_failure_reason' do
- it 'contains an error description' do
- controller.signed_request_account
- expect(controller.signature_verification_failure_reason[:error]).to eq 'Invalid Date header: not RFC 2616 compliant date: "wrong date"'
- end
- end
- end
- context 'with request older than a day' do
- before do
- get :success
- fake_request = Request.new(:get, request.url)
- fake_request.add_headers({ 'Date' => 2.days.ago.utc.httpdate })
- fake_request.on_behalf_of(author)
- request.headers.merge!(fake_request.headers)
- end
- describe '#signed_request?' do
- it 'returns true' do
- expect(controller.signed_request?).to be true
- end
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- describe '#signature_verification_failure_reason' do
- it 'contains an error description' do
- controller.signed_request_account
- expect(controller.signature_verification_failure_reason[:error]).to eq 'Signed request date outside acceptable time window'
- end
- end
- end
- context 'with inaccessible key' do
- before do
- get :success
- author = Fabricate(:account, domain: 'localhost:5000', uri: 'http://localhost:5000/actor')
- fake_request = Request.new(:get, request.url)
- fake_request.on_behalf_of(author)
- author.destroy
- request.headers.merge!(fake_request.headers)
- stub_request(:get, 'http://localhost:5000/actor').to_raise(Mastodon::HostValidationError)
- end
- describe '#signed_request?' do
- it 'returns true' do
- expect(controller.signed_request?).to be true
- end
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- end
- context 'with body' do
- before do
- allow(controller).to receive(:actor_refresh_key!).and_return(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
- describe '#signed_request?' do
- it 'returns true' do
- 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
- end
- context 'when path does not match' do
- before do
- request.path = '/alternative-path'
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- describe '#signature_verification_failure_reason' do
- it 'contains an error description' do
- controller.signed_request_account
- expect(controller.signature_verification_failure_reason[:error]).to include('using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)')
- expect(controller.signature_verification_failure_reason[:signed_string]).to include("(request-target): post /alternative-path\n")
- end
- end
- end
- context 'when method does not match' do
- before do
- get :success
- end
- describe '#signed_request_account' do
- it 'returns nil' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- end
- context 'when body has been tampered' do
- before do
- post :success, body: 'doo doo doo'
- end
- describe '#signed_request_account' do
- it 'returns nil when body has been tampered' do
- expect(controller.signed_request_account).to be_nil
- end
- end
- end
- end
- end
- context 'when a signature is required' do
- before do
- get :signature_required
- end
- context 'without signature header' do
- it 'returns HTTP 401' do
- expect(response).to have_http_status(401)
- end
- it 'returns an error' do
- expect(Oj.load(response.body)['error']).to eq 'Request not signed'
- end
- end
- end
- end
|