Skip to content
Snippets Groups Projects
Commit 00840f4f authored by Eugen Rochko's avatar Eugen Rochko Committed by GitHub
Browse files

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
parent 1cebfed2
No related branches found
No related tags found
No related merge requests found
Showing
with 140 additions and 28 deletions
...@@ -10,6 +10,7 @@ AllCops: ...@@ -10,6 +10,7 @@ AllCops:
- 'node_modules/**/*' - 'node_modules/**/*'
- 'Vagrantfile' - 'Vagrantfile'
- 'vendor/**/*' - 'vendor/**/*'
- 'lib/json_ld/*'
Bundler/OrderedGems: Bundler/OrderedGems:
Enabled: false Enabled: false
......
...@@ -68,6 +68,9 @@ gem 'tzinfo-data', '~> 1.2017' ...@@ -68,6 +68,9 @@ gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0' gem 'webpacker', '~> 2.0'
gem 'webpush' gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.16' gem 'fabrication', '~> 2.16'
gem 'fuubar', '~> 2.2' gem 'fuubar', '~> 2.2'
......
...@@ -179,6 +179,8 @@ GEM ...@@ -179,6 +179,8 @@ GEM
activesupport (>= 4.0.1) activesupport (>= 4.0.1)
hamlit (>= 1.2.0) hamlit (>= 1.2.0)
railties (>= 4.0.1) railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.5) hashdiff (0.3.5)
highline (1.7.8) highline (1.7.8)
hiredis (0.6.1) hiredis (0.6.1)
...@@ -211,6 +213,13 @@ GEM ...@@ -211,6 +213,13 @@ GEM
idn-ruby (0.1.0) idn-ruby (0.1.0)
jmespath (1.3.1) jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.5)
multi_json (~> 1.12)
rdf (~> 2.2)
json-ld-preloaded (2.2.1)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.1.3) jsonapi-renderer (0.1.3)
jwt (1.5.6) jwt (1.5.6)
kaminari (1.0.1) kaminari (1.0.1)
...@@ -348,6 +357,11 @@ GEM ...@@ -348,6 +357,11 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
rake (12.0.0) rake (12.0.0)
rdf (2.2.8)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
rdf (~> 2.0)
redis (3.3.3) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
...@@ -531,6 +545,7 @@ DEPENDENCIES ...@@ -531,6 +545,7 @@ DEPENDENCIES
httplog (~> 0.99) httplog (~> 0.99)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0) kaminari (~> 1.0)
letter_opener (~> 1.4) letter_opener (~> 1.4)
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
...@@ -560,6 +575,7 @@ DEPENDENCIES ...@@ -560,6 +575,7 @@ DEPENDENCIES
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0) rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3.1)
redis (~> 3.3) redis (~> 3.3)
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
......
...@@ -17,6 +17,11 @@ module JsonLdHelper ...@@ -17,6 +17,11 @@ module JsonLdHelper
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) !json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
end end
def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
graph.dump(:normalize)
end
def fetch_resource(uri) def fetch_resource(uri)
response = build_request(uri).perform response = build_request(uri).perform
return if response.code != 200 return if response.code != 200
...@@ -29,6 +34,14 @@ module JsonLdHelper ...@@ -29,6 +34,14 @@ module JsonLdHelper
nil nil
end end
def merge_context(context, new_context)
if context.is_a?(Array)
context << new_context
else
[context, new_context]
end
end
private private
def build_request(uri) def build_request(uri)
......
...@@ -11,7 +11,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base ...@@ -11,7 +11,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
def serializable_hash(options = nil) def serializable_hash(options = nil)
options = serialization_options(options) 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) self.class.transform_key_casing!(serialized_hash, instance_options)
end end
end end
# 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
...@@ -9,6 +9,8 @@ class ActivityPub::ProcessCollectionService < BaseService ...@@ -9,6 +9,8 @@ class ActivityPub::ProcessCollectionService < BaseService
return if @account.suspended? || !supported_context? return if @account.suspended? || !supported_context?
verify_account! if different_actor?
case @json['type'] case @json['type']
when 'Collection', 'CollectionPage' when 'Collection', 'CollectionPage'
process_items @json['items'] process_items @json['items']
...@@ -23,6 +25,10 @@ class ActivityPub::ProcessCollectionService < BaseService ...@@ -23,6 +25,10 @@ class ActivityPub::ProcessCollectionService < BaseService
private private
def different_actor?
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri && @json['signature'].present?
end
def process_items(items) def process_items(items)
items.reverse_each.map { |item| process_item(item) }.compact items.reverse_each.map { |item| process_item(item) }.compact
end end
...@@ -35,4 +41,9 @@ class ActivityPub::ProcessCollectionService < BaseService ...@@ -35,4 +41,9 @@ class ActivityPub::ProcessCollectionService < BaseService
activity = ActivityPub::Activity.factory(item, @account) activity = ActivityPub::Activity.factory(item, @account)
activity&.perform activity&.perform
end end
def verify_account!
account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
@account = account unless account.nil?
end
end end
...@@ -24,11 +24,11 @@ class AuthorizeFollowService < BaseService ...@@ -24,11 +24,11 @@ class AuthorizeFollowService < BaseService
end end
def build_json(follow_request) def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
follow_request, follow_request,
serializer: ActivityPub::AcceptFollowSerializer, serializer: ActivityPub::AcceptFollowSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(follow_request.target_account))
end end
def build_xml(follow_request) def build_xml(follow_request)
......
...@@ -138,10 +138,14 @@ class BatchedRemoveStatusService < BaseService ...@@ -138,10 +138,14 @@ class BatchedRemoveStatusService < BaseService
def build_json(status) def build_json(status)
return @activity_json[status.id] if @activity_json.key?(status.id) 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, status,
serializer: ActivityPub::DeleteSerializer, serializer: ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json)
end
def sign_json(status, json)
Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account))
end end
end end
...@@ -27,11 +27,11 @@ class BlockService < BaseService ...@@ -27,11 +27,11 @@ class BlockService < BaseService
end end
def build_json(block) def build_json(block)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
block, block,
serializer: ActivityPub::BlockSerializer, serializer: ActivityPub::BlockSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(block.account))
end end
def build_xml(block) def build_xml(block)
......
...@@ -34,11 +34,11 @@ class FavouriteService < BaseService ...@@ -34,11 +34,11 @@ class FavouriteService < BaseService
end end
def build_json(favourite) def build_json(favourite)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
favourite, favourite,
serializer: ActivityPub::LikeSerializer, serializer: ActivityPub::LikeSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(favourite.account))
end end
def build_xml(favourite) def build_xml(favourite)
......
...@@ -67,10 +67,10 @@ class FollowService < BaseService ...@@ -67,10 +67,10 @@ class FollowService < BaseService
end end
def build_json(follow_request) def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
follow_request, follow_request,
serializer: ActivityPub::FollowSerializer, serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(follow_request.account))
end end
end end
...@@ -47,11 +47,11 @@ class ProcessMentionsService < BaseService ...@@ -47,11 +47,11 @@ class ProcessMentionsService < BaseService
end end
def build_json(status) def build_json(status)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
status, status,
serializer: ActivityPub::ActivitySerializer, serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(status.account))
end end
def follow_remote_account_service def follow_remote_account_service
......
...@@ -42,10 +42,10 @@ class ReblogService < BaseService ...@@ -42,10 +42,10 @@ class ReblogService < BaseService
end end
def build_json(reblog) def build_json(reblog)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
reblog, reblog,
serializer: ActivityPub::ActivitySerializer, serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(reblog.account))
end end
end end
...@@ -19,11 +19,11 @@ class RejectFollowService < BaseService ...@@ -19,11 +19,11 @@ class RejectFollowService < BaseService
end end
def build_json(follow_request) def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
follow_request, follow_request,
serializer: ActivityPub::RejectFollowSerializer, serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(follow_request.target_account))
end end
def build_xml(follow_request) def build_xml(follow_request)
......
...@@ -56,7 +56,7 @@ class RemoveStatusService < BaseService ...@@ -56,7 +56,7 @@ class RemoveStatusService < BaseService
# ActivityPub # ActivityPub
ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:inbox_url)) do |inbox_url| 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
end end
...@@ -66,7 +66,7 @@ class RemoveStatusService < BaseService ...@@ -66,7 +66,7 @@ class RemoveStatusService < BaseService
# ActivityPub # ActivityPub
ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| 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
end end
...@@ -74,12 +74,16 @@ class RemoveStatusService < BaseService ...@@ -74,12 +74,16 @@ class RemoveStatusService < BaseService
@salmon_xml ||= stream_entry_to_xml(@stream_entry) @salmon_xml ||= stream_entry_to_xml(@stream_entry)
end end
def signed_activity_json
@signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account))
end
def activity_json def activity_json
@activity_json ||= ActiveModelSerializers::SerializableResource.new( @activity_json ||= ActiveModelSerializers::SerializableResource.new(
@status, @status,
serializer: ActivityPub::DeleteSerializer, serializer: ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json
end end
def remove_reblogs def remove_reblogs
......
...@@ -20,11 +20,11 @@ class UnblockService < BaseService ...@@ -20,11 +20,11 @@ class UnblockService < BaseService
end end
def build_json(unblock) def build_json(unblock)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
unblock, unblock,
serializer: ActivityPub::UndoBlockSerializer, serializer: ActivityPub::UndoBlockSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(unblock.account))
end end
def build_xml(block) def build_xml(block)
......
...@@ -21,11 +21,11 @@ class UnfavouriteService < BaseService ...@@ -21,11 +21,11 @@ class UnfavouriteService < BaseService
end end
def build_json(favourite) def build_json(favourite)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
favourite, favourite,
serializer: ActivityPub::UndoLikeSerializer, serializer: ActivityPub::UndoLikeSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(favourite.account))
end end
def build_xml(favourite) def build_xml(favourite)
......
...@@ -23,11 +23,11 @@ class UnfollowService < BaseService ...@@ -23,11 +23,11 @@ class UnfollowService < BaseService
end end
def build_json(follow) def build_json(follow)
ActiveModelSerializers::SerializableResource.new( Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
follow, follow,
serializer: ActivityPub::UndoFollowSerializer, serializer: ActivityPub::UndoFollowSerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json).sign!(follow.account))
end end
def build_xml(follow) def build_xml(follow)
......
...@@ -12,7 +12,7 @@ class ActivityPub::DistributionWorker ...@@ -12,7 +12,7 @@ class ActivityPub::DistributionWorker
return if skip_distribution? return if skip_distribution?
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[payload, @account.id, inbox_url] [signed_payload, @account.id, inbox_url]
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
...@@ -28,11 +28,15 @@ class ActivityPub::DistributionWorker ...@@ -28,11 +28,15 @@ class ActivityPub::DistributionWorker
@inboxes ||= @account.followers.inboxes @inboxes ||= @account.followers.inboxes
end end
def signed_payload
@signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
end
def payload def payload
@payload ||= ActiveModelSerializers::SerializableResource.new( @payload ||= ActiveModelSerializers::SerializableResource.new(
@status, @status,
serializer: ActivityPub::ActivitySerializer, serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).to_json ).as_json
end end
end end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment