From 459e6d3c94c983fbf66093bb613b712796d89027 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 2 Jul 2026 12:50:54 +0200 Subject: [PATCH] Refactor tests to not include private keys in database records for remote keypairs (#39682) --- app/models/keypair.rb | 6 +- spec/fabricators/account_fabricator.rb | 8 +-- spec/fabricators/keypair_fabricator.rb | 8 +-- .../activitypub/linked_data_signature_spec.rb | 4 +- spec/rails_helper.rb | 1 + spec/support/signed_request_helpers.rb | 5 +- spec/support/signing_keys_helpers.rb | 57 +++++++++++++++++++ 7 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 spec/support/signing_keys_helpers.rb diff --git a/app/models/keypair.rb b/app/models/keypair.rb index 4e2f3ce6c52..41fa74a0b32 100644 --- a/app/models/keypair.rb +++ b/app/models/keypair.rb @@ -31,8 +31,6 @@ class Keypair < ApplicationRecord ed25519: 1, }, validate: true - attr_accessor :require_private_key - validates :uri, presence: true, uniqueness: true, if: -> { account.remote? } validates :uri, absence: true, if: -> { account.local? } @@ -41,9 +39,7 @@ class Keypair < ApplicationRecord validates :public_key, presence: true validates :private_key, presence: true, if: -> { account.local? } - - # NOTE: this should be true in production, but tests heavily rely on remote accounts having a keypair - validates :private_key, absence: true, if: -> { account.remote? && !require_private_key } + validates :private_key, absence: true, if: -> { account.remote? } scope :unexpired, -> { where(expires_at: nil).or(where.not(expires_at: ..Time.now.utc)) } scope :usable, -> { unexpired.where(revoked: false) } diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb index bce8803be75..81b92861338 100644 --- a/spec/fabricators/account_fabricator.rb +++ b/spec/fabricators/account_fabricator.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true -keypair = OpenSSL::PKey::RSA.new(2048) -public_key = keypair.public_key.to_pem -private_key = keypair.to_pem - Fabricator(:account) do transient :suspended, :silenced username { sequence(:username) { |i| "#{Faker::Internet.user_name(separators: %w(_))}#{i}" } } last_webfingered_at { Time.now.utc } - public_key { public_key } - private_key { private_key } + public_key { SigningKeysHelpers::PUBLIC_RSA_TEST_KEY } + private_key { |attrs| attrs[:domain].present? ? nil : SigningKeysHelpers::PRIVATE_RSA_TEST_KEY } suspended_at { |attrs| attrs[:suspended] ? Time.now.utc : nil } silenced_at { |attrs| attrs[:silenced] ? Time.now.utc : nil } user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil } diff --git a/spec/fabricators/keypair_fabricator.rb b/spec/fabricators/keypair_fabricator.rb index a871f0dae1d..9a741a6651e 100644 --- a/spec/fabricators/keypair_fabricator.rb +++ b/spec/fabricators/keypair_fabricator.rb @@ -1,19 +1,15 @@ # frozen_string_literal: true -keypair = OpenSSL::PKey::RSA.new(2048) -public_key = keypair.public_key.to_pem -private_key = keypair.to_pem - Fabricator(:keypair) do account type :rsa - public_key public_key + public_key SigningKeysHelpers::PUBLIC_RSA_TEST_KEY expires_at nil revoked false after_build do |keypair| if keypair.account.local? - keypair.private_key ||= private_key + keypair.private_key ||= SigningKeysHelpers::PRIVATE_RSA_TEST_KEY keypair.local_fragment ||= "##{Random.hex}" else keypair.uri ||= ActivityPub::TagManager.instance.key_uri_for(keypair.account) diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 113cfcea9bd..4bdebfacefe 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -146,6 +146,8 @@ RSpec.describe ActivityPub::LinkedDataSignature do describe '#sign!' do subject { described_class.new(raw_json).sign!(sender) } + let(:sender) { Fabricate(:account) } + it 'returns a hash with a signature, the expected context, and the signature can be verified', :aggregate_failures do expect(subject).to be_a Hash expect(subject['signature']).to be_a Hash @@ -159,6 +161,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.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified)) + Base64.strict_encode64(private_key_from_keypair(from_actor.keypair).sign(OpenSSL::Digest.new('SHA256'), to_be_verified)) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 21b56c27cd9..393d968f00c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -88,6 +88,7 @@ RSpec.configure do |config| config.include Redisable config.include DomainHelpers config.include ThreadingHelpers + config.include SigningKeysHelpers config.include SignedRequestHelpers, type: :request config.include CommandLineHelpers, type: :cli config.include SystemHelpers, type: :system diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb index 25a71f5d5e3..7791d741484 100644 --- a/spec/support/signed_request_helpers.rb +++ b/spec/support/signed_request_helpers.rb @@ -12,7 +12,8 @@ module SignedRequestHelpers 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.keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + + signature = Base64.strict_encode64(private_key_from_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}\"" @@ -32,7 +33,7 @@ module SignedRequestHelpers 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.keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + signature = Base64.strict_encode64(private_key_from_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}\"" diff --git a/spec/support/signing_keys_helpers.rb b/spec/support/signing_keys_helpers.rb new file mode 100644 index 00000000000..12a2aafecd8 --- /dev/null +++ b/spec/support/signing_keys_helpers.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module SigningKeysHelpers + PUBLIC_RSA_TEST_KEY = <<~RSA + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuD7++C+IYBzC96Na27S5 + qqJhE6OSAe3/1r/hhGlvCl5a415Ma+wDDyHp8LcRI9fLcuyo+Hc0DNiBmjlNc9Oa + TDlhfx917cYV7bHbw3yT0OcwcavsgnZBd3GRVaMgY0thAuLtw7XgMnRy4i2steUJ + +anbsiC7F65gWsgvsD4W8Dk3Bmf5r+oDtgfo19t0NPsNM+pXtL1IKbBwnnyzkcO/ + f2kbSVvHgX7A9X33jca4Kgn1yyw/y5lYANKwi+Um8eEYXPVqWeYGc0/k8ZvcYS6B + w0XWHLG9kjQU/ApoKlaLshmU46sCDKkJKw0p58urOV9vtwaAbx+0NK9GaU7C5QSa + vQIDAQAB + -----END PUBLIC KEY----- + RSA + + PRIVATE_RSA_TEST_KEY = <<~RSA + -----BEGIN RSA PRIVATE KEY----- + MIIEpQIBAAKCAQEAuD7++C+IYBzC96Na27S5qqJhE6OSAe3/1r/hhGlvCl5a415M + a+wDDyHp8LcRI9fLcuyo+Hc0DNiBmjlNc9OaTDlhfx917cYV7bHbw3yT0Ocwcavs + gnZBd3GRVaMgY0thAuLtw7XgMnRy4i2steUJ+anbsiC7F65gWsgvsD4W8Dk3Bmf5 + r+oDtgfo19t0NPsNM+pXtL1IKbBwnnyzkcO/f2kbSVvHgX7A9X33jca4Kgn1yyw/ + y5lYANKwi+Um8eEYXPVqWeYGc0/k8ZvcYS6Bw0XWHLG9kjQU/ApoKlaLshmU46sC + DKkJKw0p58urOV9vtwaAbx+0NK9GaU7C5QSavQIDAQABAoIBABxnIbky4qwmYuv4 + E86g2qpyY9K6OYzwmqsJY4OdGVAY4ZwBcniEpqgTi1PfdNX4s1VhJF9BSRXd3oTe + 5pC/gx7TDbOiLvTbv4+oBn/pWYQvz6kGXuxxvH/kUwpHnnuQKEFgqFSuWgSNLRSv + A9v6lgIV7FdWcmEhMZttFuTtfW3EsTtL6Agjj6pNpREdgDQ4vu8LIMpTW9OOxee7 + Ui5oNSc+HNxsEo+Tb0pqfTuD6iztRIxLfBRS8M4EK9UTO82H4qYMb9HI6XfBshQ7 + 3hhh9tDvo2QgX4L4FW8z64kmNli4MdhG+HDC2HWqX+f/4GxZUsUPWTkUVCS/nw2O + JqYW+cECgYEA3ktP58HFbx3zfK1MdB2GjhyTMQqiP4VH50qj0VWvqEFdcP6nlMj4 + mhSwIhJJ2rzlclV0e5TAcHrWDx20Bpo72YfFORqMVIwXO5ZV9z3Xc83fu3KjUscv + 5UAghznN2Qc4CsxEI/I8/LMpLP2aua6RQPfQYk8LcdrafK7WBdkkF80CgYEA1C7J + 1xeIJHJxiLQ8g4g3BSPOP6MiM2qlQyapegWw7NA+sgG0MtTAddaZFkYrtkGZVvNF + lFQQCe9SWzW1pwZxHlmOm95PC9B89E+kzoEEUU/scXtP04krp4eUXbuBfTQ0aLU+ + QZ3FsUkedLVJ8UH8zanY/VnjhtCgXdsT6Mz+vrECgYEAiBs2xqE3QMzm67y0Jhh5 + 7YODgDCRnTD/EJf76816KxwymV/ivc+7n6PxIDtwavTjy/iUxKIUngooDMNUGgLP + iGaAFHGz4ISSKRLoeeSsiaRRS9VqOOHq6oQ0Jnf3GN45qyrcweGtA9Cy8nApD23a + VBwnxDm/uSuWQWdPde85ETUCgYEAycEjky6A+YcIhaA72iXviyecuc34e0NwmQVu + KOS4crUgqEoOejbqOiIvtopKjiaaE5+GDaBRD+FMQgY0D/mEHgOyImukZet8pSIF + 54WuAVMp1E4YfV/07ntwjB/65H57RwTviZznmceY+ghXotvH8hcKiPyr6Ej/876Y + k8g4gkECgYEAvN2GfwrLOgYaS3M0uldpOCHqod9zFABmzlmGn6+VLVkskFAA/RiC + NO+Pm38bh3FbxBhLXM4sTGHn157/WCvaCSUSsjGJCkOemhXOJ+zEvo6yqANrtGMt + f5zmr3d79JfG+WJbzZj7eO6LNbf2RHQKtuTLYNl0vmRpIcMaYJ8deNo= + -----END RSA PRIVATE KEY----- + RSA + + # Keypairs for remote actors typically do not include their private keys, + # but we need to sign requests in tests. So use the known test private key + # for known test public keys when testing. + def private_key_from_keypair(keypair) + case keypair.public_key + when PUBLIC_RSA_TEST_KEY + OpenSSL::PKey.read(PRIVATE_RSA_TEST_KEY) + else + OpenSSL::PKey.read(keypair.private_key) + end + end +end