Skip to content
Extraits de code Groupes Projets
Non vérifiée Valider cabdbb7f rédigé par Eugen Rochko's avatar Eugen Rochko Validation de GitHub
Parcourir les fichiers

Add CLI task for rotating keys (#8466)

* If an Update is signed with known key, skip re-following procedure

Because it means the remote actor did *not* lose their database

* Add CLI method for rotating keys

    bin/tootctl accounts rotate [USERNAME]

Generates a new RSA key per account and sends out an Update activity
signed with the old key.

* Key rotation: Space out Update fan-outs every 5 minutes per 1000 accounts

* Skip suspended accounts in key rotation
parent 8adf485c
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
......@@ -11,6 +11,6 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
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
......@@ -32,7 +32,7 @@ class ActivityPub::LinkedDataSignature
end
end
def sign!(creator)
def sign!(creator, sign_with: nil)
options = {
'type' => 'RsaSignature2017',
'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
......@@ -42,8 +42,9 @@ class ActivityPub::LinkedDataSignature
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
......
......@@ -22,10 +22,11 @@ class Request
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
......@@ -70,7 +71,7 @@ class Request
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
......
......@@ -5,9 +5,10 @@ class ActivityPub::ProcessAccountService < BaseService
# 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
......@@ -31,7 +32,7 @@ class ActivityPub::ProcessAccountService < BaseService
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
......
......@@ -10,7 +10,8 @@ class ActivityPub::DeliveryWorker
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
......@@ -27,7 +28,7 @@ class ActivityPub::DeliveryWorker
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
......
......@@ -5,7 +5,8 @@ class ActivityPub::UpdateDistributionWorker
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|
......@@ -26,7 +27,7 @@ class ActivityPub::UpdateDistributionWorker
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
......
......@@ -3,13 +3,16 @@
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
# 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
......@@ -13,7 +13,7 @@ module Mastodon
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.
......
......@@ -10,7 +10,7 @@ module Mastodon
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.
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter