Refactor ActivityPub handling to prepare for non-Account actors (#19212)

* Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService

ActivityPub::FetchRemoteAccountService is kept as a wrapper for when the actor is
specifically required to be an Account

* Refactor SignatureVerification to allow non-Account actors

* fixup! Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService

* Refactor ActivityPub::FetchRemoteKeyService to potentially return non-Account actors

* Refactor inbound ActivityPub payload processing to accept non-Account actors

* Refactor inbound ActivityPub processing to accept activities relayed through non-Account

* Refactor how Account key URIs are built

* Refactor Request and drop unused key_id_format parameter

* Rename ActivityPub::Dereferencer `signature_account` to `signature_actor`
shrike
Claire 2022-09-21 22:45:57 +02:00 committed by GitHub
parent 84aff598ea
commit 8cf7006d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 436 additions and 180 deletions

View File

@ -7,7 +7,7 @@ class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureAuthentication
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
before_action :set_body_classes

View File

@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
before_action :require_signature!
before_action :require_account_signature!
before_action :set_claim_result
def create

View File

@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_items
before_action :set_size
before_action :set_type

View File

@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!
before_action :require_account_signature!
before_action :set_items
before_action :set_cache_headers

View File

@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
before_action :require_signature!
before_action :require_actor_signature!
skip_before_action :authenticate_user!
def create
@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def upgrade_account
if signed_request_account.ostatus?
if signed_request_account&.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
DeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
end
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil?
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name)
end
end

View File

@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_statuses
before_action :set_cache_headers

View File

@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
DESCENDANTS_LIMIT = 60
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies

View File

@ -45,10 +45,14 @@ module SignatureVerification
end
end
def require_signature!
def require_account_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
def require_actor_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
end
def signed_request?
request.headers['Signature'].present?
end
@ -68,7 +72,11 @@ module SignatureVerification
end
def signed_request_account
return @signed_request_account if defined?(@signed_request_account)
signed_request_actor.is_a?(Account) ? signed_request_actor : nil
end
def signed_request_actor
return @signed_request_actor if defined?(@signed_request_actor)
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
@ -78,22 +86,22 @@ module SignatureVerification
verify_signature_strength!
verify_body_digest!
account = account_from_key_id(signature_params['keyId'])
actor = actor_from_key_id(signature_params['keyId'])
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string
return account unless verify_signature(account, signature, compare_signed_string).nil?
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
return account unless verify_signature(account, signature, compare_signed_string).nil?
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
fail_with! "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
rescue SignatureVerificationError => e
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
@ -112,7 +120,7 @@ module SignatureVerification
def fail_with!(message)
@signature_verification_failure_reason = message
@signed_request_account = nil
@signed_request_actor = nil
end
def signature_params
@ -160,10 +168,10 @@ module SignatureVerification
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
def verify_signature(account, signature, compare_signed_string)
if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
def verify_signature(actor, signature, compare_signed_string)
if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
@signed_request_actor = actor
@signed_request_actor
end
rescue OpenSSL::PKey::RSAError
nil
@ -226,7 +234,7 @@ module SignatureVerification
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
def account_from_key_id(key_id)
def actor_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@ -237,13 +245,13 @@ module SignatureVerification
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
@ -255,12 +263,14 @@ module SignatureVerification
.run
end
def account_refresh_key(account)
return if account.local? || !account.activitypub?
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true, suppress_errors: false)
def actor_refresh_key!(actor)
return if actor.local? || !actor.activitypub?
return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale?
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, Webfinger::Error => e
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
end

View File

@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }

View File

@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }

View File

@ -8,7 +8,7 @@ class StatusesController < ApplicationController
layout 'public'
before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers

View File

@ -8,7 +8,7 @@ class TagsController < ApplicationController
layout 'public'
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local
before_action :set_tag

View File

