Change code to use first usable `keypairs` record for local user for signing, if it exists

Falls back to `private_key` attribute otherwise
pull/37931/head
Claire 17 hours ago
parent afdaa33ad3
commit d62e75100c

@ -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'])

@ -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

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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}\""

Loading…
Cancel
Save