@@ -745,4 +745,181 @@ public function test_set_rfc9421_unsupported() {
745745 \delete_option ( 'activitypub_rfc9421_signature ' );
746746 \remove_filter ( 'pre_http_request ' , $ mock_callback );
747747 }
748+
749+ /**
750+ * Test Ed25519 signature verification via activitypub_pre_get_public_key filter.
751+ *
752+ * @covers ::verify_http_signature
753+ * @covers \Activitypub\Signature\Http_Message_Signature::verify_ed25519_signature
754+ */
755+ public function test_ed25519_signature_verification () {
756+ // Generate Ed25519 keypair.
757+ $ keypair = \sodium_crypto_sign_keypair ();
758+ $ public_key = \sodium_crypto_sign_publickey ( $ keypair );
759+ $ private_key = \sodium_crypto_sign_secretkey ( $ keypair );
760+
761+ // Create signature base string.
762+ $ date = \gmdate ( 'D, d M Y H:i:s T ' );
763+ $ created = \time ();
764+ $ params_string = \sprintf (
765+ '("@method" "@target-uri" "date");created=%d;keyid="test-fasp-server-id" ' ,
766+ $ created
767+ );
768+ $ signature_base = "\"@method \": POST \n" ;
769+ $ signature_base .= "\"@target-uri \": https://example.org/wp-json/activitypub/1.0/fasp/capabilities/test/1/activation \n" ;
770+ $ signature_base .= "\"date \": $ date \n" ;
771+ $ signature_base .= "\"@signature-params \": $ params_string " ;
772+
773+ // Sign with Ed25519.
774+ $ signature = \sodium_crypto_sign_detached ( $ signature_base , $ private_key );
775+
776+ // Create signature headers.
777+ $ signature_input = "sig= $ params_string " ;
778+ $ signature_header = 'sig=: ' . \base64_encode ( $ signature ) . ': ' ;
779+
780+ // Mock the public key retrieval to return Ed25519 key.
781+ $ mock_ed25519_key = function ( $ key , $ key_id ) use ( $ public_key ) {
782+ if ( 'test-fasp-server-id ' === $ key_id ) {
783+ return array (
784+ 'type ' => 'ed25519 ' ,
785+ 'key ' => $ public_key ,
786+ );
787+ }
788+ return $ key ;
789+ };
790+ \add_filter ( 'activitypub_pre_get_public_key ' , $ mock_ed25519_key , 10 , 2 );
791+
792+ $ _SERVER ['REQUEST_METHOD ' ] = 'POST ' ;
793+ $ _SERVER ['REQUEST_URI ' ] = '/ ' . \rest_get_url_prefix () . '/ ' . ACTIVITYPUB_REST_NAMESPACE . '/fasp/capabilities/test/1/activation ' ;
794+ $ _SERVER ['HTTP_HOST ' ] = 'example.org ' ;
795+ $ _SERVER ['HTTPS ' ] = 'on ' ;
796+
797+ // Create REST request with Ed25519 signature.
798+ $ request = new \WP_REST_Request ( 'POST ' , ACTIVITYPUB_REST_NAMESPACE . '/fasp/capabilities/test/1/activation ' );
799+ $ request ->set_header ( 'Date ' , $ date );
800+ $ request ->set_header ( 'Host ' , 'example.org ' );
801+ $ request ->set_header ( 'Signature-Input ' , $ signature_input );
802+ $ request ->set_header ( 'Signature ' , $ signature_header );
803+
804+ // Verification should succeed.
805+ $ result = Signature::verify_http_signature ( $ request );
806+ $ this ->assertTrue ( $ result , 'Valid Ed25519 signature should verify ' );
807+
808+ \remove_filter ( 'activitypub_pre_get_public_key ' , $ mock_ed25519_key );
809+ }
810+
811+ /**
812+ * Test Ed25519 signature verification fails with invalid signature.
813+ *
814+ * @covers ::verify_http_signature
815+ * @covers \Activitypub\Signature\Http_Message_Signature::verify_ed25519_signature
816+ */
817+ public function test_ed25519_invalid_signature_fails () {
818+ // Generate Ed25519 keypair.
819+ $ keypair = \sodium_crypto_sign_keypair ();
820+ $ public_key = \sodium_crypto_sign_publickey ( $ keypair );
821+
822+ // Create a different keypair to sign with (simulates wrong key).
823+ $ wrong_keypair = \sodium_crypto_sign_keypair ();
824+ $ wrong_secret_key = \sodium_crypto_sign_secretkey ( $ wrong_keypair );
825+
826+ // Create signature base string.
827+ $ date = \gmdate ( 'D, d M Y H:i:s T ' );
828+ $ created = \time ();
829+ $ params_string = \sprintf (
830+ '("@method" "@target-uri" "date");created=%d;keyid="test-fasp-server-id" ' ,
831+ $ created
832+ );
833+ $ signature_base = "\"@method \": POST \n" ;
834+ $ signature_base .= "\"@target-uri \": https://example.org/wp-json/activitypub/1.0/fasp/capabilities/test/1/activation \n" ;
835+ $ signature_base .= "\"date \": $ date \n" ;
836+ $ signature_base .= "\"@signature-params \": $ params_string " ;
837+
838+ // Sign with WRONG key.
839+ $ signature = \sodium_crypto_sign_detached ( $ signature_base , $ wrong_secret_key );
840+
841+ // Create signature headers.
842+ $ signature_input = "sig= $ params_string " ;
843+ $ signature_header = 'sig=: ' . \base64_encode ( $ signature ) . ': ' ;
844+
845+ // Mock the public key retrieval to return the CORRECT public key.
846+ $ mock_ed25519_key = function ( $ key , $ key_id ) use ( $ public_key ) {
847+ if ( 'test-fasp-server-id ' === $ key_id ) {
848+ return array (
849+ 'type ' => 'ed25519 ' ,
850+ 'key ' => $ public_key ,
851+ );
852+ }
853+ return $ key ;
854+ };
855+ \add_filter ( 'activitypub_pre_get_public_key ' , $ mock_ed25519_key , 10 , 2 );
856+
857+ $ _SERVER ['REQUEST_METHOD ' ] = 'POST ' ;
858+ $ _SERVER ['REQUEST_URI ' ] = '/ ' . \rest_get_url_prefix () . '/ ' . ACTIVITYPUB_REST_NAMESPACE . '/fasp/capabilities/test/1/activation ' ;
859+ $ _SERVER ['HTTP_HOST ' ] = 'example.org ' ;
860+ $ _SERVER ['HTTPS ' ] = 'on ' ;
861+
862+ // Create REST request with Ed25519 signature (signed with wrong key).
863+ $ request = new \WP_REST_Request ( 'POST ' , ACTIVITYPUB_REST_NAMESPACE . '/fasp/capabilities/test/1/activation ' );
864+ $ request ->set_header ( 'Date ' , $ date );
865+ $ request ->set_header ( 'Host ' , 'example.org ' );
866+ $ request ->set_header ( 'Signature-Input ' , $ signature_input );
867+ $ request ->set_header ( 'Signature ' , $ signature_header );
868+
869+ // Verification should fail.
870+ $ result = Signature::verify_http_signature ( $ request );
871+ $ this ->assertWPError ( $ result , 'Invalid Ed25519 signature should fail verification ' );
872+ $ this ->assertEquals ( 'activitypub_signature ' , $ result ->get_error_code () );
873+
874+ \remove_filter ( 'activitypub_pre_get_public_key ' , $ mock_ed25519_key );
875+ }
876+
877+ /**
878+ * Test Ed25519 signature verification fails with invalid key length.
879+ *
880+ * @covers ::verify_http_signature
881+ * @covers \Activitypub\Signature\Http_Message_Signature::verify_ed25519_signature
882+ */
883+ public function test_ed25519_invalid_key_length_fails () {
884+ // Create signature headers with dummy values.
885+ $ date = \gmdate ( 'D, d M Y H:i:s T ' );
886+ $ created = \time ();
887+ $ params_string = \sprintf (
888+ '("@method" "@target-uri" "date");created=%d;keyid="test-fasp-server-id" ' ,
889+ $ created
890+ );
891+ $ signature_input = "sig= $ params_string " ;
892+ $ signature_header = 'sig=: ' . \base64_encode ( \str_repeat ( 'x ' , 64 ) ) . ': ' ; // 64 bytes for signature.
893+
894+ // Mock the public key retrieval to return an invalid length key.
895+ $ mock_invalid_key = function ( $ key , $ key_id ) {
896+ if ( 'test-fasp-server-id ' === $ key_id ) {
897+ return array (
898+ 'type ' => 'ed25519 ' ,
899+ 'key ' => 'too-short ' , // Invalid key length.
900+ );
901+ }
902+ return $ key ;
903+ };
904+ \add_filter ( 'activitypub_pre_get_public_key ' , $ mock_invalid_key , 10 , 2 );
905+
906+ $ _SERVER ['REQUEST_METHOD ' ] = 'POST ' ;
907+ $ _SERVER ['REQUEST_URI ' ] = '/ ' . \rest_get_url_prefix () . '/ ' . ACTIVITYPUB_REST_NAMESPACE . '/fasp/capabilities/test/1/activation ' ;
908+ $ _SERVER ['HTTP_HOST ' ] = 'example.org ' ;
909+ $ _SERVER ['HTTPS ' ] = 'on ' ;
910+
911+ // Create REST request.
912+ $ request = new \WP_REST_Request ( 'POST ' , ACTIVITYPUB_REST_NAMESPACE . '/fasp/capabilities/test/1/activation ' );
913+ $ request ->set_header ( 'Date ' , $ date );
914+ $ request ->set_header ( 'Host ' , 'example.org ' );
915+ $ request ->set_header ( 'Signature-Input ' , $ signature_input );
916+ $ request ->set_header ( 'Signature ' , $ signature_header );
917+
918+ // Verification should fail due to invalid key length.
919+ $ result = Signature::verify_http_signature ( $ request );
920+ $ this ->assertWPError ( $ result , 'Invalid Ed25519 key length should fail verification ' );
921+ $ this ->assertEquals ( 'invalid_key_length ' , $ result ->get_error_code () );
922+
923+ \remove_filter ( 'activitypub_pre_get_public_key ' , $ mock_invalid_key );
924+ }
748925}
0 commit comments