@ -116,12 +116,12 @@ class ActivityPub::Activity
def dereference_object!
return unless @object.is_a?(String)
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_actor: signed_fetch_actor)
@object = dereferencer.object unless dereferencer.object.nil?
end
def signed_fetch_account
def signed_fetch_actor
return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present?
first_mentioned_local_account || first_local_follower
@ -163,15 +163,15 @@ class ActivityPub::Activity
end
def followed_by_local_accounts?
@account.passive_relationships.exists? || @options[:relayed_through_account]&.passive_relationships&.exists?
@account.passive_relationships.exists? || (@options[:relayed_through_actor].is_a?(Account) && @options[:relayed_through_actor].passive_relationships&.exists?)
end
def requested_through_relay?
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
@options[:relayed_through_actor] && Relay.find_by(inbox_url: @options[:relayed_through_actor].inbox_url)&.enabled?
end
def reject_payload!
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_actor] && "via #{@options[:relayed_through_actor].uri}"}")
nil
end
end

View File

@ -3,10 +3,10 @@
class ActivityPub::Dereferencer
include JsonLdHelper
def initialize(uri, permitted_origin: nil, signature_account: nil)
def initialize(uri, permitted_origin: nil, signature_actor: nil)
@uri = uri
@permitted_origin = permitted_origin
@signature_account = signature_account
@signature_actor = signature_actor
end
def object
@ -46,7 +46,7 @@ class ActivityPub::Dereferencer
req.add_headers('Accept' => 'application/activity+json, application/ld+json')
req.add_headers(headers) if headers
req.on_behalf_of(@signature_account) if @signature_account
req.on_behalf_of(@signature_actor) if @signature_actor
req.perform do |res|
if res.code == 200

View File

@ -9,7 +9,7 @@ class ActivityPub::LinkedDataSignature
@json = json.with_indifferent_access
end
def verify_account!
def verify_actor!
return unless @json['signature'].is_a?(Hash)
type = @json['signature']['type']
@ -18,7 +18,7 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
return if creator.nil?
@ -35,7 +35,7 @@ class ActivityPub::LinkedDataSignature
def sign!(creator, sign_with: nil)
options = {
'type' => 'RsaSignature2017',
'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
'creator' => ActivityPub::TagManager.instance.key_uri_for(creator),
'created' => Time.now.utc.iso8601,
}

View File

@ -44,6 +44,10 @@ class ActivityPub::TagManager
end
end
def key_uri_for(target)
[uri_for(target), '#main-key'].join
end
def uri_for_username(username)
account_url(username: username)
end
@ -155,6 +159,10 @@ class ActivityPub::TagManager
path_params[param]
end
def uri_to_actor(uri)
uri_to_resource(uri, Account)
end
def uri_to_resource(uri, klass)
return if uri.nil?

View File

