diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb index f6c4eeb90ed..3f1adb88f0a 100644 --- a/app/lib/activitypub/linked_data_signature.rb +++ b/app/lib/activitypub/linked_data_signature.rb @@ -33,18 +33,19 @@ class ActivityPub::LinkedDataSignature end def sign!(creator, sign_with: nil) + keypair = sign_with.presence || creator.keypair + options = { 'type' => 'RsaSignature2017', - 'creator' => ActivityPub::TagManager.instance.key_uri_for(creator), + 'creator' => keypair.uri, '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 - keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : creator.keypair - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_signed)) + signature = Base64.strict_encode64(keypair.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_signed)) # Mastodon's context is either an array or a single URL context_with_security = Array(@json['@context']) diff --git a/app/lib/request.rb b/app/lib/request.rb index 81e59fb2ec6..d60ce5dd1e1 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -100,9 +100,8 @@ class Request def on_behalf_of(actor, sign_with: nil) raise ArgumentError, 'actor must not be nil' if actor.nil? - key_id = ActivityPub::TagManager.instance.key_uri_for(actor) - keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair - @signing = HttpSignatureDraft.new(keypair, key_id) + keypair = sign_with.presence || actor.keypair + @signing = HttpSignatureDraft.new(keypair.keypair, keypair.uri) self end diff --git a/app/models/account.rb b/app/models/account.rb index a1bafc8fd6a..52403475e94 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -289,7 +289,7 @@ class Account < ApplicationRecord end def keypair - @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) + keypairs.usable.first || Keypair.from_legacy_account(self) end def tags_as_strings=(tag_names) diff --git a/app/models/keypair.rb b/app/models/keypair.rb index 80c313f4df7..ff643a54ec1 100644 --- a/app/models/keypair.rb +++ b/app/models/keypair.rb @@ -74,4 +74,21 @@ class Keypair < ApplicationRecord type: :rsa ) end + + def self.from_worker_arg(account, private_key_pem_or_hash) + if private_key_pem_or_hash.is_a?(String) + account.keypairs.build( + private_key: private_key_pem_or_hash, + uri: ActivityPub::TagManager.instance.key_uri_for(account), + type: :rsa + ) + else + account.keypairs.build( + private_key: private_key_pem_or_hash['private_key'], + public_key: private_key_pem_or_hash['public_key'], + uri: private_key_pem_or_hash['uri'], + type: private_key_pem_or_hash['type'] + ) + end + end end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index ff1a70104b8..6f3331fbb40 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -21,7 +21,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? } - has_one :public_key, serializer: ActivityPub::PublicKeySerializer + has_one :keypair, key: :public_key, serializer: ActivityPub::PublicKeySerializer has_many :virtual_tags, key: :tag has_many :virtual_attachments, key: :attachment diff --git a/app/serializers/activitypub/public_key_serializer.rb b/app/serializers/activitypub/public_key_serializer.rb index 8621517e7cd..5ad42d2e612 100644 --- a/app/serializers/activitypub/public_key_serializer.rb +++ b/app/serializers/activitypub/public_key_serializer.rb @@ -6,11 +6,11 @@ class ActivityPub::PublicKeySerializer < ActivityPub::Serializer attributes :id, :owner, :public_key_pem def id - ActivityPub::TagManager.instance.key_uri_for(object) + object.uri end def owner - ActivityPub::TagManager.instance.uri_for(object) + ActivityPub::TagManager.instance.uri_for(object.actor) end def public_key_pem diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 8cd39f700ca..26ecd0c4b3b 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -48,7 +48,7 @@ class ActivityPub::DeliveryWorker def build_request(http_client) Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request| - request.on_behalf_of(@source_account, sign_with: @options[:sign_with]) + request.on_behalf_of(@source_account, sign_with: sign_with) request.add_headers(HEADERS) request.add_headers({ 'Collection-Synchronization' => synchronization_header }) if ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] != 'true' && @options[:synchronize_followers] end @@ -93,4 +93,8 @@ class ActivityPub::DeliveryWorker def request_pool RequestPool.current end + + def sign_with + @options[:sign_with].presence && Keypair.from_worker_arg(@source_account, @options[:sign_with]) + end end diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb index 976f516498d..00823b4add1 100644 --- a/app/workers/activitypub/update_distribution_worker.rb +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -23,6 +23,10 @@ class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker end def payload - @payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with])) + @payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: sign_with)) + end + + def sign_with + @options[:sign_with].presence && Keypair.from_worker_arg(@account, @options[:sign_with]) end end diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb index 25e966bd8ea..7a31354c40d 100644 --- a/lib/mastodon/cli/accounts.rb +++ b/lib/mastodon/cli/accounts.rb @@ -617,10 +617,22 @@ module Mastodon::CLI def rotate_keys_for_account(account, delay = 0) fail_with_message 'No such account' if account.nil? - old_key = account.private_key + old_key = account.keypair new_key = OpenSSL::PKey::RSA.new(2048) - account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem) - ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, { 'sign_with' => old_key }) + + account.update(private_key: nil, public_key: '', keypairs: [account.keypairs.build(uri: ActivityPub::TagManager.instance.key_uri_for(account), type: :rsa, public_key: new_key.public_key.to_pem, private_key: new_key.to_pem)]) + + ActivityPub::UpdateDistributionWorker.perform_in( + delay, + account.id, + { + 'sign_with' => { + 'private_key' => old_key.private_key, + 'uri' => old_key.uri, + 'type' => old_key.type, + }, + } + ) end end end diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 7aaff9680e3..113cfcea9bd 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -159,6 +159,6 @@ RSpec.describe ActivityPub::LinkedDataSignature do 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_actor.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified)) + Base64.strict_encode64(from_actor.keypair.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified)) end end diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index 927c6ca8deb..54cf4549f1f 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -951,15 +951,15 @@ RSpec.describe Mastodon::CLI::Accounts do let(:arguments) { [account.username] } it 'correctly rotates keys for the specified account' do - old_private_key = account.private_key - old_public_key = account.public_key + old_private_key = account.keypair.private_key + old_public_key = account.keypair.public_key expect { subject } .to output_results('OK') account.reload - expect(account.private_key).to_not eq(old_private_key) - expect(account.public_key).to_not eq(old_public_key) + expect(account.keypair.private_key).to_not eq(old_private_key) + expect(account.keypair.public_key).to_not eq(old_public_key) end it 'broadcasts the new keys for the specified account' do @@ -986,15 +986,15 @@ RSpec.describe Mastodon::CLI::Accounts do let(:options) { { all: true } } it 'correctly rotates keys for all local accounts' do - old_private_keys = accounts.map(&:private_key) - old_public_keys = accounts.map(&:public_key) + old_private_keys = accounts.map { |account| account.keypair.private_key } + old_public_keys = accounts.map { |account| account.keypair.public_key } expect { subject } .to output_results('rotated') accounts.each(&:reload) - expect(accounts.map(&:private_key)).to_not eq(old_private_keys) - expect(accounts.map(&:public_key)).to_not eq(old_public_keys) + expect(accounts.map { |account| account.keypair.private_key }).to_not eq(old_private_keys) + expect(accounts.map { |account| account.keypair.public_key }).to_not eq(old_public_keys) end it 'broadcasts the new keys for each account' do diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index ca85b0fbfcd..33f546f717a 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -247,9 +247,10 @@ RSpec.describe Account do end describe '#keypair' do - it 'returns an RSA key pair' do + it 'returns a Keypair object with a RSA key pair' do account = Fabricate(:account) - expect(account.keypair).to be_instance_of OpenSSL::PKey::RSA + expect(account.keypair).to be_instance_of Keypair + expect(account.keypair.keypair).to be_instance_of OpenSSL::PKey::RSA end end @@ -754,7 +755,7 @@ RSpec.describe Account do expect(account) .to be_private_key .and be_public_key - expect(account.keypair) + expect(account.keypair.keypair) .to be_private .and be_public end @@ -764,7 +765,7 @@ RSpec.describe Account do it 'does not generate keys' do key = OpenSSL::PKey::RSA.new(1024).public_key account = described_class.create!(domain: 'remote', uri: 'https://remote/actor', username: 'remote_user_with_public', public_key: key.to_pem) - expect(account.keypair.params).to eq key.params + expect(account.keypair.keypair.params).to eq key.params end it 'normalizes domain' do diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb index a4423af748f..25a71f5d5e3 100644 --- a/spec/support/signed_request_helpers.rb +++ b/spec/support/signed_request_helpers.rb @@ -9,10 +9,10 @@ module SignedRequestHelpers headers['Host'] = Rails.configuration.x.local_domain signed_headers = headers.merge('(request-target)' => "get #{path}").slice('(request-target)', 'Host', 'Date') - key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) keypair = sign_with.keypair + key_id = keypair.uri signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + signature = Base64.strict_encode64(keypair.keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" @@ -29,10 +29,10 @@ module SignedRequestHelpers signed_headers = headers.merge('(request-target)' => "post #{path}").slice('(request-target)', 'Host', 'Date', 'Digest') - key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) keypair = sign_with.keypair + key_id = keypair.uri signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + signature = Base64.strict_encode64(keypair.keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""