Skip to content

Commit

Permalink
Add handling of Linked Data Signatures in payloads (mastodon#4687)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Gargron committed Aug 26, 2017
1 parent 0403e4d commit 95e14d6
Show file tree
Hide file tree
Showing 25 changed files with 369 additions and 30 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Expand Up @@ -10,6 +10,7 @@ AllCops:
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
- 'lib/json_ld/*'

Bundler/OrderedGems:
Enabled: false
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -68,6 +68,9 @@ gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0'
gem 'webpush'

gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'

group :development, :test do
gem 'fabrication', '~> 2.16'
gem 'fuubar', '~> 2.2'
Expand Down
16 changes: 16 additions & 0 deletions Gemfile.lock
Expand Up @@ -179,6 +179,8 @@ GEM
activesupport (>= 4.0.1)
hamlit (>= 1.2.0)
railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.5)
highline (1.7.8)
hiredis (0.6.1)
Expand Down Expand Up @@ -211,6 +213,13 @@ GEM
idn-ruby (0.1.0)
jmespath (1.3.1)
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)
jwt (1.5.6)
kaminari (1.0.1)
Expand Down Expand Up @@ -348,6 +357,11 @@ GEM
rainbow (2.2.2)
rake
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-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
Expand Down Expand Up @@ -531,6 +545,7 @@ DEPENDENCIES
httplog (~> 0.99)
i18n-tasks (~> 0.9)
idn-ruby
json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
Expand Down Expand Up @@ -560,6 +575,7 @@ DEPENDENCIES
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3.1)
redis (~> 3.3)
redis-namespace (~> 1.5)
redis-rails (~> 5.0)
Expand Down
13 changes: 13 additions & 0 deletions app/helpers/jsonld_helper.rb
Expand Up @@ -17,6 +17,11 @@ def supported_context?(json)
!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
Expand All @@ -29,6 +34,14 @@ def body_to_json(body)
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)
Expand Down
2 changes: 1 addition & 1 deletion app/lib/activitypub/adapter.rb
Expand Up @@ -11,7 +11,7 @@ def self.transform_key_casing!(value, _options)

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
56 changes: 56 additions & 0 deletions 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
11 changes: 11 additions & 0 deletions app/services/activitypub/process_collection_service.rb
Expand Up @@ -9,6 +9,8 @@ def call(body, account)

return if @account.suspended? || !supported_context?

verify_account! if different_actor?

case @json['type']
when 'Collection', 'CollectionPage'
process_items @json['items']
Expand All @@ -23,6 +25,10 @@ def call(body, account)

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
Expand All @@ -35,4 +41,9 @@ def process_item(item)
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
4 changes: 2 additions & 2 deletions app/services/authorize_follow_service.rb
Expand Up @@ -24,11 +24,11 @@ def create_notification(follow_request)
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)
Expand Down
8 changes: 6 additions & 2 deletions app/services/batched_remove_status_service.rb
Expand Up @@ -138,10 +138,14 @@ def redis
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
4 changes: 2 additions & 2 deletions app/services/block_service.rb
Expand Up @@ -27,11 +27,11 @@ def create_notification(block)
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)
Expand Down
4 changes: 2 additions & 2 deletions app/services/favourite_service.rb
Expand Up @@ -34,11 +34,11 @@ def create_notification(favourite)
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)
Expand Down
4 changes: 2 additions & 2 deletions app/services/follow_service.rb
Expand Up @@ -67,10 +67,10 @@ def build_follow_xml(follow)
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
4 changes: 2 additions & 2 deletions app/services/process_mentions_service.rb
Expand Up @@ -47,11 +47,11 @@ def create_notification(status, mention)
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
Expand Down
4 changes: 2 additions & 2 deletions app/services/reblog_service.rb
Expand Up @@ -42,10 +42,10 @@ def create_notification(reblog)
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
4 changes: 2 additions & 2 deletions app/services/reject_follow_service.rb
Expand Up @@ -19,11 +19,11 @@ def create_notification(follow_request)
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)
Expand Down
10 changes: 7 additions & 3 deletions app/services/remove_status_service.rb
Expand Up @@ -56,7 +56,7 @@ def remove_from_remote_affected

# 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

Expand All @@ -66,20 +66,24 @@ def remove_from_remote_followers

# 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

def salmon_xml
@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
Expand Down
4 changes: 2 additions & 2 deletions app/services/unblock_service.rb
Expand Up @@ -20,11 +20,11 @@ def create_notification(unblock)
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)
Expand Down
4 changes: 2 additions & 2 deletions app/services/unfavourite_service.rb
Expand Up @@ -21,11 +21,11 @@ def create_notification(favourite)
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)
Expand Down
4 changes: 2 additions & 2 deletions app/services/unfollow_service.rb
Expand Up @@ -23,11 +23,11 @@ def create_notification(follow)
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)
Expand Down

0 comments on commit 95e14d6

Please sign in to comment.