diff --git a/config/install/automatic_updates.settings.yml b/config/install/automatic_updates.settings.yml
index 87210fd7ff262d69e064eb240acd47be10d8cff4..0e900e9b0a50be2cbf66d9d63ec9da57586ec076 100644
--- a/config/install/automatic_updates.settings.yml
+++ b/config/install/automatic_updates.settings.yml
@@ -3,5 +3,5 @@ enable_psa: true
 notify: true
 check_frequency: 43200
 enable_readiness_checks: true
-download_uri: 'https://ftp.drupal.org/files/projects'
+hashes_uri: 'https://updates.drupal.org/release-hashes'
 ignored_paths: "modules/custom/*\nthemes/custom/*\nprofiles/custom/*"
diff --git a/config/schema/automatic_updates.schema.yml b/config/schema/automatic_updates.schema.yml
index 7d378de5d6fee32ea581247b9f055282ebbfd8fe..0a45a08a0e950b49f5fef51eccf0fc87c5a7487a 100644
--- a/config/schema/automatic_updates.schema.yml
+++ b/config/schema/automatic_updates.schema.yml
@@ -17,9 +17,9 @@ automatic_updates.settings:
     enable_readiness_checks:
       type: boolean
       label: 'Enable readiness checks'
-    download_uri:
+    hashes_uri:
       type: string
-      label: 'Endpoint URI for file hashes and quasi patch files'
+      label: 'Endpoint URI for file hashes'
     ignored_paths:
       type: string
       label: 'List of files paths to ignore when running readiness checks'