@ -40,12 +40,11 @@ class Request
set_digest! if options.key?(:body)
end
def on_behalf_of(account, key_id_format = :uri, sign_with: nil)
raise ArgumentError, 'account must not be nil' if account.nil?
def on_behalf_of(actor, sign_with: nil)
raise ArgumentError, 'actor must not be nil' if actor.nil?
@account = account
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair
@key_id_format = key_id_format
@actor = actor
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
self
end
@ -79,7 +78,7 @@ class Request
end
def headers
(@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
end
class << self
@ -128,12 +127,7 @@ class Request
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
ActivityPub::TagManager.instance.key_uri_for(@actor)
end
def http_client

View File

@ -6,7 +6,7 @@ class ActivityPub::PublicKeySerializer < ActivityPub::Serializer
attributes :id, :owner, :public_key_pem
def id
[ActivityPub::TagManager.instance.uri_for(object), '#main-key'].join
ActivityPub::TagManager.instance.key_uri_for(object)
end
def owner

View File

@ -1,80 +1,12 @@
# frozen_string_literal: true
class ActivityPub::FetchRemoteAccountService < BaseService
include JsonLdHelper
include DomainControlHelper
include WebfingerHelper
class Error < StandardError; end
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
return if domain_not_allowed?(uri)
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
actor = super
return actor if actor.nil? || actor.is_a?(Account)
@json = begin
if prefetched_body.nil?
fetch_resource(uri, id)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
rescue Oj::ParseError
raise Error, "Error parsing JSON-LD document #{uri}"
end
raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
@uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
check_webfinger! unless only_key
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
rescue Error => e
Rails.logger.debug "Fetching account #{uri} failed: #{e.message}"
raise unless suppress_errors
end
private
def check_webfinger!
webfinger = webfinger!("acct:#{@username}@#{@domain}")
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
return
end
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
@username, @domain = split_acct(webfinger.subject)
unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
end
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
rescue Webfinger::RedirectError => e
raise Error, e.message
rescue Webfinger::Error => e
raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
end
def split_acct(acct)
acct.gsub(/\Aacct:/, '').split('@')
end
def supported_context?
super(@json)
end
def expected_type?
equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
Rails.logger.debug "Fetching account #{uri} failed: Expected Account, got #{actor.class.name}"
raise Error, "Expected Account, got #{actor.class.name}" unless suppress_errors
end
end

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
class ActivityPub::FetchRemoteActorService < BaseService
include JsonLdHelper
include DomainControlHelper
include WebfingerHelper
class Error < StandardError; end
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
return if domain_not_allowed?(uri)
return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
@json = begin
if prefetched_body.nil?
fetch_resource(uri, id)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
rescue Oj::ParseError
raise Error, "Error parsing JSON-LD document #{uri}"
end
raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
@uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
check_webfinger! unless only_key
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
rescue Error => e
Rails.logger.debug "Fetching actor #{uri} failed: #{e.message}"
raise unless suppress_errors
end
private
def check_webfinger!
webfinger = webfinger!("acct:#{@username}@#{@domain}")
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
return
end
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
@username, @domain = split_acct(webfinger.subject)
unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
end
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
rescue Webfinger::RedirectError => e
raise Error, e.message
rescue Webfinger::Error => e
raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
end
def split_acct(acct)
acct.gsub(/\Aacct:/, '').split('@')
end
def supported_context?
super(@json)
end
def expected_type?
equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
end
end

View File

@ -5,7 +5,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
class Error < StandardError; end
# Returns account that owns the key
# Returns actor that owns the key
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank?
@ -27,7 +27,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
raise Error, "Unexpected object type for key #{uri}" unless expected_type?
return find_account(@json['id'], @json, suppress_errors) if person?
return find_actor(@json['id'], @json, suppress_errors) if person?
@owner = fetch_resource(owner_uri, true)
@ -36,7 +36,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?
find_account(owner_uri, @owner, suppress_errors)
find_actor(owner_uri, @owner, suppress_errors)
rescue Error => e
Rails.logger.debug "Fetching key #{uri} failed: #{e.message}"
raise unless suppress_errors
@ -44,18 +44,18 @@ class ActivityPub::FetchRemoteKeyService < BaseService
private
def find_account(uri, prefetched_body, suppress_errors)
account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
account
def find_actor(uri, prefetched_body, suppress_errors)
actor = ActivityPub::TagManager.instance.uri_to_actor(uri)
actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
actor
end
def expected_type?
person? || public_key?
actor? || public_key?
end
def person?
equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
def actor?
equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
end
def public_key?
@ -67,7 +67,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
end
def expected_owner_type?
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
end
def confirmed_owner?

View File

@ -3,8 +3,8 @@
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
def call(body, account, **options)
@account = account
def call(body, actor, **options)
@account = actor
@json = original_json = Oj.load(body, mode: :strict)
@options = options
@ -16,6 +16,7 @@ class ActivityPub::ProcessCollectionService < BaseService
end
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
return unless @account.is_a?(Account)
if @json['signature'].present?
# We have verified the signature, but in the compaction step above, might
@ -66,8 +67,10 @@ class ActivityPub::ProcessCollectionService < BaseService
end
def verify_account!
@options[:relayed_through_account] = @account
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
@options[:relayed_through_actor] = @account
@account = ActivityPub::LinkedDataSignature.new(@json).verify_actor!
@account = nil unless @account.is_a?(Account)
@account
rescue JSON::LD::JsonLdError => e
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
nil

View File

@ -47,7 +47,7 @@ class FetchResourceService < BaseService
body = response.body_with_limit
json = body_to_json(body)
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
elsif !terminal
link_header = response['Link'] && parse_link_header(response)

View File

@ -72,7 +72,7 @@ class Keys::ClaimService < BaseService
def build_post_request(uri)
Request.new(:post, uri).tap do |request|
request.on_behalf_of(@source_account, :uri)
request.on_behalf_of(@source_account)
request.add_headers(HEADERS)
end
end

View File

@ -20,8 +20,8 @@ class ResolveURLService < BaseService
private
def process_url
if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
if equals_or_includes_any?(type, ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
ActivityPub::FetchRemoteActorService.new.call(resource_url, prefetched_body: body)
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil?

View File

@ -37,7 +37,7 @@ class ActivityPub::DeliveryWorker
def build_request(http_client)
Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request|
request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with])
request.on_behalf_of(@source_account, sign_with: @options[:sign_with])
request.add_headers(HEADERS)
request.add_headers({ 'Collection-Synchronization' => synchronization_header }) if ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] != 'true' && @options[:synchronize_followers]
end

