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
This commit is contained in:
		| @@ -10,6 +10,7 @@ AllCops: | ||||
|   - 'node_modules/**/*' | ||||
|   - 'Vagrantfile' | ||||
|   - 'vendor/**/*' | ||||
|   - 'lib/json_ld/*' | ||||
|  | ||||
| Bundler/OrderedGems: | ||||
|   Enabled: false | ||||
|   | ||||
							
								
								
									
										3
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -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' | ||||
|   | ||||
							
								
								
									
										16
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @@ -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) | ||||
| @@ -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) | ||||
| @@ -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) | ||||
| @@ -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) | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										56
									
								
								app/lib/activitypub/linked_data_signature.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/lib/activitypub/linked_data_signature.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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? | ||||
|  | ||||
|     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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										4
									
								
								config/initializers/json_ld.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								config/initializers/json_ld.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| require_relative '../../lib/json_ld/identity' | ||||
| require_relative '../../lib/json_ld/security' | ||||
							
								
								
									
										86
									
								
								lib/json_ld/identity.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/json_ld/identity.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										50
									
								
								lib/json_ld/security.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lib/json_ld/security.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										86
									
								
								spec/lib/activitypub/linked_data_signature_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								spec/lib/activitypub/linked_data_signature_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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' | ||||
|  | ||||
| RSpec.describe ActivityPub::ProcessCollectionService do | ||||
|   subject { ActivityPub::ProcessCollectionService.new } | ||||
|   subject { described_class.new } | ||||
|  | ||||
|   describe '#call' do | ||||
|     pending | ||||
|     context 'when actor is the sender' | ||||
|     context 'when actor differs from sender' | ||||
|   end | ||||
| end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user