def update_account
return if @account.uri != object_uri
- ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object)
+ ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true)
end
end
end
end
- def sign!(creator)
+ def sign!(creator, sign_with: nil)
options = {
'type' => 'RsaSignature2017',
'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
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(creator.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_signed))
+ signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest::SHA256.new, to_be_signed))
@json.merge('signature' => options.merge('signatureValue' => signature))
end
set_digest! if options.key?(:body)
end
- def on_behalf_of(account, key_id_format = :acct)
+ def on_behalf_of(account, key_id_format = :acct, sign_with: nil)
raise ArgumentError unless account.local?
@account = account
+ @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair
@key_id_format = key_id_format
self
def signature
algorithm = 'rsa-sha256'
- signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
+ signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers}\",signature=\"#{signature}\""
end
# Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain
- def call(username, domain, json)
+ def call(username, domain, json, options = {})
return if json['inbox'].blank? || unsupported_uri_scheme?(json['id'])
+ @options = options
@json = json
@uri = @json['id']
@username = username
return if @account.nil?
after_protocol_change! if protocol_changed?
- after_key_change! if key_changed?
+ after_key_change! if key_changed? && !@options[:signed_with_known_key]
check_featured_collection! if @account.featured_collection_url.present?
@account
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
- def perform(json, source_account_id, inbox_url)
+ def perform(json, source_account_id, inbox_url, options = {})
+ @options = options.with_indifferent_access
@json = json
@source_account = Account.find(source_account_id)
@inbox_url = inbox_url
def build_request
request = Request.new(:post, @inbox_url, body: @json)
- request.on_behalf_of(@source_account, :uri)
+ request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with])
request.add_headers(HEADERS)
end
sidekiq_options queue: 'push'
- def perform(account_id)
+ def perform(account_id, options = {})
+ @options = options.with_indifferent_access
@account = Account.find(account_id)
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
end
def signed_payload
- @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
+ @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account, sign_with: @options[:sign_with]))
end
def payload
require 'thor'
require_relative 'mastodon/media_cli'
require_relative 'mastodon/emoji_cli'
-
+require_relative 'mastodon/accounts_cli'
module Mastodon
class CLI < Thor
- desc 'media SUBCOMMAND ...ARGS', 'manage media files'
+ desc 'media SUBCOMMAND ...ARGS', 'Manage media files'
subcommand 'media', Mastodon::MediaCLI
- desc 'emoji SUBCOMMAND ...ARGS', 'manage custom emoji'
+ desc 'emoji SUBCOMMAND ...ARGS', 'Manage custom emoji'
subcommand 'emoji', Mastodon::EmojiCLI
+
+ desc 'accounts SUBCOMMAND ...ARGS', 'Manage accounts'
+ subcommand 'accounts', Mastodon::AccountsCLI
end
end
--- /dev/null
+# frozen_string_literal: true
+
+require 'rubygems/package'
+require_relative '../../config/boot'
+require_relative '../../config/environment'
+require_relative 'cli_helper'
+
+module Mastodon
+ class AccountsCLI < Thor
+ option :all, type: :boolean
+ desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
+ long_desc <<-LONG_DESC
+ Generate and broadcast new RSA keys as part of security
+ maintenance.
+
+ With the --all option, all local accounts will be subject
+ to the rotation. Otherwise, and by default, only a single
+ account specified by the USERNAME argument will be
+ processed.
+ LONG_DESC
+ def rotate(username = nil)
+ if options[:all]
+ processed = 0
+ delay = 0
+
+ Account.local.without_suspended.find_in_batches do |accounts|
+ accounts.each do |account|
+ rotate_keys_for_account(account, delay)
+ processed += 1
+ say('.', :green, false)
+ end
+
+ delay += 5.minutes
+ end
+
+ say
+ say("OK, rotated keys for #{processed} accounts", :green)
+ elsif username.present?
+ rotate_keys_for_account(Account.find_local(username))
+ say('OK', :green)
+ else
+ say('No account(s) given', :red)
+ end
+ end
+
+ private
+
+ def rotate_keys_for_account(account, delay = 0)
+ old_key = account.private_key
+ new_key = OpenSSL::PKey::RSA.new(2048).to_pem
+ account.update(private_key: new_key)
+ ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
+ end
+ end
+end
option :suffix
option :overwrite, type: :boolean
option :unlisted, type: :boolean
- desc 'import PATH', 'import emoji from a TAR archive at PATH'
+ desc 'import PATH', 'Import emoji from a TAR archive at PATH'
long_desc <<-LONG_DESC
Imports custom emoji from a TAR archive specified by PATH.
class MediaCLI < Thor
option :days, type: :numeric, default: 7
option :background, type: :boolean, default: false
- desc 'remove', 'remove remote media files'
+ desc 'remove', 'Remove remote media files'
long_desc <<-DESC
Removes locally cached copies of media attachments from other servers.