From 8a74aa589178ff54a1b2e32b7c84fc0069aa20a3 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Thu, 7 Sep 2023 15:04:39 -0500 Subject: [PATCH] Store keypairs as options keyed to user IDs. (#416) --- includes/class-signature.php | 133 ++++++++++++++---- includes/model/class-application-user.php | 40 ------ includes/model/class-blog-user.php | 42 ------ includes/model/class-user.php | 45 +----- ...typub-rest-post-signature-verification.php | 4 +- tests/test-class-activitypub-signature.php | 110 +++++++++++++++ 6 files changed, 217 insertions(+), 157 deletions(-) create mode 100644 tests/test-class-activitypub-signature.php diff --git a/includes/class-signature.php b/includes/class-signature.php index 3eb901c..20ba576 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -4,7 +4,6 @@ namespace Activitypub; use WP_Error; use DateTime; use DateTimeZone; -use Activitypub\Model\User; use Activitypub\Collection\Users; /** @@ -23,22 +22,14 @@ class Signature { * * @return mixed The public key. */ - public static function get_public_key( $user_id, $force = false ) { + public static function get_public_key_for( $user_id, $force = false ) { if ( $force ) { - self::generate_key_pair( $user_id ); + self::generate_key_pair_for( $user_id ); } - if ( User::APPLICATION_USER_ID === $user_id ) { - $key = \get_option( 'activitypub_magic_sig_public_key' ); - } else { - $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); - } + $key_pair = self::get_keypair_for( $user_id ); - if ( ! $key ) { - return self::get_public_key( $user_id, true ); - } - - return $key; + return $key_pair['public_key']; } /** @@ -49,22 +40,32 @@ class Signature { * * @return mixed The private key. */ - public static function get_private_key( $user_id, $force = false ) { + public static function get_private_key_for( $user_id, $force = false ) { if ( $force ) { - self::generate_key_pair( $user_id ); + self::generate_key_pair_for( $user_id ); } - if ( User::APPLICATION_USER_ID === $user_id ) { - $key = \get_option( 'activitypub_magic_sig_private_key' ); - } else { - $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); + $key_pair = self::get_keypair_for( $user_id ); + + return $key_pair['private_key']; + } + + /** + * Return the key pair for a given user. + * + * @param int $user_id The WordPress User ID. + * + * @return array The key pair. + */ + public static function get_keypair_for( $user_id ) { + $option_key = self::get_signature_options_key_for( $user_id ); + $key_pair = \get_option( $option_key ); + + if ( ! $key_pair ) { + $key_pair = self::generate_key_pair_for( $user_id ); } - if ( ! $key ) { - return self::get_private_key( $user_id, true ); - } - - return $key; + return $key_pair; } /** @@ -72,9 +73,18 @@ class Signature { * * @param int $user_id The WordPress User ID. * - * @return void + * @return array The key pair. */ - public static function generate_key_pair() { + protected static function generate_key_pair_for( $user_id ) { + $option_key = self::get_signature_options_key_for( $user_id ); + $key_pair = self::check_legacy_key_pair_for( $user_id ); + + if ( $key_pair ) { + \add_option( $option_key, $key_pair ); + + return $key_pair; + } + $config = array( 'digest_alg' => 'sha512', 'private_key_bits' => 2048, @@ -88,10 +98,76 @@ class Signature { $detail = \openssl_pkey_get_details( $key ); - return array( + // check if keys are valid + if ( + empty( $priv_key ) || ! is_string( $priv_key ) || + ! isset( $detail['key'] ) || ! is_string( $detail['key'] ) + ) { + return array( + 'private_key' => null, + 'public_key' => null, + ); + } + + $key_pair = array( 'private_key' => $priv_key, 'public_key' => $detail['key'], ); + + // persist keys + \add_option( $option_key, $key_pair ); + + return $key_pair; + } + + /** + * Undocumented function + * + * @param [type] $user_id + * @return void + */ + protected static function get_signature_options_key_for( $user_id ) { + $id = $user_id; + + if ( $user_id > 0 ) { + $user = \get_userdata( $user_id ); + $id = $user->user_login; + } + + return 'activitypub_keypair_for_' . $id; + } + + /** + * Check if there is a legacy key pair + * + * @param int $user_id The WordPress User ID. + * + * @return array|bool The key pair or false. + */ + protected static function check_legacy_key_pair_for( $user_id ) { + switch ( $user_id ) { + case 0: + $public_key = \get_option( 'activitypub_blog_user_public_key' ); + $private_key = \get_option( 'activitypub_blog_user_private_key' ); + break; + case -1: + $public_key = \get_option( 'activitypub_application_user_public_key' ); + $private_key = \get_option( 'activitypub_application_user_private_key' ); + break; + default: + $public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); + $private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); + break; + } + + if ( ! empty( $public_key ) && is_string( $public_key ) && ! empty( $private_key ) && is_string( $private_key ) ) { + return array( + 'private_key' => $private_key, + 'public_key' => $public_key, + ); + } + + return false; } /** @@ -107,7 +183,7 @@ class Signature { */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $user = Users::get_by_id( $user_id ); - $key = $user->get__private_key(); + $key = self::get_private_key_for( $user->get__id() ); $url_parts = \wp_parse_url( $url ); @@ -136,7 +212,6 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - $user = Users::get_by_id( $user_id ); $key_id = $user->get_url() . '#main-key'; if ( ! empty( $digest ) ) { diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 5537612..0042338 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -46,46 +46,6 @@ class Application_User extends Blog_User { return $this::get_name(); } - public function get__public_key() { - $key = \get_option( 'activitypub_application_user_public_key' ); - - if ( $key ) { - return $key; - } - - $this->generate_key_pair(); - - $key = \get_option( 'activitypub_application_user_public_key' ); - - return $key; - } - - /** - * @param int $user_id - * - * @return mixed - */ - public function get__private_key() { - $key = \get_option( 'activitypub_application_user_private_key' ); - - if ( $key ) { - return $key; - } - - $this->generate_key_pair(); - - return \get_option( 'activitypub_application_user_private_key' ); - } - - private function generate_key_pair() { - $key_pair = Signature::generate_key_pair(); - - if ( ! is_wp_error( $key_pair ) ) { - \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'] ); - \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'] ); - } - } - public function get_followers() { return null; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 14d087e..25b19e9 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -187,48 +187,6 @@ class Blog_User extends User { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } - public function get__public_key() { - $key = \get_option( 'activitypub_blog_user_public_key' ); - - if ( $key ) { - return $key; - } - - $this->generate_key_pair(); - - $key = \get_option( 'activitypub_blog_user_public_key' ); - - return $key; - } - - /** - * Get the User-Private-Key. - * - * @param int $user_id - * - * @return mixed - */ - public function get__private_key() { - $key = \get_option( 'activitypub_blog_user_private_key' ); - - if ( $key ) { - return $key; - } - - $this->generate_key_pair(); - - return \get_option( 'activitypub_blog_user_private_key' ); - } - - private function generate_key_pair() { - $key_pair = Signature::generate_key_pair(); - - if ( ! is_wp_error( $key_pair ) ) { - \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'] ); - \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'] ); - } - } - public function get_attachment() { return array(); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 0b9127d..8dfc575 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -159,53 +159,10 @@ class User extends Actor { return array( 'id' => $this->get_id() . '#main-key', 'owner' => $this->get_id(), - 'publicKeyPem' => $this->get__public_key(), + 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), ); } - /** - * @param int $this->get__id() - * - * @return mixed - */ - public function get__public_key() { - $key = \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); - - if ( $key ) { - return $key; - } - - $this->generate_key_pair(); - - return \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); - } - - /** - * @param int $this->get__id() - * - * @return mixed - */ - public function get__private_key() { - $key = \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); - - if ( $key ) { - return $key; - } - - $this->generate_key_pair(); - - return \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); - } - - private function generate_key_pair() { - $key_pair = Signature::generate_key_pair(); - - if ( ! is_wp_error( $key_pair ) ) { - \update_user_meta( $this->get__id(), 'magic_sig_public_key', $key_pair['public_key'], true ); - \update_user_meta( $this->get__id(), 'magic_sig_private_key', $key_pair['private_key'], true ); - } - } - /** * Returns the Inbox-API-Endpoint. * diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index fdca732..2d1c2f9 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -45,7 +45,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $user = Activitypub\Collection\Users::get_by_id( 1 ); - $public_key = $user->get__public_key(); + $public_key = Activitypub\Signature::get_public_key_for( $user->get__id() ); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; @@ -57,7 +57,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { $user = Activitypub\Collection\Users::get_by_id( 1 ); - $public_key = $user->get__public_key(); + $public_key = Activitypub\Signature::get_public_key_for( $user->get__id() ); // return ActivityPub Profile with signature return array( 'id' => $actor, diff --git a/tests/test-class-activitypub-signature.php b/tests/test-class-activitypub-signature.php new file mode 100644 index 0000000..0352163 --- /dev/null +++ b/tests/test-class-activitypub-signature.php @@ -0,0 +1,110 @@ +get__id() ); + $public_key = Activitypub\Signature::get_public_key_for( $user->get__id() ); + $private_key = Activitypub\Signature::get_private_key_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + } + + public function test_signature_legacy() { + // check user + $user = Activitypub\Collection\Users::get_by_id( 1 ); + + $public_key = 'public key ' . $user->get__id(); + $private_key = 'private key ' . $user->get__id(); + + update_user_meta( $user->get__id(), 'magic_sig_public_key', $public_key ); + update_user_meta( $user->get__id(), 'magic_sig_private_key', $private_key ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + + // check application user + $user = Activitypub\Collection\Users::get_by_id( -1 ); + + $public_key = 'public key ' . $user->get__id(); + $private_key = 'private key ' . $user->get__id(); + + add_option( 'activitypub_application_user_public_key', $public_key ); + add_option( 'activitypub_application_user_private_key', $private_key ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + + // check blog user + \define( 'ACTIVITYPUB_DISABLE_BLOG_USER', false ); + $user = Activitypub\Collection\Users::get_by_id( 0 ); + + $public_key = 'public key ' . $user->get__id(); + $private_key = 'private key ' . $user->get__id(); + + add_option( 'activitypub_blog_user_public_key', $public_key ); + add_option( 'activitypub_blog_user_private_key', $private_key ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + } + + public function test_signature_consistancy() { + // check user + $user = Activitypub\Collection\Users::get_by_id( 1 ); + + $public_key = 'public key ' . $user->get__id(); + $private_key = 'private key ' . $user->get__id(); + + update_user_meta( $user->get__id(), 'magic_sig_public_key', $public_key ); + update_user_meta( $user->get__id(), 'magic_sig_private_key', $private_key ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + + update_user_meta( $user->get__id(), 'magic_sig_public_key', $public_key . '-update' ); + update_user_meta( $user->get__id(), 'magic_sig_private_key', $private_key . '-update' ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + } + + public function test_signature_consistancy2() { + $user = Activitypub\Collection\Users::get_by_id( 1 ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + $public_key = Activitypub\Signature::get_public_key_for( $user->get__id() ); + $private_key = Activitypub\Signature::get_private_key_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + + update_user_meta( $user->get__id(), 'magic_sig_public_key', 'test' ); + update_user_meta( $user->get__id(), 'magic_sig_private_key', 'test' ); + + $key_pair = Activitypub\Signature::get_keypair_for( $user->get__id() ); + + $this->assertNotEmpty( $key_pair ); + $this->assertEquals( $key_pair['public_key'], $public_key ); + $this->assertEquals( $key_pair['private_key'], $private_key ); + } +}