diff --git a/package_manager/package_manager.module b/package_manager/package_manager.module
index d989334c0235b008e2ba4138118937216cd9e79e..55ca66e5d1aa144f392ec7072e7d1a71dbe0571f 100644
--- a/package_manager/package_manager.module
+++ b/package_manager/package_manager.module
@@ -68,6 +68,23 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) {
       $output .= '  </li>';
       $output .= '</ul>';
 
+      $output .= '<h4 id="package-manager-tuf-info">' . t('Enabling PHP-TUF protection') . '</h4>';
+      $output .= '<p>' . t('Package Manager requires <a href=":php-tuf">PHP-TUF</a>, which implements <a href=":tuf">The Update Framework</a> as a way to help secure Composer package downloads via the <a href=":php-tuf-plugin">PHP-TUF Composer integration plugin</a>. This plugin must be installed and configured properly in order to use Package Manager.', [
+        ':php-tuf' => 'https://github.com/php-tuf/php-tuf',
+        ':tuf' => 'https://theupdateframework.io/',
+        ':php-tuf-plugin' => 'https://github.com/php-tuf/composer-integration',
+      ]) . '</p>';
+      $output .= '<p>' . t('To install and configure the plugin as needed, you can run the following commands:') . '</p>';
+      $output .= '<pre><code>';
+      $output .= "composer config allow-plugins.php-tuf/composer-integration true\n";
+      $output .= "composer require php-tuf/composer-integration";
+      $output .= '</code></pre>';
+      $output .= '<p>' . t('Package Manager currently requires the <code>https://packages.drupal.org</code> Composer repository to be defined in your <code>composer.json</code> file, since Drupal.org is currently the only package repository that has support for TUF. To set this up, run the following commands (assuming your site is based on the <code>drupal/recommended-project</code> or <code>drupal/legacy-project</code> templates):') . '</p>';
+      $output .= '<pre><code>';
+      $output .= "composer config --unset repositories.0\n";
+      $output .= "composer config repositories.drupal '{\"type\": \"composer\", \"url\": \"https://packages.drupal.org/8\", \"tuf\": true}'\n";
+      $output .= '</code></pre>';
+
       $output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported Composer plugins in my codebase?') . '</h4>';
       $output .= '<p>' . t('A fresh Drupal installation only uses supported Composer plugins, but some modules or themes may depend on additional Composer plugins. Please <a href=":new-issue">create a new issue</a> when you encounter this.', [
         ':new-issue' => 'https://www.drupal.org/node/add/project-issue/automatic_updates',
diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index 735ed8753b8dd35c8b55f61529b9e996ffb1ebb3..786c44ca681d6bf0e48c2aeddf8e484ef074c2c7 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -198,6 +198,9 @@ services:
     class: Drupal\package_manager\Validator\PhpExtensionsValidator
     tags:
       - { name: event_subscriber }
+  # @todo Tag this service as an event subscriber in https://drupal.org/i/3358504,
+  #   once packages.drupal.org supports TUF.
+  Drupal\package_manager\Validator\PhpTufValidator: {}
   package_manager.update_processor:
     class: Drupal\package_manager\PackageManagerUpdateProcessor
     arguments:
diff --git a/package_manager/src/ComposerInspector.php b/package_manager/src/ComposerInspector.php
index 94550aba3e4896a628ecf11b5153f705561ef056..b0b37df64dea9a26df1872ad5d79bd68edd67484 100644
--- a/package_manager/src/ComposerInspector.php
+++ b/package_manager/src/ComposerInspector.php
@@ -239,6 +239,7 @@ class ComposerInspector implements LoggerAwareInterface {
    *   but if it is a boolean, an array or a map, JSON decoding should be
    *   applied.
    *
+   * @see ::getAllowPluginsConfig()
    * @see \Composer\Command\ConfigCommand::execute()
    */
   public function getConfig(string $key, string $context): ?string {
@@ -502,4 +503,32 @@ class ComposerInspector implements LoggerAwareInterface {
     };
   }
 
+  /**
+   * Returns the value of `allow-plugins` config setting.
+   *
+   * @param string $dir
+   *   The directory in which to run Composer.
+   *
+   * @return bool[]|bool
+   *   An array of boolean flags to allow or disallow certain plugins, or TRUE
+   *   if all plugins are allowed.
+   *
+   * @see https://getcomposer.org/doc/06-config.md#allow-plugins
+   */
+  public function getAllowPluginsConfig(string $dir): array|bool {
+    // If `allow-plugins` is `false`, Composer 2.5.4 and earlier has no output.
+    $value = $this->getConfig('allow-plugins', $dir) ?? 'false';
+
+    // Try to convert the value we got back to a boolean. If that can't be done,
+    // assume it's an array of plugin-specific flags and parse it as JSON.
+    try {
+      $value = static::toBoolean($value);
+    }
+    catch (\UnhandledMatchError) {
+      $value = json_decode($value, TRUE, flags: JSON_THROW_ON_ERROR);
+    }
+    // An empty array indicates that no plugins are allowed.
+    return $value ?: [];
+  }
+
 }
diff --git a/package_manager/src/Validator/ComposerPluginsValidator.php b/package_manager/src/Validator/ComposerPluginsValidator.php
index 0fdc0268d909a7ea60bcc73d89b85a365526ce75..369f0fb5a6c46b6e2cd8c12394de1f4e64a1fa5b 100644
--- a/package_manager/src/Validator/ComposerPluginsValidator.php
+++ b/package_manager/src/Validator/ComposerPluginsValidator.php
@@ -85,6 +85,7 @@ final class ComposerPluginsValidator implements EventSubscriberInterface {
     'drupal/core-project-message' => '*',
     'phpstan/extension-installer' => '^1.1',
     // cSpell:enable
+    PhpTufValidator::PLUGIN_NAME => '^1',
   ];
 
   /**
@@ -152,23 +153,13 @@ final class ComposerPluginsValidator implements EventSubscriberInterface {
       ? $stage->getStageDirectory()
       : $this->pathLocator->getProjectRoot();
     try {
-      // @see https://getcomposer.org/doc/06-config.md#allow-plugins
-      $value = $this->inspector->getConfig('allow-plugins', $dir);
+      $allowed_plugins = $this->inspector->getAllowPluginsConfig($dir);
     }
     catch (RuntimeException $exception) {
       $event->addErrorFromThrowable($exception, $this->t('Unable to determine Composer <code>allow-plugins</code> setting.'));
       return;
     }
 
-    // Try to convert the value we got back to a boolean. If that can't be done,
-    // assume it's an array of plugin-specific flags and parse it as JSON.
-    try {
-      $allowed_plugins = ComposerInspector::toBoolean($value);
-    }
-    catch (\UnhandledMatchError) {
-      $allowed_plugins = json_decode($value, TRUE, flags: JSON_THROW_ON_ERROR);
-    }
-
     if ($allowed_plugins === TRUE) {
       $event->addError([$this->t('All composer plugins are allowed because <code>config.allow-plugins</code> is configured to <code>true</code>. This is an unacceptable security risk.')]);
       return;
diff --git a/package_manager/src/Validator/PhpTufValidator.php b/package_manager/src/Validator/PhpTufValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..a12d8dcc3964d5e722d82d33424a1ff079788f72
--- /dev/null
+++ b/package_manager/src/Validator/PhpTufValidator.php
@@ -0,0 +1,180 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\package_manager\Validator;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\package_manager\ComposerInspector;
+use Drupal\package_manager\Event\PreApplyEvent;
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Event\PreOperationStageEvent;
+use Drupal\package_manager\Event\PreRequireEvent;
+use Drupal\package_manager\Event\StatusCheckEvent;
+use Drupal\package_manager\PathLocator;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Validates that PHP-TUF is installed and correctly configured.
+ *
+ * In both the active and stage directories, this checks for the following
+ * conditions:
+ * - The PHP-TUF plugin is installed.
+ * - The plugin is not explicitly blocked by Composer's `allow-plugins`
+ *   configuration.
+ * - Composer is aware of at least one repository hosted at
+ *   packages.drupal.org (since that's currently the only server that supports
+ *   TUF), and that those repositories have TUF support explicitly enabled.
+ *
+ * Note that this validator is currently not active, because the service
+ * definition is not tagged as an event subscriber. This will be changed in
+ * https://drupal.org/i/3358504, once TUF support is rolled out on
+ * packages.drupal.org.
+ *
+ * @internal
+ *   This is an internal part of Package Manager and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
+ */
+final class PhpTufValidator implements EventSubscriberInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The name of the PHP-TUF Composer integration plugin.
+   *
+   * @var string
+   */
+  public const PLUGIN_NAME = 'php-tuf/composer-integration';
+
+  /**
+   * Constructs a PhpTufValidator object.
+   *
+   * @param \Drupal\package_manager\PathLocator $pathLocator
+   *   The path locator service.
+   * @param \Drupal\package_manager\ComposerInspector $composerInspector
+   *   The Composer inspector service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   The module handler service.
+   */
+  public function __construct(
+    private readonly PathLocator $pathLocator,
+    private readonly ComposerInspector $composerInspector,
+    private readonly ModuleHandlerInterface $moduleHandler
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      StatusCheckEvent::class => 'validate',
+      PreCreateEvent::class => 'validate',
+      PreRequireEvent::class => 'validate',
+      PreApplyEvent::class => 'validate',
+    ];
+  }
+
+  /**
+   * Reacts to a stage event by validating PHP-TUF configuration as needed.
+   *
+   * @param \Drupal\package_manager\Event\PreOperationStageEvent $event
+   *   The event object.
+   */
+  public function validate(PreOperationStageEvent $event): void {
+    $messages = $this->validateTuf($this->pathLocator->getProjectRoot());
+    if ($messages) {
+      $event->addError($messages, $this->t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+    }
+
+    $stage = $event->stage;
+    if ($stage->stageDirectoryExists()) {
+      $messages = $this->validateTuf($stage->getStageDirectory());
+      if ($messages) {
+        $event->addError($messages, $this->t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+      }
+    }
+  }
+
+  /**
+   * Flags messages if PHP-TUF is not installed and configured properly.
+   *
+   * @param string $dir
+   *   The directory to examine.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
+   *   The error messages, if any.
+   */
+  private function validateTuf(string $dir): array {
+    $messages = [];
+
+    if ($this->moduleHandler->moduleExists('help')) {
+      $help_url = Url::fromRoute('help.page', ['name' => 'package_manager'])
+        ->setOption('fragment', 'package-manager-tuf-info')
+        ->toString();
+    }
+
+    // The Composer plugin must be installed.
+    $installed_packages = $this->composerInspector->getInstalledPackagesList($dir);
+    if (!isset($installed_packages[static::PLUGIN_NAME])) {
+      $message = $this->t('The <code>@plugin</code> plugin is not installed.', [
+        '@plugin' => static::PLUGIN_NAME,
+      ]);
+      if (isset($help_url)) {
+        $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to install the plugin.', [
+          '@message' => $message,
+          ':url' => $help_url,
+        ]);
+      }
+      $messages[] = $message;
+    }
+
+    // And it has to be explicitly enabled.
+    $allowed_plugins = $this->composerInspector->getAllowPluginsConfig($dir);
+    if ($allowed_plugins !== TRUE && empty($allowed_plugins[static::PLUGIN_NAME])) {
+      $message = $this->t('The <code>@plugin</code> plugin is not listed as an allowed plugin.', [
+        '@plugin' => static::PLUGIN_NAME,
+      ]);
+      if (isset($help_url)) {
+        $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to configure the plugin.', [
+          '@message' => $message,
+          ':url' => $help_url,
+        ]);
+      }
+      $messages[] = $message;
+    }
+
+    // Get the defined repositories that use packages.drupal.org.
+    $repositories = array_filter(
+      Json::decode($this->composerInspector->getConfig('repositories', $dir)),
+      fn (array $r): bool => str_starts_with($r['url'], 'https://packages.drupal.org')
+    );
+
+    // All packages.drupal.org repositories must have TUF protection.
+    foreach ($repositories as $repository) {
+      if (empty($repository['tuf'])) {
+        $messages[] = $this->t('TUF is not enabled for the @url repository.', [
+          '@url' => $repository['url'],
+        ]);
+      }
+    }
+
+    // There must be at least one repository using packages.drupal.org, since
+    // that's the only repository which supports TUF right now.
+    if (empty($repositories)) {
+      $message = $this->t('The <code>https://packages.drupal.org</code> Composer repository must be defined in <code>composer.json</code>.');
+      if (isset($help_url)) {
+        $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to set up this repository.', [
+          '@message' => $message,
+          ':url' => $help_url,
+        ]);
+      }
+      $messages[] = $message;
+    }
+    return $messages;
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/package_manager/tests/src/Kernel/ComposerInspectorTest.php
index a645a34b31b6af538c0b258b0db54ca2e568ff0f..5e61f9cdc9472a81b49160682c2237b4034555aa 100644
--- a/package_manager/tests/src/Kernel/ComposerInspectorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerInspectorTest.php
@@ -410,4 +410,66 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase {
     $this->assertSame($is_metapackage, is_null($list['test/package']->path));
   }
 
