* 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`
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
skip_before_action :authenticate_user!
- before_action :require_signature!
+ before_action :require_account_signature!
before_action :set_claim_result
def create
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
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!
+ before_action :require_account_signature!
before_action :set_items
before_action :set_cache_headers
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
- before_action :require_signature!
+ before_action :require_actor_signature!
skip_before_action :authenticate_user!
def create
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)
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
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
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
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
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?
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
def fail_with!(message)
@signature_verification_failure_reason = message
- @signed_request_account = nil
+ @signed_request_actor = nil
end
def signature_params
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
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)
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
.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
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 }
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 }
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
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
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
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
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
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
@json = json.with_indifferent_access
end
- def verify_account!
+ def verify_actor!
return unless @json['signature'].is_a?(Hash)
type = @json['signature']['type']
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?
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,
}
end
end
+ def key_uri_for(target)
+ [uri_for(target), '#main-key'].join
+ end
+
def uri_for_username(username)
account_url(username: username)
end
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?
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
end
def headers
- (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
+ (@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
end
class << self
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
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
# 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)
-
- @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
+ actor = super
+ return actor if actor.nil? || actor.is_a?(Account)
- 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
--- /dev/null
+# 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
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?
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)
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
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?
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?
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
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
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
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)
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
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?
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
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
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
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)
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
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
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
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)
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
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
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
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))
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' })
end
context 'with signature account' do
- let(:signature_account) { Fabricate(:account) }
+ let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
end
context 'with signature account' do
- let(:signature_account) { Fabricate(:account) }
+ let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
stub_jsonld_contexts!
end
- describe '#verify_account!' do
+ describe '#verify_actor!' do
context 'when signature matches' do
let(:raw_signature) 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
let(:signature) { nil }
it 'returns nil' do
- expect(subject.verify_account!).to be_nil
+ expect(subject.verify_actor!).to be_nil
end
end
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
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
--- /dev/null
+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
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)
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)
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)
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