From 00840f4f2edb8d1d46638ccbc90a1f4462d0867a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 26 Aug 2017 13:47:38 +0200 Subject: Add handling of Linked Data Signatures in payloads (#4687) * Add handling of Linked Data Signatures in payloads * Add a way to sign JSON, fix canonicalization of signature options * Fix signatureValue encoding, send out signed JSON when distributing * Add missing security context --- app/helpers/jsonld_helper.rb | 13 +++++ app/lib/activitypub/adapter.rb | 2 +- app/lib/activitypub/linked_data_signature.rb | 56 ++++++++++++++++++++++ .../activitypub/process_collection_service.rb | 11 +++++ app/services/authorize_follow_service.rb | 4 +- app/services/batched_remove_status_service.rb | 8 +++- app/services/block_service.rb | 4 +- app/services/favourite_service.rb | 4 +- app/services/follow_service.rb | 4 +- app/services/process_mentions_service.rb | 4 +- app/services/reblog_service.rb | 4 +- app/services/reject_follow_service.rb | 4 +- app/services/remove_status_service.rb | 10 ++-- app/services/unblock_service.rb | 4 +- app/services/unfavourite_service.rb | 4 +- app/services/unfollow_service.rb | 4 +- app/workers/activitypub/distribution_worker.rb | 8 +++- 17 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 app/lib/activitypub/linked_data_signature.rb (limited to 'app') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 8355eb055..09446c8be 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -17,6 +17,11 @@ module JsonLdHelper !json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) end + def canonicalize(json) + graph = RDF::Graph.new << JSON::LD::API.toRdf(json) + graph.dump(:normalize) + end + def fetch_resource(uri) response = build_request(uri).perform return if response.code != 200 @@ -29,6 +34,14 @@ module JsonLdHelper nil end + def merge_context(context, new_context) + if context.is_a?(Array) + context << new_context + else + [context, new_context] + end + end + private def build_request(uri) diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index df132f019..92210579e 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -11,7 +11,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base def serializable_hash(options = nil) options = serialization_options(options) - serialized_hash = { '@context': ActivityPub::TagManager::CONTEXT }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) + serialized_hash = { '@context': [ActivityPub::TagManager::CONTEXT, 'https://w3id.org/security/v1'] }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) self.class.transform_key_casing!(serialized_hash, instance_options) end end diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb new file mode 100644 index 000000000..7173aed19 --- /dev/null +++ b/app/lib/activitypub/linked_data_signature.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class ActivityPub::LinkedDataSignature + include JsonLdHelper + + CONTEXT = 'https://w3id.org/identity/v1' + + def initialize(json) + @json = json + end + + def verify_account! + return unless @json['signature'].is_a?(Hash) + + type = @json['signature']['type'] + creator_uri = @json['signature']['creator'] + signature = @json['signature']['signatureValue'] + + return unless type == 'RsaSignature2017' + + creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account) + creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri) + + return if creator.nil? + + options_hash = hash(@json['signature'].without('type', 'id', 'signatureValue').merge('@context' => CONTEXT)) + document_hash = hash(@json.without('signature')) + to_be_verified = options_hash + document_hash + + if creator.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), to_be_verified) + creator + end + end + + def sign!(creator) + options = { + 'type' => 'RsaSignature2017', + 'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join, + 'created' => Time.now.utc.iso8601, + } + + options_hash = hash(options.without('type', 'id', 'signatureValue').merge('@context' => CONTEXT)) + document_hash = hash(@json.without('signature')) + to_be_signed = options_hash + document_hash + + signature = Base64.strict_encode64(creator.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_signed)) + + @json.merge('@context' => merge_context(@json['@context'], CONTEXT), 'signature' => options.merge('signatureValue' => signature)) + end + + private + + def hash(obj) + Digest::SHA256.hexdigest(canonicalize(obj)) + end +end diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index cd861c075..2cf15553d 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -9,6 +9,8 @@ class ActivityPub::ProcessCollectionService < BaseService return if @account.suspended? || !supported_context? + verify_account! if different_actor? + case @json['type'] when 'Collection', 'CollectionPage' process_items @json['items'] @@ -23,6 +25,10 @@ class ActivityPub::ProcessCollectionService < BaseService private + def different_actor? + @json['actor'].present? && value_or_id(@json['actor']) != @account.uri && @json['signature'].present? + end + def process_items(items) items.reverse_each.map { |item| process_item(item) }.compact end @@ -35,4 +41,9 @@ class ActivityPub::ProcessCollectionService < BaseService activity = ActivityPub::Activity.factory(item, @account) activity&.perform end + + def verify_account! + account = ActivityPub::LinkedDataSignature.new(@json).verify_account! + @account = account unless account.nil? + end end diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 6f036dc5a..b1bff8962 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -24,11 +24,11 @@ class AuthorizeFollowService < BaseService end def build_json(follow_request) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( follow_request, serializer: ActivityPub::AcceptFollowSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(follow_request.target_account)) end def build_xml(follow_request) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index e6c8c9208..c90f4401d 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -138,10 +138,14 @@ class BatchedRemoveStatusService < BaseService def build_json(status) return @activity_json[status.id] if @activity_json.key?(status.id) - @activity_json[status.id] = ActiveModelSerializers::SerializableResource.new( + @activity_json[status.id] = sign_json(status, ActiveModelSerializers::SerializableResource.new( status, serializer: ActivityPub::DeleteSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json) + end + + def sign_json(status, json) + Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account)) end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index f2253226b..b39c3eef2 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -27,11 +27,11 @@ class BlockService < BaseService end def build_json(block) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( block, serializer: ActivityPub::BlockSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(block.account)) end def build_xml(block) diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 4aa935170..44df3ed13 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -34,11 +34,11 @@ class FavouriteService < BaseService end def build_json(favourite) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( favourite, serializer: ActivityPub::LikeSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(favourite.account)) end def build_xml(favourite) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 2be625cd8..a92eb6b88 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -67,10 +67,10 @@ class FollowService < BaseService end def build_json(follow_request) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( follow_request, serializer: ActivityPub::FollowSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(follow_request.account)) end end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 2b8a77147..f123bf869 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -47,11 +47,11 @@ class ProcessMentionsService < BaseService end def build_json(status) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(status.account)) end def follow_remote_account_service diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 7f886af7c..5ed16c64b 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -42,10 +42,10 @@ class ReblogService < BaseService end def build_json(reblog) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( reblog, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(reblog.account)) end end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index a91266aa4..c1f7bcb60 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -19,11 +19,11 @@ class RejectFollowService < BaseService end def build_json(follow_request) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( follow_request, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(follow_request.target_account)) end def build_xml(follow_request) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index fcccbaa24..62eea677f 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -56,7 +56,7 @@ class RemoveStatusService < BaseService # ActivityPub ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:inbox_url)) do |inbox_url| - [activity_json, @account.id, inbox_url] + [signed_activity_json, @account.id, inbox_url] end end @@ -66,7 +66,7 @@ class RemoveStatusService < BaseService # ActivityPub ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| - [activity_json, @account.id, inbox_url] + [signed_activity_json, @account.id, inbox_url] end end @@ -74,12 +74,16 @@ class RemoveStatusService < BaseService @salmon_xml ||= stream_entry_to_xml(@stream_entry) end + def signed_activity_json + @signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account)) + end + def activity_json @activity_json ||= ActiveModelSerializers::SerializableResource.new( @status, serializer: ActivityPub::DeleteSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json end def remove_reblogs diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index 72fc5ab15..869f62d1c 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -20,11 +20,11 @@ class UnblockService < BaseService end def build_json(unblock) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( unblock, serializer: ActivityPub::UndoBlockSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(unblock.account)) end def build_xml(block) diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index e53798e66..2fda11bd6 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -21,11 +21,11 @@ class UnfavouriteService < BaseService end def build_json(favourite) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( favourite, serializer: ActivityPub::UndoLikeSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(favourite.account)) end def build_xml(favourite) diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 10af75146..bf151ee28 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -23,11 +23,11 @@ class UnfollowService < BaseService end def build_json(follow) - ActiveModelSerializers::SerializableResource.new( + Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( follow, serializer: ActivityPub::UndoFollowSerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json).sign!(follow.account)) end def build_xml(follow) diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 004dd25d1..14bb933c0 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -12,7 +12,7 @@ class ActivityPub::DistributionWorker return if skip_distribution? ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| - [payload, @account.id, inbox_url] + [signed_payload, @account.id, inbox_url] end rescue ActiveRecord::RecordNotFound true @@ -28,11 +28,15 @@ class ActivityPub::DistributionWorker @inboxes ||= @account.followers.inboxes end + def signed_payload + @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account)) + end + def payload @payload ||= ActiveModelSerializers::SerializableResource.new( @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter - ).to_json + ).as_json end end -- cgit