+  /**
+   * Data provider for ::testAllowedPlugins().
+   *
+   * @return array[]
+   *   The test cases.
+   */
+  public function providerAllowedPlugins(): array {
+    return [
+      'all plugins allowed' => [
+        ['allow-plugins' => TRUE],
+        TRUE,
+      ],
+      'no plugins allowed' => [
+        ['allow-plugins' => FALSE],
+        [],
+      ],
+      'some plugins allowed' => [
+        [
+          'allow-plugins.example/plugin-a' => TRUE,
+          'allow-plugins.example/plugin-b' => FALSE,
+        ],
+        [
+          'example/plugin-a' => TRUE,
+          'example/plugin-b' => FALSE,
+          // The scaffold plugin is explicitly disallowed by the fake_site
+          // fixture.
+          'drupal/core-composer-scaffold' => FALSE,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests ComposerInspector's parsing of the allowed plugins list.
+   *
+   * @param array $config
+   *   The Composer configuration to set.
+   * @param array|bool $expected_value
+   *   The expected return value from getAllowPluginsConfig().
+   *
+   * @covers ::getAllowPluginsConfig
+   *
+   * @dataProvider providerAllowedPlugins
+   */
+  public function testAllowedPlugins(array $config, bool|array $expected_value): void {
+    (new ActiveFixtureManipulator())
+      ->addConfig($config)
+      ->commitChanges();
+
+    $project_root = $this->container->get(PathLocator::class)->getProjectRoot();
+    $actual_value = $this->container->get(ComposerInspector::class)
+      ->getAllowPluginsConfig($project_root);
+
+    if (is_array($expected_value)) {
+      ksort($expected_value);
+    }
+    if (is_array($actual_value)) {
+      ksort($actual_value);
+    }
+    $this->assertSame($expected_value, $actual_value);
+  }
+
 }
diff --git a/package_manager/tests/src/Kernel/LockFileValidatorTest.php b/package_manager/tests/src/Kernel/LockFileValidatorTest.php
index 07127d9e66ddb50d5a2b047632e7860846d736b0..f99e7c9887e009d80b5296e86b9cf0c08375952b 100644
--- a/package_manager/tests/src/Kernel/LockFileValidatorTest.php
+++ b/package_manager/tests/src/Kernel/LockFileValidatorTest.php
@@ -56,6 +56,7 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
     $inspector->getConfig('extra', $arguments)->willReturn('{}');
     $inspector->getConfig('minimum-stability', $arguments)->willReturn('stable');
     $inspector->getInstalledPackagesList($arguments)->willReturn(new InstalledPackagesList());
+    $inspector->getAllowPluginsConfig($arguments)->willReturn([]);
     $inspector->validate($arguments);
     $inspector->getRootPackageInfo($arguments)->willReturn([]);
     $container->set('package_manager.composer_inspector', $inspector->reveal());
diff --git a/package_manager/tests/src/Kernel/PhpTufValidatorTest.php b/package_manager/tests/src/Kernel/PhpTufValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd8c0daa03a6bea4718f499f821d8105907aecb8
--- /dev/null
+++ b/package_manager/tests/src/Kernel/PhpTufValidatorTest.php
@@ -0,0 +1,250 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\fixture_manipulator\ActiveFixtureManipulator;
+use Drupal\fixture_manipulator\FixtureManipulator;
+use Drupal\package_manager\Event\PreApplyEvent;
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Event\PreRequireEvent;
+use Drupal\package_manager\Exception\StageEventException;
+use Drupal\package_manager\ValidationResult;
+use Drupal\package_manager\Validator\PhpTufValidator;
+
+/**
+ * @coversDefaultClass \Drupal\package_manager\Validator\PhpTufValidator
+ * @group package_manager
+ * @internal
+ */
+class PhpTufValidatorTest extends PackageManagerKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    (new ActiveFixtureManipulator())
+      ->addConfig([
+        'repositories.drupal' => [
+          'type' => 'composer',
+          'url' => 'https://packages.drupal.org/8',
+          'tuf' => TRUE,
+        ],
+        'allow-plugins.' . PhpTufValidator::PLUGIN_NAME => TRUE,
+      ])
+      ->addPackage([
+        'name' => PhpTufValidator::PLUGIN_NAME,
+        'type' => 'composer-plugin',
+        'require' => [
+          'composer-plugin-api' => '*',
+        ],
+        'extra' => [
+          'class' => 'PhpTufComposerPlugin',
+        ],
+      ])
+      ->commitChanges();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
+
+    // @todo Remove this in https://drupal.org/i/3358504, once
+    //   packages.drupal.org supports TUF.
+    $container->getDefinition(PhpTufValidator::class)
+      ->addTag('event_subscriber');
+  }
+
+  /**
+   * Tests that there are no errors if the plugin is set up correctly.
+   */
+  public function testPluginInstalledAndConfiguredProperly(): void {
+    $this->assertStatusCheckResults([]);
+    $this->assertResults([]);
+  }
+
+  /**
+   * Tests there is an error if the plugin is not installed in the project root.
+   */
+  public function testPluginNotInstalledInProjectRoot(): void {
+    (new ActiveFixtureManipulator())
+      ->removePackage(PhpTufValidator::PLUGIN_NAME)
+      ->commitChanges();
+
+    $messages = [
+      t('The <code>php-tuf/composer-integration</code> plugin is not installed.'),
+      // Composer automatically removes the plugin from the `allow-plugins`
+      // list when the plugin package is removed.
+      t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
+    ];
+    $result = ValidationResult::createError($messages, t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+    $this->assertStatusCheckResults([$result]);
+    $this->assertResults([$result], PreCreateEvent::class);
+  }
+
+  /**
+   * Tests removing the plugin from the stage on pre-require.
+   */
+  public function testPluginRemovedFromStagePreRequire(): void {
+    $this->getStageFixtureManipulator()
+      ->removePackage(PhpTufValidator::PLUGIN_NAME);
+
+    $messages = [
+      t('The <code>php-tuf/composer-integration</code> plugin is not installed.'),
+      // Composer automatically removes the plugin from the `allow-plugins`
+      // list when the plugin package is removed.
+      t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
+    ];
+    $result = ValidationResult::createError($messages, t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+    $this->assertResults([$result], PreRequireEvent::class);
+  }
+
+  /**
+   * Tests removing the plugin from the stage before applying it.
+   */
+  public function testPluginRemovedFromStagePreApply(): void {
+    $stage = $this->createStage();
+    $stage->create();
+    $stage->require(['ext-json:*']);
+
+    (new FixtureManipulator())
+      ->removePackage(PhpTufValidator::PLUGIN_NAME)
+      ->commitChanges($stage->getStageDirectory());
+
+    $messages = [
+      t('The <code>php-tuf/composer-integration</code> plugin is not installed.'),
+      // Composer automatically removes the plugin from the `allow-plugins`
+      // list when the plugin package is removed.
+      t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
+    ];
+    $result = ValidationResult::createError($messages, t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+    try {
+      $stage->apply();
+      $this->fail('Expected an exception but none was thrown.');
+    }
+    catch (StageEventException $e) {
+      $this->assertInstanceOf(PreApplyEvent::class, $e->event);
+      $this->assertValidationResultsEqual([$result], $e->event->getResults());
+    }
+  }
+
+  /**
+   * Data provider for testing invalid plugin configuration.
+   *
+   * @return array[]
+   *   The test cases.
+   */
+  public function providerInvalidConfiguration(): array {
+    return [
+      'plugin specifically disallowed' => [
+        [
+          'allow-plugins.' . PhpTufValidator::PLUGIN_NAME => FALSE,
+        ],
+        [
+          t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
+        ],
+      ],
+      'all plugins disallowed' => [
+        [
+          'allow-plugins' => FALSE,
+        ],
+        [
+          t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
+        ],
+      ],
+      'packages.drupal.org not defined' => [
+        [
+          'repositories.drupal' => FALSE,
+        ],
+        [
+          t('The <code>https://packages.drupal.org</code> Composer repository must be defined in <code>composer.json</code>.'),
+        ],
+      ],
+      'packages.drupal.org not using TUF' => [
+        [
+          'repositories.drupal' => [
+            'type' => 'composer',
+            'url' => 'https://packages.drupal.org/8',
+          ],
+        ],
+        [
+          t('TUF is not enabled for the https://packages.drupal.org/8 repository.'),
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Data provider for testing invalid plugin configuration in the stage.
+   *
+   * @return \Generator
+   *   The test cases.
+   */
+  public function providerInvalidConfigurationInStage(): \Generator {
+    foreach ($this->providerInvalidConfiguration() as $name => $arguments) {
+      $arguments[] = PreRequireEvent::class;
+      yield "$name on pre-require" => $arguments;
+
+      array_splice($arguments, -1, NULL, PreApplyEvent::class);
+      yield "$name on pre-apply" => $arguments;
+    }
+  }
+
+  /**
+   * Tests errors caused by invalid plugin configuration in the project root.
+   *
+   * @param array $config
+   *   The Composer configuration to set.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_messages
+   *   The expected error messages.
+   *
+   * @dataProvider providerInvalidConfiguration
+   */
+  public function testInvalidConfigurationInProjectRoot(array $config, array $expected_messages): void {
+    (new ActiveFixtureManipulator())->addConfig($config)->commitChanges();
+
+    $result = ValidationResult::createError($expected_messages, t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+    $this->assertStatusCheckResults([$result]);
+    $this->assertResults([$result], PreCreateEvent::class);
+  }
+
+  /**
+   * Tests errors caused by invalid plugin configuration in the stage directory.
+   *
+   * @param array $config
+   *   The Composer configuration to set.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_messages
+   *   The expected error messages.
+   * @param string $event_class
+   *   The event before which the plugin's configuration should be changed.
+   *
+   * @dataProvider providerInvalidConfigurationInStage
+   */
+  public function testInvalidConfigurationInStage(array $config, array $expected_messages, string $event_class): void {
+    $listener = function (PreRequireEvent|PreApplyEvent $event) use ($config): void {
+      (new FixtureManipulator())
+        ->addConfig($config)
+        ->commitChanges($event->stage->getStageDirectory());
+    };
+    $this->addEventTestListener($listener, $event_class);
+
+    // LockFileValidator will complain because we have not added, removed, or
+    // updated any packages in the stage. In this very specific situation, it's
+    // okay to disable that validator to remove the interference.
+    if ($event_class === PreApplyEvent::class) {
+      $lock_file_validator = $this->container->get('package_manager.validator.lock_file');
+      $this->container->get('event_dispatcher')
+        ->removeSubscriber($lock_file_validator);
+    }
+
+    $result = ValidationResult::createError($expected_messages, t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
+    $this->assertResults([$result], $event_class);
+  }
+
+}
diff --git a/tests/src/Functional/ClickableHelpTest.php b/tests/src/Functional/ClickableHelpTest.php
index 984e760513c8292892e4ca272dedf74a588bcea2..b2db4c23847775fcaf250ad1ebfc29bf1b4ff135 100644
--- a/tests/src/Functional/ClickableHelpTest.php
+++ b/tests/src/Functional/ClickableHelpTest.php
@@ -4,8 +4,13 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
+use Drupal\Core\Url;
+use Drupal\package_manager\Event\StatusCheckEvent;
+use Drupal\package_manager\ValidationResult;
+use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber;
+
 /**
- * Tests package manager help link is clickable.
+ * Tests that links to online help in validation errors are clickable.
  *
  * @group automatic_updates
  * @internal
@@ -18,25 +23,34 @@ class ClickableHelpTest extends AutomaticUpdatesFunctionalTestBase {
   protected static $modules = [
     'automatic_updates',
     'help',
+    'package_manager_test_validation',
   ];
 
   /**
    * {@inheritdoc}
    */
-  protected $defaultTheme = 'starterkit_theme';
+  protected $defaultTheme = 'stark';
 
   /**
-   * Tests if composer executable is not present then the help link clickable.
+   * Tests that a link to online help in a validation error is clickable.
    */
   public function testHelpLinkClickable(): void {
+    $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
+      ->toString();
+
+    $result = ValidationResult::createError([
+      t('A problem was found! <a href=":url">Read all about it.</a>', [':url' => $url]),
+    ]);
+    TestSubscriber::setTestResult([$result], StatusCheckEvent::class);
+
     $this->drupalLogin($this->createUser([
       'administer site configuration',
     ]));
-    $this->config('package_manager.settings')
-      ->set('executables.composer', '/not/matching/path/to/composer')
-      ->save();
     $this->drupalGet('admin/reports/status');
-    $this->assertSession()->linkByHrefExists('/admin/help/package_manager#package-manager-composer-related-faq');
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('A problem was found! Read all about it.');
+    $assert_session->linkExists('Read all about it.');
+    $assert_session->linkByHrefExists($url);
   }
 
 }