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 contextgh/stable
parent
1cebfed23e
commit
00840f4f2e
|
@ -10,6 +10,7 @@ AllCops:
|
||||||
- 'node_modules/**/*'
|
- 'node_modules/**/*'
|
||||||
- 'Vagrantfile'
|
- 'Vagrantfile'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
|
- 'lib/json_ld/*'
|
||||||
|
|
||||||
Bundler/OrderedGems:
|
Bundler/OrderedGems:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -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'
|
||||||
|
|
16
Gemfile.lock
16
Gemfile.lock
|
@ -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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
!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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# 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
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
@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,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../../lib/json_ld/identity'
|
||||||
|
require_relative '../../lib/json_ld/security'
|
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# This file generated automatically from https://w3id.org/identity/v1
|
||||||
|
require 'json/ld'
|
||||||
|
class JSON::LD::Context
|
||||||
|
add_preloaded("https://w3id.org/identity/v1") do
|
||||||
|
new(processingMode: "json-ld-1.0", term_definitions: {
|
||||||
|
"Credential" => TermDefinition.new("Credential", id: "https://w3id.org/credentials#Credential", simple: true),
|
||||||
|
"CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true),
|
||||||
|
"CryptographicKeyCredential" => TermDefinition.new("CryptographicKeyCredential", id: "https://w3id.org/credentials#CryptographicKeyCredential", simple: true),
|
||||||
|
"EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true),
|
||||||
|
"GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true),
|
||||||
|
"Group" => TermDefinition.new("Group", id: "https://www.w3.org/ns/activitystreams#Group", simple: true),
|
||||||
|
"Identity" => TermDefinition.new("Identity", id: "https://w3id.org/identity#Identity", simple: true),
|
||||||
|
"LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true),
|
||||||
|
"Organization" => TermDefinition.new("Organization", id: "http://schema.org/Organization", simple: true),
|
||||||
|
"Person" => TermDefinition.new("Person", id: "http://schema.org/Person", simple: true),
|
||||||
|
"PostalAddress" => TermDefinition.new("PostalAddress", id: "http://schema.org/PostalAddress", simple: true),
|
||||||
|
"about" => TermDefinition.new("about", id: "http://schema.org/about", type_mapping: "@id"),
|
||||||
|
"accessControl" => TermDefinition.new("accessControl", id: "https://w3id.org/permissions#accessControl", type_mapping: "@id"),
|
||||||
|
"address" => TermDefinition.new("address", id: "http://schema.org/address", type_mapping: "@id"),
|
||||||
|
"addressCountry" => TermDefinition.new("addressCountry", id: "http://schema.org/addressCountry", simple: true),
|
||||||
|
"addressLocality" => TermDefinition.new("addressLocality", id: "http://schema.org/addressLocality", simple: true),
|
||||||
|
"addressRegion" => TermDefinition.new("addressRegion", id: "http://schema.org/addressRegion", simple: true),
|
||||||
|
"cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true),
|
||||||
|
"cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true),
|
||||||
|
"cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true),
|
||||||
|
"claim" => TermDefinition.new("claim", id: "https://w3id.org/credentials#claim", type_mapping: "@id"),
|
||||||
|
"comment" => TermDefinition.new("comment", id: "http://www.w3.org/2000/01/rdf-schema#comment", simple: true),
|
||||||
|
"created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"),
|
||||||
|
"cred" => TermDefinition.new("cred", id: "https://w3id.org/credentials#", simple: true, prefix: true),
|
||||||
|
"credential" => TermDefinition.new("credential", id: "https://w3id.org/credentials#credential", type_mapping: "@id"),
|
||||||
|
"dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true),
|
||||||
|
"description" => TermDefinition.new("description", id: "http://schema.org/description", simple: true),
|
||||||
|
"digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true),
|
||||||
|
"digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true),
|
||||||
|
"domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true),
|
||||||
|
"email" => TermDefinition.new("email", id: "http://schema.org/email", simple: true),
|
||||||
|
"expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"familyName" => TermDefinition.new("familyName", id: "http://schema.org/familyName", simple: true),
|
||||||
|
"givenName" => TermDefinition.new("givenName", id: "http://schema.org/givenName", simple: true),
|
||||||
|
"id" => TermDefinition.new("id", id: "@id", simple: true),
|
||||||
|
"identity" => TermDefinition.new("identity", id: "https://w3id.org/identity#", simple: true, prefix: true),
|
||||||
|
"identityService" => TermDefinition.new("identityService", id: "https://w3id.org/identity#identityService", type_mapping: "@id"),
|
||||||
|
"idp" => TermDefinition.new("idp", id: "https://w3id.org/identity#idp", type_mapping: "@id"),
|
||||||
|
"image" => TermDefinition.new("image", id: "http://schema.org/image", type_mapping: "@id"),
|
||||||
|
"initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true),
|
||||||
|
"issued" => TermDefinition.new("issued", id: "https://w3id.org/credentials#issued", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"issuer" => TermDefinition.new("issuer", id: "https://w3id.org/credentials#issuer", type_mapping: "@id"),
|
||||||
|
"label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true),
|
||||||
|
"member" => TermDefinition.new("member", id: "http://schema.org/member", type_mapping: "@id"),
|
||||||
|
"memberOf" => TermDefinition.new("memberOf", id: "http://schema.org/memberOf", type_mapping: "@id"),
|
||||||
|
"name" => TermDefinition.new("name", id: "http://schema.org/name", simple: true),
|
||||||
|
"nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true),
|
||||||
|
"normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true),
|
||||||
|
"owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"),
|
||||||
|
"password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true),
|
||||||
|
"paymentProcessor" => TermDefinition.new("paymentProcessor", id: "https://w3id.org/payswarm#processor", simple: true),
|
||||||
|
"perm" => TermDefinition.new("perm", id: "https://w3id.org/permissions#", simple: true, prefix: true),
|
||||||
|
"postalCode" => TermDefinition.new("postalCode", id: "http://schema.org/postalCode", simple: true),
|
||||||
|
"preferences" => TermDefinition.new("preferences", id: "https://w3id.org/payswarm#preferences", type_mapping: "@vocab"),
|
||||||
|
"privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"),
|
||||||
|
"privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true),
|
||||||
|
"ps" => TermDefinition.new("ps", id: "https://w3id.org/payswarm#", simple: true, prefix: true),
|
||||||
|
"publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"),
|
||||||
|
"publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true),
|
||||||
|
"publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"),
|
||||||
|
"rdf" => TermDefinition.new("rdf", id: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", simple: true, prefix: true),
|
||||||
|
"rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true),
|
||||||
|
"recipient" => TermDefinition.new("recipient", id: "https://w3id.org/credentials#recipient", type_mapping: "@id"),
|
||||||
|
"revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"schema" => TermDefinition.new("schema", id: "http://schema.org/", simple: true, prefix: true),
|
||||||
|
"sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true),
|
||||||
|
"signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true),
|
||||||
|
"signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signatureAlgorithm", simple: true),
|
||||||
|
"signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true),
|
||||||
|
"streetAddress" => TermDefinition.new("streetAddress", id: "http://schema.org/streetAddress", simple: true),
|
||||||
|
"title" => TermDefinition.new("title", id: "http://purl.org/dc/terms/title", simple: true),
|
||||||
|
"type" => TermDefinition.new("type", id: "@type", simple: true),
|
||||||
|
"url" => TermDefinition.new("url", id: "http://schema.org/url", type_mapping: "@id"),
|
||||||
|
"writePermission" => TermDefinition.new("writePermission", id: "https://w3id.org/permissions#writePermission", type_mapping: "@id"),
|
||||||
|
"xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# This file generated automatically from https://w3id.org/security/v1
|
||||||
|
require 'json/ld'
|
||||||
|
class JSON::LD::Context
|
||||||
|
add_preloaded("https://w3id.org/security/v1") do
|
||||||
|
new(processingMode: "json-ld-1.0", term_definitions: {
|
||||||
|
"CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true),
|
||||||
|
"EcdsaKoblitzSignature2016" => TermDefinition.new("EcdsaKoblitzSignature2016", id: "https://w3id.org/security#EcdsaKoblitzSignature2016", simple: true),
|
||||||
|
"EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true),
|
||||||
|
"GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true),
|
||||||
|
"LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true),
|
||||||
|
"LinkedDataSignature2016" => TermDefinition.new("LinkedDataSignature2016", id: "https://w3id.org/security#LinkedDataSignature2016", simple: true),
|
||||||
|
"authenticationTag" => TermDefinition.new("authenticationTag", id: "https://w3id.org/security#authenticationTag", simple: true),
|
||||||
|
"canonicalizationAlgorithm" => TermDefinition.new("canonicalizationAlgorithm", id: "https://w3id.org/security#canonicalizationAlgorithm", simple: true),
|
||||||
|
"cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true),
|
||||||
|
"cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true),
|
||||||
|
"cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true),
|
||||||
|
"created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"),
|
||||||
|
"dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true),
|
||||||
|
"digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true),
|
||||||
|
"digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true),
|
||||||
|
"domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true),
|
||||||
|
"encryptionKey" => TermDefinition.new("encryptionKey", id: "https://w3id.org/security#encryptionKey", simple: true),
|
||||||
|
"expiration" => TermDefinition.new("expiration", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"id" => TermDefinition.new("id", id: "@id", simple: true),
|
||||||
|
"initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true),
|
||||||
|
"iterationCount" => TermDefinition.new("iterationCount", id: "https://w3id.org/security#iterationCount", simple: true),
|
||||||
|
"nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true),
|
||||||
|
"normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true),
|
||||||
|
"owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"),
|
||||||
|
"password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true),
|
||||||
|
"privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"),
|
||||||
|
"privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true),
|
||||||
|
"publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"),
|
||||||
|
"publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true),
|
||||||
|
"publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"),
|
||||||
|
"revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
|
||||||
|
"salt" => TermDefinition.new("salt", id: "https://w3id.org/security#salt", simple: true),
|
||||||
|
"sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true),
|
||||||
|
"signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true),
|
||||||
|
"signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signingAlgorithm", simple: true),
|
||||||
|
"signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true),
|
||||||
|
"type" => TermDefinition.new("type", id: "@type", simple: true),
|
||||||
|
"xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,86 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::LinkedDataSignature do
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
let!(:sender) { Fabricate(:account, uri: 'http://example.com/alice') }
|
||||||
|
|
||||||
|
let(:raw_json) do
|
||||||
|
{
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id' => 'http://example.com/hello-world',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:json) { raw_json.merge('signature' => signature) }
|
||||||
|
|
||||||
|
subject { described_class.new(json) }
|
||||||
|
|
||||||
|
describe '#verify_account!' do
|
||||||
|
context 'when signature matches' do
|
||||||
|
let(:raw_signature) do
|
||||||
|
{
|
||||||
|
'creator' => 'http://example.com/alice',
|
||||||
|
'created' => '2017-09-23T20:21:34Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when signature is missing' do
|
||||||
|
let(:signature) { nil }
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(subject.verify_account!).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when signature is tampered' do
|
||||||
|
let(:raw_signature) do
|
||||||
|
{
|
||||||
|
'creator' => 'http://example.com/alice',
|
||||||
|
'created' => '2017-09-23T20:21:34Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(subject.verify_account!).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sign!' do
|
||||||
|
subject { described_class.new(raw_json).sign!(sender) }
|
||||||
|
|
||||||
|
it 'returns a hash' do
|
||||||
|
expect(subject).to be_a Hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains signature context' do
|
||||||
|
expect(subject['@context']).to include('https://www.w3.org/ns/activitystreams', 'https://w3id.org/identity/v1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains signature' do
|
||||||
|
expect(subject['signature']).to be_a Hash
|
||||||
|
expect(subject['signature']['signatureValue']).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can be verified again' do
|
||||||
|
expect(described_class.new(subject).verify_account!).to eq sender
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign(from_account, 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::SHA256.new, to_be_verified))
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,9 +1,10 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ActivityPub::ProcessCollectionService do
|
RSpec.describe ActivityPub::ProcessCollectionService do
|
||||||
subject { ActivityPub::ProcessCollectionService.new }
|
subject { described_class.new }
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
pending
|
context 'when actor is the sender'
|
||||||
|
context 'when actor differs from sender'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Reference in New Issue