View File

@ -5,11 +5,15 @@ class ActivityPub::ProcessingWorker
sidekiq_options backtrace: true, retry: 8
def perform(account_id, body, delivered_to_account_id = nil)
account = Account.find_by(id: account_id)
return if account.nil?
def perform(actor_id, body, delivered_to_account_id = nil, actor_type = 'Account')
case actor_type
when 'Account'
actor = Account.find_by(id: actor_id)
end
ActivityPub::ProcessCollectionService.new.call(body, account, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
return if actor.nil?
ActivityPub::ProcessCollectionService.new.call(body, actor, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
rescue ActiveRecord::RecordInvalid => e
Rails.logger.debug "Error processing incoming ActivityPub object: #{e}"
end

View File

@ -420,7 +420,7 @@ RSpec.describe AccountsController, type: :controller do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
get :show, params: { username: account.username, format: format }
end

View File

@ -24,7 +24,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
Fabricate(:status_pin, account: account)
Fabricate(:status_pin, account: account)

View File

@ -15,7 +15,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
end
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do

View File

@ -6,7 +6,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
let(:remote_account) { nil }
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'POST #create' do

View File

@ -28,7 +28,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do

View File

@ -168,7 +168,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
before do
stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
allow(controller).to receive(:signed_request_account).and_return(remote_querier)
allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
Fabricate(:status, thread: status, visibility: :public)
Fabricate(:status, thread: status, visibility: :public)

View File

@ -3,6 +3,16 @@
require 'rails_helper'
describe ApplicationController, type: :controller do
class WrappedActor
attr_reader :wrapped_account
def initialize(wrapped_account)
@wrapped_account = wrapped_account
end
delegate :uri, :keypair, to: :wrapped_account
end
controller do
include SignatureVerification
@ -73,6 +83,41 @@ describe ApplicationController, type: :controller do
end
end
context 'with a valid actor that is not an Account' do
let(:actor) { WrappedActor.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 request older than a day' do
before do
get :success

View File

@ -426,7 +426,7 @@ describe StatusesController do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
context 'when account blocks account' do

View File

@ -115,7 +115,7 @@ RSpec.describe ActivityPub::Activity::Announce do
let(:object_json) { 'https://example.com/actor/hello-world' }
subject { described_class.new(json, sender, relayed_through_account: relay_account) }
subject { described_class.new(json, sender, relayed_through_actor: relay_account) }
before do
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))

View File

@ -4,10 +4,10 @@ RSpec.describe ActivityPub::Dereferencer do
describe '#object' do
let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
let(:permitted_origin) { 'https://example.com' }
let(:signature_account) { nil }
let(:signature_actor) { nil }
let(:uri) { nil }
subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
subject { described_class.new(uri, permitted_origin: permitted_origin, signature_actor: signature_actor).object }
before do
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
@ -21,7 +21,7 @@ RSpec.describe ActivityPub::Dereferencer do
end
context 'with signature account' do
let(:signature_account) { Fabricate(:account) }
let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
@ -52,7 +52,7 @@ RSpec.describe ActivityPub::Dereferencer do
end
context 'with signature account' do
let(:signature_account) { Fabricate(:account) }
let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject

View File

@ -20,7 +20,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
stub_jsonld_contexts!
end
describe '#verify_account!' do
describe '#verify_actor!' do
context 'when signature matches' do
let(:raw_signature) do
{
@ -32,7 +32,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
it 'returns creator' do
expect(subject.verify_account!).to eq sender
expect(subject.verify_actor!).to eq sender
end
end
@ -40,7 +40,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { nil }
it 'returns nil' do
expect(subject.verify_account!).to be_nil
expect(subject.verify_actor!).to be_nil
end
end
@ -55,7 +55,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
it 'returns nil' do
expect(subject.verify_account!).to be_nil
expect(subject.verify_actor!).to be_nil
end
end
end
@ -73,14 +73,14 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end
it 'can be verified again' do
expect(described_class.new(subject).verify_account!).to eq sender
expect(described_class.new(subject).verify_actor!).to eq sender
end
end
def sign(from_account, options, document)
def sign(from_actor, options, document)
options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
document_hash = Digest::SHA256.hexdigest(canonicalize(document))
to_be_verified = options_hash + document_hash
Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
Base64.strict_encode64(from_actor.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
end
end

View File

@ -0,0 +1,180 @@
require 'rails_helper'
RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
subject { ActivityPub::FetchRemoteActorService.new }
let!(:actor) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://example.com/alice',
type: 'Person',
preferredUsername: 'alice',
name: 'Alice',
summary: 'Foo bar',
inbox: 'http://example.com/alice/inbox',
}
end
describe '#call' do
let(:account) { subject.call('https://example.com/alice', id: true) }
shared_examples 'sets profile data' do
it 'returns an account' do
expect(account).to be_an Account
end
it 'sets display name' do
expect(account.display_name).to eq 'Alice'
end
it 'sets note' do
expect(account.note).to eq 'Foo bar'
end
it 'sets URL' do
expect(account.url).to eq 'https://example.com/alice'
end
end
context 'when the account does not have a inbox' do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do
actor[:inbox] = nil
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'returns nil' do
expect(account).to be_nil
end
end
context 'when URI and WebFinger share the same host' do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'sets username and domain from webfinger' do
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'example.com'
end
include_examples 'sets profile data'
end
context 'when WebFinger presents different domain than URI' do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'looks up "redirected" webfinger' do
account
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
end
it 'sets username and domain from final webfinger' do
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'iscool.af'
end
include_examples 'sets profile data'
end
context 'when WebFinger returns a different URI' do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'does not create account' do
expect(account).to be_nil
end
end
context 'when WebFinger returns a different URI after a redirection' do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'looks up "redirected" webfinger' do
account
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
end
it 'does not create account' do
expect(account).to be_nil
end
end
context 'with wrong id' do
it 'does not create account' do
expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
end
end
end
end

View File

@ -68,7 +68,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
it 'does not process payload if no signature exists' do
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
expect(ActivityPub::Activity).not_to receive(:factory)
subject.call(json, forwarder)
@ -77,7 +77,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'processes payload with actor if valid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' }
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor)
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(actor)
expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
subject.call(json, forwarder)
@ -86,7 +86,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'does not process payload if invalid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' }
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
expect(ActivityPub::Activity).not_to receive(:factory)
subject.call(json, forwarder)

View File

@ -66,7 +66,7 @@ RSpec.describe FetchResourceService, type: :service do
it 'signs request' do
subject
expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made
expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.key_uri_for(Account.representative))}"/ })).to have_been_made
end
context 'when content type is application/atom+xml' do