diff --git a/src/ProjectInfoTrait.php b/src/ProjectInfoTrait.php
index 1fde5d6b42ca2a83619738f83217a87ca73b21a3..b3790f6c65b36486f7b6bdf68954de2fb09460b8 100644
--- a/src/ProjectInfoTrait.php
+++ b/src/ProjectInfoTrait.php
@@ -9,21 +9,65 @@ use PackageVersions\Versions;
  */
 trait ProjectInfoTrait {
 
+  /**
+   * Get extension list.
+   *
+   * @param string $extension_type
+   *   The extension type.
+   *
+   * @return \Drupal\Core\Extension\ExtensionList
+   *   The extension list service.
+   */
+  protected function getExtensionList($extension_type) {
+    if (isset($this->{$extension_type})) {
+      $list = $this->{$extension_type};
+    }
+    else {
+      $list = \Drupal::service("extension.list.$extension_type");
+    }
+    return $list;
+  }
+
+  /**
+   * Returns an array of info files information of available extensions.
+   *
+   * @param string $extension_type
+   *   The extension type.
+   *
+   * @return array
+   *   An associative array of extension information arrays, keyed by extension
+   *   name.
+   */
+  protected function getInfos($extension_type) {
+    $file_paths = $this->getExtensionList($extension_type)->getPathnames();
+    $infos = $this->getExtensionList($extension_type)->getAllAvailableInfo();
+    return array_map(function ($key, array $info) use ($file_paths) {
+      $info['packaged'] = $info['project'] ?? FALSE;
+      $info['project'] = $this->getProjectName($key, $info);
+      $info['install path'] = $file_paths[$key] ? dirname($file_paths[$key]) : '';
+      $info['version'] = $this->getExtensionVersion($info);
+      return $info;
+    }, array_keys($infos), $infos);
+  }
+
   /**
    * Get the extension version.
    *
-   * @param string $extension_name
-   *   The extension name.
    * @param array $info
    *   The extension's info.
    *
    * @return string|null
    *   The version or NULL if undefined.
    */
-  protected function getExtensionVersion($extension_name, array $info) {
+  protected function getExtensionVersion(array $info) {
+    $extension_name = $info['project'];
     if (isset($info['version']) && strpos($info['version'], '-dev') === FALSE) {
       return $info['version'];
     }
+    // Handle experimental modules from core.
+    if (substr($info['install path'], 0, 4) === "core") {
+      return $this->getExtensionList('module')->get('system')->info['version'];
+    }
     $composer_json = $this->getComposerJson($extension_name, $info);
     $extension_name = isset($composer_json['name']) ? $composer_json['name'] : $extension_name;
     try {
@@ -59,6 +103,9 @@ trait ProjectInfoTrait {
         $project_name = $this->getSuffix($composer_json['name'], '/', $extension_name);
       }
     }
+    if ($project_name === 'system') {
+      $project_name = 'drupal';
+    }
     return $project_name;
   }
 
diff --git a/src/ReadinessChecker/MissingProjectInfo.php b/src/ReadinessChecker/MissingProjectInfo.php
index a1599ee4c806dfc047f56a67529fafffd31a7fd0..089cf05471da685f15d2d24467e2cf6002a907e6 100644
--- a/src/ReadinessChecker/MissingProjectInfo.php
+++ b/src/ReadinessChecker/MissingProjectInfo.php
@@ -72,12 +72,12 @@ class MissingProjectInfo extends Filesystem {
   protected function missingProjectInfoCheck() {
     $messages = [];
     foreach ($this->getExtensionsTypes() as $extension_type) {
-      foreach ($this->getInfos($extension_type) as $extension_name => $info) {
-        if ($this->isIgnoredPath(drupal_get_path($info['type'], $extension_name))) {
+      foreach ($this->getInfos($extension_type) as $info) {
+        if ($this->isIgnoredPath($info['install path'])) {
           continue;
         }
-        if (!$this->getExtensionVersion($extension_name, $info)) {
-          $messages[] = $this->t('The project "@extension" can not be updated because its version is either undefined or a dev release.', ['@extension' => $extension_name]);
+        if (!$info['version']) {
+          $messages[] = $this->t('The project "@extension" can not be updated because its version is either undefined or a dev release.', ['@extension' => $info['name']]);
         }
       }
     }
@@ -94,18 +94,4 @@ class MissingProjectInfo extends Filesystem {
     return ['modules', 'profiles', 'themes'];
   }
 
-  /**
-   * Returns an array of info files information of available extensions.
-   *
-   * @param string $extension_type
-   *   The extension type.
-   *
-   * @return array
-   *   An associative array of extension information arrays, keyed by extension
-   *   name.
-   */
-  protected function getInfos($extension_type) {
-    return $this->{$extension_type}->getAllAvailableInfo();
-  }
-
 }
diff --git a/src/ReadinessChecker/ModifiedFiles.php b/src/ReadinessChecker/ModifiedFiles.php
index 2d2b516d623193e6e847e35bed89056ba4f45372..99c041d1af049af915e7cc06fe70a62a28ed4bc5 100644
--- a/src/ReadinessChecker/ModifiedFiles.php
+++ b/src/ReadinessChecker/ModifiedFiles.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\automatic_updates\ReadinessChecker;
 
+use Drupal\automatic_updates\ProjectInfoTrait;
 use Drupal\automatic_updates\Services\ModifiedFilesInterface;
 use Drupal\Core\Extension\ExtensionList;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -11,6 +12,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
  */
 class ModifiedFiles implements ReadinessCheckerInterface {
   use StringTranslationTrait;
+  use ProjectInfoTrait;
 
   /**
    * The modified files service.
@@ -24,21 +26,21 @@ class ModifiedFiles implements ReadinessCheckerInterface {
    *
    * @var \Drupal\Core\Extension\ExtensionList
    */
-  protected $modules;
+  protected $module;
 
   /**
    * The profile extension list.
    *
    * @var \Drupal\Core\Extension\ExtensionList
    */
-  protected $profiles;
+  protected $profile;
 
   /**
    * The theme extension list.
    *
    * @var \Drupal\Core\Extension\ExtensionList
    */
-  protected $themes;
+  protected $theme;
 
   /**
    * An array of array of strings of extension paths.
@@ -83,11 +85,10 @@ class ModifiedFiles implements ReadinessCheckerInterface {
   protected function modifiedFilesCheck() {
     $messages = [];
     $extensions = [];
-    $extensions['system'] = $this->modules->get('system')->info;
     foreach ($this->getExtensionsTypes() as $extension_type) {
-      foreach ($this->getInfos($extension_type) as $extension_name => $info) {
-        if (substr($this->getPath($extension_type, $extension_name), 0, 4) !== 'core') {
-          $extensions[$extension_name] = $info;
+      foreach ($this->getInfos($extension_type) as $info) {
+        if (substr($info['install path'], 0, 4) !== 'core' || $info['project'] === 'drupal') {
+          $extensions[$info['project']] = $info;
         }
       }
     }
@@ -104,39 +105,7 @@ class ModifiedFiles implements ReadinessCheckerInterface {
    *   The extension types.
    */
   protected function getExtensionsTypes() {
-    return ['modules', 'profiles', 'themes'];
-  }
-
-  /**
-   * Returns an array of info files information of available extensions.
-   *
-   * @param string $extension_type
-   *   The extension type.
-   *
-   * @return array
-   *   An associative array of extension information arrays, keyed by extension
-   *   name.
-   */
-  protected function getInfos($extension_type) {
-    return $this->{$extension_type}->getAllAvailableInfo();
-  }
-
-  /**
-   * Returns an extension file path.
-   *
-   * @param string $extension_type
-   *   The extension type.
-   * @param string $extension_name
-   *   The extension name.
-   *
-   * @return string
-   *   An extension file path or NULL if it does not exist.
-   */
-  protected function getPath($extension_type, $extension_name) {
-    if (!isset($this->paths[$extension_type])) {
-      $this->paths[$extension_type] = $this->{$extension_type}->getPathnames();
-    }
-    return isset($this->paths[$extension_type][$extension_name]) ? $this->paths[$extension_type][$extension_name] : NULL;
+    return ['module', 'profile', 'theme'];
   }
 
 }
diff --git a/src/Services/ModifiedFiles.php b/src/Services/ModifiedFiles.php
index 41f50885088765e78279286cd58aeb09a01a7ca9..2937862ef5190c58bd747be9b076a5a8a850d07b 100644
--- a/src/Services/ModifiedFiles.php
+++ b/src/Services/ModifiedFiles.php
@@ -72,6 +72,7 @@ class ModifiedFiles implements ModifiedFilesInterface {
    */
   public function getModifiedFiles(array $extensions = []) {
     $modified_files = [];
+    /** @var \GuzzleHttp\Promise\PromiseInterface[] $promises */
     $promises = $this->getHashRequests($extensions);
     // Wait until all the requests are finished.
     (new EachPromise($promises, [
@@ -86,13 +87,16 @@ class ModifiedFiles implements ModifiedFilesInterface {
   /**
    * Process checking hashes of files from external URL.
    *
-   * @param resource $resource
-   *   A resource handle.
+   * @param array $resource
+   *   An array of response resource and project info.
    * @param array $modified_files
    *   The list of modified files.
    */
-  protected function processHashes($resource, array &$modified_files) {
-    while (($line = fgets($resource)) !== FALSE) {
+  protected function processHashes(array $resource, array &$modified_files) {
+    $response = $resource['response'];
+    $info = $resource['info'];
+    $file_root = $info['install path'];
+    while (($line = fgets($response)) !== FALSE) {
       list($hash, $file) = preg_split('/\s+/', $line, 2);
       $file = trim($file);
       // If the line is empty, proceed to the next line.
@@ -107,15 +111,18 @@ class ModifiedFiles implements ModifiedFilesInterface {
       if ($this->isIgnoredPath($file)) {
         continue;
       }
-      $file_path = $this->drupalFinder->getDrupalRoot() . DIRECTORY_SEPARATOR . $file;
+      if ($info['project'] === 'drupal') {
+        $file_root = $this->drupalFinder->getDrupalRoot();
+      }
+      $file_path = $file_root . DIRECTORY_SEPARATOR . $file;
       if (!file_exists($file_path) || hash_file('sha512', $file_path) !== $hash) {
         $modified_files[] = $file_path;
       }
     }
-    if (!feof($resource)) {
+    if (!feof($response)) {
       $this->logger->error('Stream for resource closed prematurely.');
     }
-    fclose($resource);
+    fclose($response);
   }
 
   /**
@@ -131,9 +138,9 @@ class ModifiedFiles implements ModifiedFilesInterface {
    * @@codingStandardsIgnoreEnd
    */
   protected function getHashRequests(array $extensions) {
-    foreach ($extensions as $extension_name => $info) {
-      $url = $this->buildUrl($extension_name, $info);
-      yield $this->getPromise($url);
+    foreach ($extensions as $info) {
+      $url = $this->buildUrl($info);
+      yield $this->getPromise($url, $info);
     }
   }
 
@@ -142,36 +149,39 @@ class ModifiedFiles implements ModifiedFilesInterface {
    *
    * @param string $url
    *   The URL.
+   * @param array $info
+   *   The extension's info.
    *
    * @return \GuzzleHttp\Promise\PromiseInterface
    *   The promise.
    */
-  protected function getPromise($url) {
+  protected function getPromise($url, array $info) {
     return $this->httpClient->requestAsync('GET', $url, [
       'stream' => TRUE,
       'read_timeout' => 30,
     ])
-      ->then(function (ResponseInterface $response) {
-        return $response->getBody()->detach();
+      ->then(function (ResponseInterface $response) use ($info) {
+        return [
+          'response' => $response->getBody()->detach(),
+          'info' => $info,
+        ];
       });
   }
 
   /**
    * Build an extension's hash file URL.
    *
-   * @param string $extension_name
-   *   The extension name.
    * @param array $info
    *   The extension's info.
    *
    * @return string
    *   The URL endpoint with for an extension.
    */
-  protected function buildUrl($extension_name, array $info) {
-    $version = $this->getExtensionVersion($extension_name, $info);
-    $project_name = $this->getProjectName($extension_name, $info);
+  protected function buildUrl(array $info) {
+    $version = $info['version'];
+    $project_name = $info['project'];
     $hash_name = $this->getHashName($info);
-    $uri = ltrim($this->configFactory->get('automatic_updates.settings')->get('download_uri'), '/');
+    $uri = ltrim($this->configFactory->get('automatic_updates.settings')->get('hashes_uri'), '/');
     return Url::fromUri($uri . "/$project_name/$version/$hash_name")->toString();
   }
 
@@ -185,11 +195,11 @@ class ModifiedFiles implements ModifiedFilesInterface {
    *   The hash name.
    */
   protected function getHashName(array $info) {
-    $hash_name = 'SHA512SUMS';
-    if (isset($info['project'])) {
-      $hash_name .= '-package';
+    $hash_name = 'contents-sha512sums';
+    if ($info['packaged']) {
+      $hash_name .= '-packaged';
     }
-    return $hash_name;
+    return $hash_name . '.txt';
   }
 
 }
diff --git a/tests/modules/test_automatic_updates/src/Controller/HashesController.php b/tests/modules/test_automatic_updates/src/Controller/HashesController.php
index f2026c9eb4b15e19d009b9a37a4f29fd8ad80d87..fb9622c67cbe155b2b6e0b49e9429c9508db893e 100644
--- a/tests/modules/test_automatic_updates/src/Controller/HashesController.php
+++ b/tests/modules/test_automatic_updates/src/Controller/HashesController.php
@@ -31,6 +31,14 @@ class HashesController extends ControllerBase {
       // Fake out a change in the LICENSE.txt.
       $response->setContent("2d4ce6b272311ca4159056fb75138eba1814b65323c35ae5e0978233918e45e62bb32fdd2e0e8f657954fd5823c045762b3b59645daf83246d88d8797726e02c  core/LICENSE.txt\n");
     }
+    elseif ($extension === 'ctools' && $version === '3.2') {
+      // Fake out a change in the LICENSE.txt.
+      $response->setContent("aee80b1f9f7f4a8a00dcf6e6ce6c41988dcaedc4de19d9d04460cbfb05d99829ffe8f9d038468eabbfba4d65b38e8dbef5ecf5eb8a1b891d9839cda6c48ee957  LICENSE.txt\n");
+    }
+    elseif ($extension === 'ctools' && $version === '3.1') {
+      // Fake out a change in the LICENSE.txt.
+      $response->setContent("c82147962109321f8fb9c802735d31aab659a1cc3cd13d36dc5371c8b682ff60f23d41c794f2d9dc970ef9634b7fc8bcf35e3b95132644fe2ec97a341658a3f6  LICENSE.txt\n");
+    }
     return $response;
   }
 
diff --git a/tests/src/Functional/ModifiedFilesTest.php b/tests/src/Functional/ModifiedFilesTest.php
index e7df3e7acdb97ba61a15f0d5924ad25370d8f4e7..51b88850d11845b189483bc01fc3b3f60239d51c 100644
--- a/tests/src/Functional/ModifiedFilesTest.php
+++ b/tests/src/Functional/ModifiedFilesTest.php
@@ -33,14 +33,33 @@ class ModifiedFilesTest extends BrowserTestBase {
       $this->container->get('config.factory')
     );
     $this->initHashesEndpoint($modified_files, 'core', '8.7.0');
-    $files = $modified_files->getModifiedFiles(['system' => []]);
+    $extensions = $modified_files->getInfos('module');
+    $extensions = array_filter($extensions, function (array $extension) {
+      return $extension['project'] === 'drupal';
+    });
+    $files = $modified_files->getModifiedFiles($extensions);
     $this->assertEmpty($files);
 
     // Hash doesn't match i.e. modified code, including contrib logic.
     $this->initHashesEndpoint($modified_files, 'core', '8.0.0');
-    $files = $modified_files->getModifiedFiles(['system' => []]);
+    $files = $modified_files->getModifiedFiles($extensions);
     $this->assertCount(1, $files);
     $this->assertStringEndsWith('core/LICENSE.txt', $files[0]);
+
+    // Test contrib hash matches.
+    $extensions = $modified_files->getInfos('module');
+    $extensions = array_filter($extensions, function (array $extension) {
+      return $extension['name'] === 'Chaos Tools';
+    });
+    $this->initHashesEndpoint($modified_files, 'ctools', '3.2');
+    $files = $modified_files->getModifiedFiles($extensions);
+    $this->assertEmpty($files);
+
+    // Test contrib doesn't match.
+    $this->initHashesEndpoint($modified_files, 'ctools', '3.1');
+    $files = $modified_files->getModifiedFiles($extensions);
+    $this->assertCount(1, $files);
+    $this->assertStringEndsWith('contrib/ctools/LICENSE.txt', $files[0]);
   }
 
   /**
@@ -77,8 +96,17 @@ class TestModifiedFiles extends ModifiedFiles {
   /**
    * {@inheritdoc}
    */
-  protected function buildUrl($extension_name, array $info) {
+  protected function buildUrl(array $info) {
     return $this->endpoint;
   }
 
+  // @codingStandardsIgnoreStart
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfos($extension_type) {
+    return parent::getInfos($extension_type);
+  }
+  // codingStandardsIgnoreEnd
+
 }
diff --git a/tests/src/Kernel/ProjectInfoTraitTest.php b/tests/src/Kernel/ProjectInfoTraitTest.php
index 6c961db6074f26550279719e62d613d50e203574..9330a81cf32ef75e408abec3815f9863d4d34d34 100644
--- a/tests/src/Kernel/ProjectInfoTraitTest.php
+++ b/tests/src/Kernel/ProjectInfoTraitTest.php
@@ -25,8 +25,9 @@ class ProjectInfoTraitTest extends KernelTestBase {
    */
   public function testTrait($expected, $info, $extension_name) {
     $class = new ProjectInfoTestClass();
-    $this->assertSame($expected['version'], $class->getExtensionVersion($extension_name, $info));
-    $this->assertSame($expected['project'], $class->getProjectName($extension_name, $info));
+    $project_name = $class->getProjectName($extension_name, $info);
+    $this->assertSame($expected['project'], $project_name);
+    $this->assertSame($expected['version'], $class->getExtensionVersion($info + ['project' => $project_name]));
   }
 
   /**
@@ -47,6 +48,7 @@ class ProjectInfoTraitTest extends KernelTestBase {
       'core' => '8.x',
       'configure' => 'entity.node_type.collection',
       'dependencies' => ['drupal:text'],
+      'install path' => '',
     ];
     $infos['node']['extension_name'] = 'node';
 
@@ -62,6 +64,7 @@ class ProjectInfoTraitTest extends KernelTestBase {
       'core' => '8.x',
       'configure' => 'update.settings',
       'dependencies' => ['file'],
+      'install path' => '',
     ];
     $infos['update']['extension_name'] = 'drupal/update';
 
@@ -80,6 +83,7 @@ class ProjectInfoTraitTest extends KernelTestBase {
       'required' => 'true',
       'configure' => 'system.admin_config_system',
       'dependencies' => [],
+      'install path' => '',
     ];
     $infos['system']['extension_name'] = 'system';
 
@@ -95,6 +99,7 @@ class ProjectInfoTraitTest extends KernelTestBase {
       'core' => '8.x',
       'configure' => 'automatic_updates.settings',
       'dependencies' => ['system', 'update'],
+      'install path' => '',
     ];
     $infos['automatic_updates']['extension_name'] = 'automatic_updates';
 
@@ -111,6 +116,7 @@ class ProjectInfoTraitTest extends KernelTestBase {
       'package' => 'Core',
       'core' => '8.x',
       'dependencies' => ['system'],
+      'install path' => '',
     ];
     $infos['ctools']['extension_name'] = 'ctools';
 
@@ -132,8 +138,8 @@ class ProjectInfoTestClass {
   /**
    * {@inheritdoc}
    */
-  public function getExtensionVersion($extension_name, array $info) {
-    return $this->getVersion($extension_name, $info);
+  public function getExtensionVersion(array $info) {
+    return $this->getVersion($info);
   }
 
   /**
diff --git a/tests/src/Kernel/ReadinessChecker/MissingProjectInfoTest.php b/tests/src/Kernel/ReadinessChecker/MissingProjectInfoTest.php
index 60e4cf6f4b59eff42d11bb0ea554b25da6e4c823..9929506521758f5aac1ea26e1d2d461132139edb 100644
--- a/tests/src/Kernel/ReadinessChecker/MissingProjectInfoTest.php
+++ b/tests/src/Kernel/ReadinessChecker/MissingProjectInfoTest.php
@@ -64,7 +64,9 @@ class TestMissingProjectInfo extends MissingProjectInfo {
         'description' => 'Handles general site configuration for administrators.',
         'package' => 'Core',
         'version' => 'VERSION',
-        'project' => 'drupal',
+        'packaged' => FALSE,
+        'project' => $this->getProjectName('system', []),
+        'install path' => drupal_get_path('module', 'system'),
         'core' => '8.x',
         'required' => 'true',
         'configure' => 'system.admin_config_system',