diff --git a/doc/repository-spec-v2.md b/doc/repository-spec-v2.md
index b65b4d2..06ad714 100644
--- a/doc/repository-spec-v2.md
+++ b/doc/repository-spec-v2.md
@@ -120,7 +120,7 @@
 The `dart pub` client allows users to save an opaque `<token>` for each
 `<hosted-url>`. When the `dart pub` client makes a request to a `<hosted-url>`
 for which it has a `<token>` stored, it will attach an `Authorization` header
-as follows: 
+as follows:
 
  * `Authorization: Bearer <token>`
 
@@ -229,6 +229,7 @@
     "version": "<version>",
     "retracted": true || false, /* optional field, false if omitted */
     "archive_url": "https://.../archive.tar.gz",
+    "archive_sha256": "95cbaad58e2cf32d1aa852f20af1fcda1820ead92a4b1447ea7ba1ba18195d27"
     "pubspec": {
       /* pubspec contents as JSON object */
     }
@@ -238,6 +239,7 @@
       "version": "<package>",
       "retracted": true || false, /* optional field, false if omitted */
       "archive_url": "https://.../archive.tar.gz",
+      "archive_sha256": "95cbaad58e2cf32d1aa852f20af1fcda1820ead92a4b1447ea7ba1ba18195d27"
       "pubspec": {
         /* pubspec contents as JSON object */
       }
@@ -256,6 +258,15 @@
 other blob storage service. If temporary URLs are returned it is wise to not set
 expiration to less than 25 minutes (to allow for retries and clock drift).
 
+The `archive_sha256` should be the hex-encoded sha256 checksum of the file at
+archive_url. It is an optional field that allows the pub client to verify the
+integrity of the downloaded archive.
+
+The `archive_sha256` also provides an easy way for clients to detect if
+something has changed on the server. In the absense of this field the client can
+still download the archive to obtain a checksum and detect changes to the
+archive.
+
 If `<hosted-url>` for the server returning `archive_url` is a prefix of
 `archive_url`, then the `Authorization: Bearer <token>` is also included when
 `archive_url` is requested. Example: if `https://pub.example.com/path` returns
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 193f345..13c6e70 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -24,6 +24,7 @@
 import '../pubspec_utils.dart';
 import '../solver.dart';
 import '../source/git.dart';
+import '../source/hosted.dart';
 import '../system_cache.dart';
 import '../utils.dart';
 
@@ -357,6 +358,7 @@
         : null;
     final lockFileYaml = lockFile == null ? null : loadYaml(lockFile);
     final lockFileEditor = lockFile == null ? null : YamlEditor(lockFile);
+    final hasContentHashes = _lockFileHasContentHashes(lockFileYaml);
     for (final p in toApply) {
       final targetPackage = p.name;
       final targetVersion = p.version;
@@ -394,6 +396,16 @@
             lockFileYaml['packages'].containsKey(targetPackage)) {
           lockFileEditor.update(
               ['packages', targetPackage, 'version'], targetVersion.toString());
+          // Remove the now outdated content-hash - it will be restored below
+          // after resolution.
+          if (lockFileEditor
+              .parseAt(['packages', targetPackage, 'description'])
+              .value
+              .containsKey('sha256')) {
+            lockFileEditor.remove(
+              ['packages', targetPackage, 'description', 'sha256'],
+            );
+          }
         } else if (targetRevision != null &&
             lockFileYaml['packages'].containsKey(targetPackage)) {
           final ref = entrypoint.lockFile.packages[targetPackage]!.toRef();
@@ -457,8 +469,58 @@
           writeTextFile(entrypoint.pubspecPath, updatedPubspec);
         }
         // Only if we originally had a lock-file we write the resulting lockfile back.
-        if (lockFileEditor != null) {
-          entrypoint.saveLockFile(solveResult);
+        if (updatedLockfile != null) {
+          final updatedPackages = <PackageId>[];
+          for (var package in solveResult.packages) {
+            if (package.isRoot) continue;
+            final description = package.description;
+
+            // Handle content-hashes of hosted dependencies.
+            if (description is ResolvedHostedDescription) {
+              // Ensure we get content-hashes if the original lock-file had
+              // them.
+              if (hasContentHashes) {
+                if (description.sha256 == null) {
+                  // We removed the hash above before resolution - as we get the
+                  // locked id back we need to find the content-hash from the
+                  // version listing.
+                  //
+                  // `pub get` gets this version-listing from the downloaded
+                  // archive but we don't want to download all archives - so we
+                  // copy it from the version listing.
+                  package = (await cache.getVersions(package.toRef()))
+                      .firstWhere((id) => id == package, orElse: () => package);
+                  if ((package.description as ResolvedHostedDescription)
+                          .sha256 ==
+                      null) {
+                    // This happens when we resolved a package from a legacy
+                    // server not providing archive_sha256. As a side-effect of
+                    // downloading the package we compute and store the sha256.
+                    package = await cache.downloadPackage(package);
+                  }
+                }
+              } else {
+                // The original pubspec.lock did not have content-hashes. Remove
+                // any content hash, so we don't start adding them.
+                package = PackageId(
+                  package.name,
+                  package.version,
+                  description.withSha256(null),
+                );
+              }
+            }
+            updatedPackages.add(package);
+          }
+
+          final newLockFile = LockFile(
+            updatedPackages,
+            sdkConstraints: updatedLockfile.sdkConstraints,
+            mainDependencies: pubspec.dependencies.keys.toSet(),
+            devDependencies: pubspec.devDependencies.keys.toSet(),
+            overriddenDependencies: pubspec.dependencyOverrides.keys.toSet(),
+          );
+
+          newLockFile.writeToFile(entrypoint.lockFilePath, cache);
         }
       },
     );
@@ -541,3 +603,23 @@
   }
   return versionRange;
 }
+
+/// `true` iff any of the packages described by the [lockfile] has a
+/// content-hash.
+///
+/// Undefined for invalid lock files, but mostly `true`.
+bool _lockFileHasContentHashes(dynamic lockfile) {
+  if (lockfile is! Map) return true;
+  final packages = lockfile['packages'];
+  if (packages is! Map) return true;
+
+  /// We consider an empty lockfile ready to get content-hashes.
+  if (packages.isEmpty) return true;
+  for (final package in packages.values) {
+    if (package is! Map) return true;
+    final descriptor = package['description'];
+    if (descriptor is! Map) return true;
+    if (descriptor['sha256'] != null) return true;
+  }
+  return false;
+}
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart
index 0be67a0..251028e 100644
--- a/lib/src/command/get.dart
+++ b/lib/src/command/get.dart
@@ -49,6 +49,7 @@
       log.warning(log.yellow(
           'The --packages-dir flag is no longer used and does nothing.'));
     }
+
     await entrypoint.acquireDependencies(
       SolveType.get,
       dryRun: argResults['dry-run'],
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index e45a735..4b182b2 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -10,6 +10,7 @@
 import '../ascii_tree.dart' as tree;
 import '../authentication/client.dart';
 import '../command.dart';
+import '../command_runner.dart';
 import '../exceptions.dart' show DataException;
 import '../exit_codes.dart' as exit_codes;
 import '../http.dart';
@@ -126,12 +127,12 @@
       if (error.statusCode == 401) {
         msg += '$host package repository requested authentication!\n'
             'You can provide credentials using:\n'
-            '    pub token add $host\n';
+            '    $topLevelProgram pub token add $host\n';
       }
       if (error.statusCode == 403) {
         msg += 'Insufficient permissions to the resource at the $host '
             'package repository.\nYou can modify credentials using:\n'
-            '    pub token add $host\n';
+            '    $topLevelProgram pub token add $host\n';
       }
       if (error.serverMessage != null) {
         msg += '\n${error.serverMessage!}\n';
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 6f22fd5..e35fa9c 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -204,7 +204,11 @@
         latestIsOverridden = true;
       }
 
-      final packageStatus = await current?.source.status(current, cache);
+      final packageStatus = await current?.source.status(
+        current.toRef(),
+        current.version,
+        cache,
+      );
       final discontinued =
           packageStatus == null ? false : packageStatus.isDiscontinued;
       final discontinuedReplacedBy = packageStatus?.discontinuedReplacedBy;
diff --git a/lib/src/command/uploader.dart b/lib/src/command/uploader.dart
index 07ffacc..4b6406d 100644
--- a/lib/src/command/uploader.dart
+++ b/lib/src/command/uploader.dart
@@ -27,8 +27,7 @@
 
   UploaderCommand() {
     argParser.addOption('server',
-        defaultsTo: Platform.environment['PUB_HOSTED_URL'] ??
-            'https://pub.dartlang.org',
+        defaultsTo: Platform.environment['PUB_HOSTED_URL'] ?? 'https://pub.dev',
         help: 'The package server on which the package is hosted.\n',
         hide: true);
     argParser.addOption('package',
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 2683cdf..7a1527d 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -8,7 +8,6 @@
 import 'dart:math';
 
 import 'package:collection/collection.dart';
-import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 import 'package:pool/pool.dart';
 import 'package:pub_semver/pub_semver.dart';
@@ -31,6 +30,7 @@
 import 'pubspec.dart';
 import 'sdk.dart';
 import 'solver.dart';
+import 'solver/report.dart';
 import 'source/cached.dart';
 import 'source/unknown.dart';
 import 'system_cache.dart';
@@ -291,11 +291,11 @@
   ///
   /// Performs version resolution according to [SolveType].
   ///
-  /// [useLatest], if provided, defines a list of packages that will be
-  /// unlocked and forced to their latest versions. If [upgradeAll] is
-  /// true, the previous lockfile is ignored and all packages are re-resolved
-  /// from scratch. Otherwise, it will attempt to preserve the versions of all
-  /// previously locked packages.
+  /// [useLatest], if provided, defines a list of packages that will be unlocked
+  /// and forced to their latest versions. If [upgradeAll] is true, the previous
+  /// lockfile is ignored and all packages are re-resolved from scratch.
+  /// Otherwise, it will attempt to preserve the versions of all previously
+  /// locked packages.
   ///
   /// Shows a report of the changes made relative to the previous lockfile. If
   /// this is an upgrade or downgrade, all transitive dependencies are shown in
@@ -305,8 +305,8 @@
   /// If [precompile] is `true` (the default), this snapshots dependencies'
   /// executables.
   ///
-  /// if [onlyReportSuccessOrFailure] is `true` only success or failure will be shown ---
-  /// in case of failure, a reproduction command is shown.
+  /// if [onlyReportSuccessOrFailure] is `true` only success or failure will be
+  /// shown --- in case of failure, a reproduction command is shown.
   ///
   /// Updates [lockFile] and [packageRoot] accordingly.
   Future<void> acquireDependencies(
@@ -365,17 +365,26 @@
       }
     }
 
+    // We have to download files also with --dry-run to ensure we know the
+    // archive hashes for downloaded files.
+    final newLockFile = await result.downloadCachedPackages(cache);
+
+    final report = SolveReport(
+        type, root, lockFile, newLockFile, result.availableVersions, cache,
+        dryRun: dryRun);
     if (!onlyReportSuccessOrFailure) {
-      await result.showReport(type, cache);
+      await report.show();
     }
+    _lockFile = newLockFile;
+
     if (!dryRun) {
-      await result.downloadCachedPackages(cache);
-      saveLockFile(result);
+      newLockFile.writeToFile(lockFilePath, cache);
     }
+
     if (onlyReportSuccessOrFailure) {
       log.message('Got dependencies$suffix.');
     } else {
-      await result.summarizeChanges(type, cache, dryRun: dryRun);
+      await report.summarize();
     }
 
     if (!dryRun) {
@@ -833,21 +842,6 @@
     }
   }
 
-  /// Saves a list of concrete package versions to the `pubspec.lock` file.
-  ///
-  /// Will use Windows line endings (`\r\n`) if a `pubspec.lock` exists, and
-  /// uses that.
-  void saveLockFile(SolveResult result) {
-    _lockFile = result.lockFile;
-
-    final windowsLineEndings = fileExists(lockFilePath) &&
-        detectWindowsLineEndings(readTextFile(lockFilePath));
-
-    final serialized = lockFile.serialize(root.dir);
-    writeTextFile(lockFilePath,
-        windowsLineEndings ? serialized.replaceAll('\n', '\r\n') : serialized);
-  }
-
   /// If the entrypoint uses the old-style `.pub` cache directory, migrates it
   /// to the new-style `.dart_tool/pub` directory.
   void migrateCache() {
@@ -926,22 +920,3 @@
         '"pub" version, please run "$topLevelProgram pub get".');
   }
 }
-
-/// Returns `true` if the [text] looks like it uses windows line endings.
-///
-/// The heuristic used is to count all `\n` in the text and if stricly more than
-/// half of them are preceded by `\r` we report `true`.
-@visibleForTesting
-bool detectWindowsLineEndings(String text) {
-  var index = -1;
-  var unixNewlines = 0;
-  var windowsNewlines = 0;
-  while ((index = text.indexOf('\n', index + 1)) != -1) {
-    if (index != 0 && text[index - 1] == '\r') {
-      windowsNewlines++;
-    } else {
-      unixNewlines++;
-    }
-  }
-  return windowsNewlines > unixNewlines;
-}
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 9d79a16..0cbb15a 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -23,6 +23,7 @@
 import 'sdk/dart.dart';
 import 'solver.dart';
 import 'solver/incompatibility_cause.dart';
+import 'solver/report.dart';
 import 'source/cached.dart';
 import 'source/git.dart';
 import 'source/hosted.dart';
@@ -178,7 +179,7 @@
     final tempDir = cache.createTempDir();
     // TODO(rnystrom): Look in "bin" and display list of binaries that
     // user can run.
-    _writeLockFile(tempDir, LockFile([id]));
+    LockFile([id]).writeToFile(p.join(tempDir, 'pubspec.lock'), cache);
 
     tryDeleteEntry(_packageDir(name));
     tryRenameDir(tempDir, _packageDir(name));
@@ -223,10 +224,11 @@
     // We want the entrypoint to be rooted at 'dep' not the dummy-package.
     result.packages.removeWhere((id) => id.name == 'pub global activate');
 
-    final sameVersions = originalLockFile != null &&
-        originalLockFile.samePackageIds(result.lockFile);
+    final lockFile = await result.downloadCachedPackages(cache);
+    final sameVersions =
+        originalLockFile != null && originalLockFile.samePackageIds(lockFile);
 
-    final PackageId id = result.lockFile.packages[name]!;
+    final PackageId id = lockFile.packages[name]!;
     if (sameVersions) {
       log.message('''
 The package $name is already activated at newest available version.
@@ -234,13 +236,20 @@
 ''');
     } else {
       // Only precompile binaries if we have a new resolution.
-      if (!silent) await result.showReport(SolveType.get, cache);
+      if (!silent) {
+        await SolveReport(
+          SolveType.get,
+          root,
+          originalLockFile ?? LockFile.empty(),
+          lockFile,
+          result.availableVersions,
+          cache,
+          dryRun: false,
+        ).show();
+      }
 
-      await result.downloadCachedPackages(cache);
-
-      final lockFile = result.lockFile;
       final tempDir = cache.createTempDir();
-      _writeLockFile(tempDir, lockFile);
+      lockFile.writeToFile(p.join(tempDir, 'pubspec.lock'), cache);
 
       // Load the package graph from [result] so we don't need to re-parse all
       // the pubspecs.
@@ -263,7 +272,7 @@
     final entrypoint = Entrypoint.global(
       _packageDir(id.name),
       cache.loadCached(id),
-      result.lockFile,
+      lockFile,
       cache,
       solveResult: result,
     );
@@ -276,11 +285,6 @@
     if (!silent) log.message('Activated ${_formatPackage(id)}.');
   }
 
-  /// Finishes activating package [package] by saving [lockFile] in the cache.
-  void _writeLockFile(String dir, LockFile lockFile) {
-    writeTextFile(p.join(dir, 'pubspec.lock'), lockFile.serialize(null));
-  }
-
   /// Shows the user the currently active package with [name], if any.
   LockFile? _describeActive(String name, SystemCache cache) {
     late final LockFile lockFile;
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index 6c66224..343388d 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -5,6 +5,7 @@
 import 'dart:convert';
 
 import 'package:collection/collection.dart' hide mapMap;
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 import 'package:source_span/source_span.dart';
@@ -29,15 +30,15 @@
 
   /// Dependency names that appeared in the root package's `dependencies`
   /// section.
-  final Set<String> _mainDependencies;
+  final Set<String> mainDependencies;
 
   /// Dependency names that appeared in the root package's `dev_dependencies`
   /// section.
-  final Set<String> _devDependencies;
+  final Set<String> devDependencies;
 
   /// Dependency names that appeared in the root package's
   /// `dependency_overrides` section.
-  final Set<String> _overriddenDependencies;
+  final Set<String> overriddenDependencies;
 
   /// Creates a new lockfile containing [ids].
   ///
@@ -59,20 +60,16 @@
             devDependencies ?? const UnmodifiableSetView.empty(),
             overriddenDependencies ?? const UnmodifiableSetView.empty());
 
-  LockFile._(
-      Map<String, PackageId> packages,
-      this.sdkConstraints,
-      this._mainDependencies,
-      this._devDependencies,
-      this._overriddenDependencies)
+  LockFile._(Map<String, PackageId> packages, this.sdkConstraints,
+      this.mainDependencies, this.devDependencies, this.overriddenDependencies)
       : packages = UnmodifiableMapView(packages);
 
   LockFile.empty()
       : packages = const {},
         sdkConstraints = {'dart': VersionConstraint.any},
-        _mainDependencies = const UnmodifiableSetView.empty(),
-        _devDependencies = const UnmodifiableSetView.empty(),
-        _overriddenDependencies = const UnmodifiableSetView.empty();
+        mainDependencies = const UnmodifiableSetView.empty(),
+        devDependencies = const UnmodifiableSetView.empty(),
+        overriddenDependencies = const UnmodifiableSetView.empty();
 
   /// Loads a lockfile from [filePath].
   factory LockFile.load(String filePath, SourceRegistry sources) {
@@ -274,8 +271,13 @@
 
     var packages = Map<String, PackageId>.from(this.packages);
     packages.remove(name);
-    return LockFile._(packages, sdkConstraints, _mainDependencies,
-        _devDependencies, _overriddenDependencies);
+    return LockFile._(
+      packages,
+      sdkConstraints,
+      mainDependencies,
+      devDependencies,
+      overriddenDependencies,
+    );
   }
 
   /// Returns the contents of the `.dart_tool/package_config` file generated
@@ -341,20 +343,18 @@
   /// [packageDir] is the containing directory of the root package, used to
   /// serialize relative path package descriptions. If it is null, they will be
   /// serialized as absolute.
-  String serialize(String? packageDir) {
+  String serialize(String? packageDir, SystemCache cache) {
     // Convert the dependencies to a simple object.
     var packageMap = {};
-    packages.forEach((name, package) {
-      var description =
-          package.description.serializeForLockfile(containingDir: packageDir);
-
-      packageMap[name] = {
-        'version': package.version.toString(),
-        'source': package.source.name,
-        'description': description,
-        'dependency': _dependencyType(package.name)
+    for (final id in packages.values) {
+      packageMap[id.name] = {
+        'version': id.version.toString(),
+        'source': id.source.name,
+        'description':
+            id.description.serializeForLockfile(containingDir: packageDir),
+        'dependency': _dependencyType(id.name)
       };
-    });
+    }
 
     var data = {
       'sdks': mapMap(sdkConstraints,
@@ -368,6 +368,21 @@
 ''';
   }
 
+  /// Saves the list of concrete package versions to [lockFilePath].
+  ///
+  /// Will use Windows line endings (`\r\n`) if the file already exists, and
+  /// uses that.
+  ///
+  /// Relative paths will be resolved relative to [lockFilePath]
+  void writeToFile(String lockFilePath, SystemCache cache) {
+    final windowsLineEndings = fileExists(lockFilePath) &&
+        detectWindowsLineEndings(readTextFile(lockFilePath));
+
+    final serialized = serialize(p.dirname(lockFilePath), cache);
+    writeTextFile(lockFilePath,
+        windowsLineEndings ? serialized.replaceAll('\n', '\r\n') : serialized);
+  }
+
   static const _directMain = 'direct main';
   static const _directDev = 'direct dev';
   static const _directOverridden = 'direct overridden';
@@ -375,12 +390,12 @@
 
   /// Returns the dependency classification for [package].
   String _dependencyType(String package) {
-    if (_mainDependencies.contains(package)) return _directMain;
-    if (_devDependencies.contains(package)) return _directDev;
+    if (mainDependencies.contains(package)) return _directMain;
+    if (devDependencies.contains(package)) return _directDev;
 
     // If a package appears in `dependency_overrides` and another dependency
     // section, the main section it appears in takes precedence.
-    if (_overriddenDependencies.contains(package)) {
+    if (overriddenDependencies.contains(package)) {
       return _directOverridden;
     }
     return _transitive;
@@ -399,3 +414,22 @@
     return true;
   }
 }
+
+/// Returns `true` if the [text] looks like it uses windows line endings.
+///
+/// The heuristic used is to count all `\n` in the text and if stricly more than
+/// half of them are preceded by `\r` we report `true`.
+@visibleForTesting
+bool detectWindowsLineEndings(String text) {
+  var index = -1;
+  var unixNewlines = 0;
+  var windowsNewlines = 0;
+  while ((index = text.indexOf('\n', index + 1)) != -1) {
+    if (index != 0 && text[index - 1] == '\r') {
+      windowsNewlines++;
+    } else {
+      unixNewlines++;
+    }
+  }
+  return windowsNewlines > unixNewlines;
+}
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index 802a8c2..2159847 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -212,7 +212,7 @@
   return path.join(cache.rootDir, 'credentials.json');
 }
 
-/// Gets the user to authorize pub as a client of pub.dartlang.org via oauth2.
+/// Gets the user to authorize pub as a client of pub.dev via oauth2.
 ///
 /// Returns a Future that completes to a fully-authorized [Client].
 Future<Client> _authorize() async {
@@ -241,7 +241,7 @@
     completer
         .complete(grant.handleAuthorizationResponse(queryToMap(queryString)));
 
-    return shelf.Response.found('https://pub.dartlang.org/authorized');
+    return shelf.Response.found('https://pub.dev/authorized');
   });
 
   var authUrl = grant.getAuthorizationUrl(
diff --git a/lib/src/solver/report.dart b/lib/src/solver/report.dart
index dab29c6..176ab67 100644
--- a/lib/src/solver/report.dart
+++ b/lib/src/solver/report.dart
@@ -10,6 +10,7 @@
 import '../log.dart' as log;
 import '../package.dart';
 import '../package_name.dart';
+import '../source/hosted.dart';
 import '../source/root.dart';
 import '../system_cache.dart';
 import '../utils.dart';
@@ -25,42 +26,114 @@
   final SolveType _type;
   final Package _root;
   final LockFile _previousLockFile;
-  final SolveResult _result;
+  final LockFile _newLockFile;
   final SystemCache _cache;
+  final bool _dryRun;
 
-  /// The dependencies in [_result], keyed by package name.
-  final _dependencies = <String, PackageId>{};
+  /// The available versions of all selected packages from their source.
+  ///
+  /// An entry here may not include the full list of versions available if the
+  /// given package was locked and did not need to be unlocked during the solve.
+  ///
+  /// Version list will not contain any retracted package versions.
+  final Map<String, List<Version>> _availableVersions;
 
   final _output = StringBuffer();
 
-  SolveReport(this._type, this._root, this._previousLockFile, this._result,
-      this._cache) {
-    // Fill the map so we can use it later.
-    for (var id in _result.packages) {
-      _dependencies[id.name] = id;
-    }
-  }
+  SolveReport(
+    this._type,
+    this._root,
+    this._previousLockFile,
+    this._newLockFile,
+    this._availableVersions,
+    this._cache, {
+    required bool dryRun,
+  }) : _dryRun = dryRun;
 
-  /// Displays a report of the results of the version resolution relative to
-  /// the previous lock file.
+  /// Displays a report of the results of the version resolution in
+  /// [_newLockFile] relative to the [_previousLockFile] file.
   Future<void> show() async {
     await _reportChanges();
     await _reportOverrides();
+    _checkContentHashesMatchOldLockfile();
+  }
+
+  void _checkContentHashesMatchOldLockfile() {
+    final issues = <String>[];
+
+    final newPackageNames = _newLockFile.packages.keys.toSet();
+    final oldPackageNames = _previousLockFile.packages.keys.toSet();
+    // We only care about packages that exist in both new and old lockfile.
+    for (final name in newPackageNames.intersection(oldPackageNames)) {
+      final newId = _newLockFile.packages[name]!;
+      final oldId = _previousLockFile.packages[name]!;
+
+      // We only care about hosted packages
+      final newDescription = newId.description;
+      final oldDescription = oldId.description;
+      if (newDescription is! ResolvedHostedDescription ||
+          oldDescription is! ResolvedHostedDescription) {
+        continue;
+      }
+
+      // We don't care about changes in the hash if the version number changed!
+      if (newId.version != oldId.version) {
+        continue;
+      }
+
+      // Use the cached content-hashes after downloading to ensure that
+      // content-hashes from legacy servers gets used.
+      final cachedHash = newDescription.sha256;
+      assert(cachedHash != null);
+
+      // Ignore cases where the old lockfile doesn't have a content-hash
+      final oldHash = oldDescription.sha256;
+      if (oldHash == null) {
+        continue;
+      }
+
+      if (!fixedTimeBytesEquals(cachedHash, oldHash)) {
+        issues.add(
+          '$name-${newId.version} from "${newDescription.description.url}"',
+        );
+      }
+    }
+
+    if (issues.isNotEmpty) {
+      log.warning('''
+The existing content-hash from pubspec.lock doesn't match contents for:
+ * ${issues.join('\n * ')}
+
+This indicates one of:
+ * The content has changed on the server since you created the pubspec.lock.
+ * The pubspec.lock has been corrupted.
+${_dryRun ? '' : '\nThe content-hashes in pubspec.lock has been updated.'}
+
+For more information see:
+$contentHashesDocumentationUrl
+''');
+    }
   }
 
   /// Displays a one-line message summarizing what changes were made (or would
   /// be made) to the lockfile.
   ///
   /// If [dryRun] is true, describes it in terms of what would be done.
-  void summarize({bool dryRun = false}) {
+  ///
+  /// [type] is the type of version resolution that was run.
+
+  /// If [type] is `SolveType.UPGRADE` it also shows the number of packages that
+  /// are not at the latest available version and the number of outdated
+  /// packages.
+  Future<void> summarize() async {
     // Count how many dependencies actually changed.
-    var dependencies = _dependencies.keys.toSet();
+    var dependencies = _newLockFile.packages.keys.toSet();
     dependencies.addAll(_previousLockFile.packages.keys);
     dependencies.remove(_root.name);
 
     var numChanged = dependencies.where((name) {
       var oldId = _previousLockFile.packages[name];
-      var newId = _dependencies[name];
+      var newId = _newLockFile.packages[name];
 
       // Added or removed dependencies count.
       if (oldId == null) return true;
@@ -78,7 +151,7 @@
       }
     }
 
-    if (dryRun) {
+    if (_dryRun) {
       if (numChanged == 0) {
         log.message('No dependencies would change$suffix.');
       } else if (numChanged == 1) {
@@ -99,6 +172,10 @@
         log.message('Changed $numChanged dependencies$suffix!');
       }
     }
+    if (_type == SolveType.upgrade) {
+      await reportDiscontinued();
+      reportOutdated();
+    }
   }
 
   /// Displays a report of all of the previous and current dependencies and
@@ -107,7 +184,7 @@
     _output.clear();
 
     // Show the new set of dependencies ordered by name.
-    var names = _result.packages.map((id) => id.name).toList();
+    var names = _newLockFile.packages.keys.toList();
     names.remove(_root.name);
     names.sort();
     for (final name in names) {
@@ -146,10 +223,10 @@
   /// if discontinued packages are detected.
   Future<void> reportDiscontinued() async {
     var numDiscontinued = 0;
-    for (var id in _result.packages) {
+    for (var id in _newLockFile.packages.values) {
       if (id.description is RootDescription) continue;
-      final status =
-          await id.source.status(id, _cache, maxAge: Duration(days: 3));
+      final status = await id.source
+          .status(id.toRef(), id.version, _cache, maxAge: Duration(days: 3));
       if (status.isDiscontinued &&
           (_root.dependencyType(id.name) == DependencyType.direct ||
               _root.dependencyType(id.name) == DependencyType.dev)) {
@@ -168,8 +245,8 @@
   /// Displays a two-line message, number of outdated packages and an
   /// instruction to run `pub outdated` if outdated packages are detected.
   void reportOutdated() {
-    final outdatedPackagesCount = _result.packages.where((id) {
-      final versions = _result.availableVersions[id.name]!;
+    final outdatedPackagesCount = _newLockFile.packages.values.where((id) {
+      final versions = _availableVersions[id.name]!;
       // A version is counted:
       // - if there is a newer version which is not a pre-release and current
       // version is also not a pre-release or,
@@ -198,7 +275,7 @@
   /// "(override)" next to overridden packages.
   Future<void> _reportPackage(String name,
       {bool alwaysShow = false, bool highlightOverride = true}) async {
-    var newId = _dependencies[name];
+    var newId = _newLockFile.packages[name];
     var oldId = _previousLockFile.packages[name];
     var id = newId ?? oldId!;
 
@@ -218,6 +295,7 @@
     //     + The package was added.
     //     > The package was upgraded from a lower version.
     //     < The package was downgraded from a higher version.
+    //     ~ Package contents has changed, but not the version number.
     //     * Any other change between the old and new package.
     String icon;
     if (isOverridden) {
@@ -228,7 +306,8 @@
     } else if (oldId == null) {
       icon = log.green('+ ');
       addedOrRemoved = true;
-    } else if (oldId.description != newId.description) {
+    } else if (oldId.description.description != newId.description.description) {
+      // Eg. a changed source in pubspec.yaml.
       icon = log.cyan('* ');
       changed = true;
     } else if (oldId.version < newId.version) {
@@ -237,6 +316,10 @@
     } else if (oldId.version > newId.version) {
       icon = log.cyan('< ');
       changed = true;
+    } else if (oldId.description != newId.description) {
+      // Eg. a changed hash or revision.
+      icon = log.cyan('~ ');
+      changed = true;
     } else {
       // Unchanged.
       icon = '  ';
@@ -245,7 +328,7 @@
     // See if there are any newer versions of the package that we were
     // unable to upgrade to.
     if (newId != null && _type != SolveType.downgrade) {
-      var versions = _result.availableVersions[newId.name]!;
+      var versions = _availableVersions[newId.name]!;
 
       var newerStable = false;
       var newerUnstable = false;
@@ -259,8 +342,12 @@
           }
         }
       }
-      final status =
-          await id.source.status(id, _cache, maxAge: Duration(days: 3));
+      final status = await id.source.status(
+        id.toRef(),
+        id.version,
+        _cache,
+        maxAge: Duration(days: 3),
+      );
 
       if (status.isRetracted) {
         if (newerStable) {
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index df3aa4a..d958b13 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -16,8 +16,6 @@
 import '../source/cached.dart';
 import '../source/hosted.dart';
 import '../system_cache.dart';
-import 'report.dart';
-import 'type.dart';
 
 /// The result of a successful version resolution.
 class SolveResult {
@@ -50,9 +48,37 @@
   /// The wall clock time the resolution took.
   final Duration resolutionTime;
 
-  /// The [LockFile] representing the packages selected by this version
-  /// resolution.
-  LockFile get lockFile {
+  /// Downloads all the cached packages selected by this version resolution.
+  ///
+  /// If some already cached package differs from what is provided by the server
+  /// (according to the content-hash) a warning is printed and the package is
+  /// redownloaded.
+  ///
+  /// Returns the [LockFile] representing the packages selected by this version
+  /// resolution. Any resolved [PackageId]s will correspond to those in the
+  /// cache (and thus to the one provided by the server).
+  ///
+  /// If there is a mismatch between the previous content-hash from pubspec.lock
+  /// and the new one a warning will be printed but the new one will be
+  /// returned.
+  Future<LockFile> downloadCachedPackages(SystemCache cache) async {
+    final resolvedPackageIds = await Future.wait(
+      packages.map((id) async {
+        if (id.source is CachedSource) {
+          return await withDependencyType(_root.dependencyType(id.name),
+              () async {
+            return await cache.downloadPackage(
+              id,
+            );
+          });
+        }
+        return id;
+      }),
+    );
+
+    // Invariant: the content-hashes in PUB_CACHE matches those provided by the
+    // server.
+
     // Don't factor in overridden dependencies' SDK constraints, because we'll
     // accept those packages even if their constraints don't match.
     var nonOverrides = pubspecs.values
@@ -67,27 +93,17 @@
             .intersect(sdkConstraints[identifier] ?? VersionConstraint.any);
       });
     }
-
-    return LockFile(packages,
-        sdkConstraints: sdkConstraints,
-        mainDependencies: MapKeySet(_root.dependencies),
-        devDependencies: MapKeySet(_root.devDependencies),
-        overriddenDependencies: MapKeySet(_root.dependencyOverrides));
+    return LockFile(
+      resolvedPackageIds,
+      sdkConstraints: sdkConstraints,
+      mainDependencies: MapKeySet(_root.dependencies),
+      devDependencies: MapKeySet(_root.devDependencies),
+      overriddenDependencies: MapKeySet(_root.dependencyOverrides),
+    );
   }
 
   final LockFile _previousLockFile;
 
-  /// Downloads all cached packages in [packages].
-  Future<void> downloadCachedPackages(SystemCache cache) async {
-    await Future.wait(packages.map((id) async {
-      final source = id.source;
-      if (source is! CachedSource) return;
-      return await withDependencyType(_root.dependencyType(id.name), () async {
-        await source.downloadToSystemCache(id, cache);
-      });
-    }));
-  }
-
   /// Returns the names of all packages that were changed.
   ///
   /// This includes packages that were added or removed.
@@ -105,30 +121,6 @@
   SolveResult(this._root, this._previousLockFile, this.packages, this.pubspecs,
       this.availableVersions, this.attemptedSolutions, this.resolutionTime);
 
-  /// Displays a report of what changes were made to the lockfile.
-  ///
-  /// [type] is the type of version resolution that was run.
-  Future<void> showReport(SolveType type, SystemCache cache) async {
-    await SolveReport(type, _root, _previousLockFile, this, cache).show();
-  }
-
-  /// Displays a one-line message summarizing what changes were made (or would
-  /// be made) to the lockfile.
-  ///
-  /// If [type] is `SolveType.UPGRADE` it also shows the number of packages
-  /// that are not at the latest available version.
-  ///
-  /// [type] is the type of version resolution that was run.
-  Future<void> summarizeChanges(SolveType type, SystemCache cache,
-      {bool dryRun = false}) async {
-    final report = SolveReport(type, _root, _previousLockFile, this, cache);
-    report.summarize(dryRun: dryRun);
-    if (type == SolveType.upgrade) {
-      await report.reportDiscontinued();
-      report.reportOutdated();
-    }
-  }
-
   /// Send analytics about the package resolution.
   void sendAnalytics(PubAnalytics pubAnalytics) {
     ArgumentError.checkNotNull(pubAnalytics);
diff --git a/lib/src/source.dart b/lib/src/source.dart
index a918c68..e7714eb 100644
--- a/lib/src/source.dart
+++ b/lib/src/source.dart
@@ -133,7 +133,7 @@
   String doGetDirectory(PackageId id, SystemCache cache,
       {String? relativeFrom});
 
-  /// Returns metadata about a given package.
+  /// Returns metadata about a given package-version.
   ///
   /// For remotely hosted packages, the information can be cached for up to
   /// [maxAge]. If [maxAge] is not given, the information is not cached.
@@ -141,7 +141,8 @@
   /// In the case of offline sources, [maxAge] is not used, since information is
   /// per definition cached.
   Future<PackageStatus> status(
-    PackageId id,
+    PackageRef ref,
+    Version version,
     SystemCache cache, {
     Duration? maxAge,
   }) async {
diff --git a/lib/src/source/cached.dart b/lib/src/source/cached.dart
index 16a7313..54630c7 100644
--- a/lib/src/source/cached.dart
+++ b/lib/src/source/cached.dart
@@ -55,7 +55,7 @@
       dirExists(getDirectoryInCache(id, cache));
 
   /// Downloads the package identified by [id] to the system cache.
-  Future<Package> downloadToSystemCache(PackageId id, SystemCache cache);
+  Future<PackageId> downloadToSystemCache(PackageId id, SystemCache cache);
 
   /// Returns the [Package]s that have been downloaded to the system cache.
   List<Package> getCachedPackages(SystemCache cache);
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index efd085e..24b1294 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -188,7 +188,7 @@
   ///
   /// This lets us avoid race conditions when getting multiple different
   /// packages from the same repository.
-  final _revisionCacheClones = <String, Future>{};
+  final _revisionCacheClones = <String, Future<void>>{};
 
   /// The paths to the canonical clones of repositories for which "git fetch"
   /// has already been run during this run of pub.
@@ -298,7 +298,10 @@
   /// itself; each of the commit-specific directories are clones of a directory
   /// in `cache/`.
   @override
-  Future<Package> downloadToSystemCache(PackageId id, SystemCache cache) async {
+  Future<PackageId> downloadToSystemCache(
+    PackageId id,
+    SystemCache cache,
+  ) async {
     return await _pool.withResource(() async {
       final ref = id.toRef();
       final description = ref.description;
@@ -327,12 +330,7 @@
           _updatePackageList(revisionCachePath, path);
         }
       });
-
-      return Package.load(
-        id.name,
-        p.join(revisionCachePath, p.fromUri(path)),
-        cache.sources,
-      );
+      return id;
     });
   }
 
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 451dce1..4e8a939 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -9,7 +9,8 @@
 import 'dart:typed_data';
 
 import 'package:collection/collection.dart'
-    show maxBy, IterableNullableExtension;
+    show IterableExtension, IterableNullableExtension, ListEquality, maxBy;
+import 'package:crypto/crypto.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
@@ -32,6 +33,8 @@
 import '../utils.dart';
 import 'cached.dart';
 
+const contentHashesDocumentationUrl = 'https://dart.dev/go/content-hashes';
+
 /// Validates and normalizes a [hostedUrl] which is pointing to a pub server.
 ///
 /// A [hostedUrl] is a URL pointing to a _hosted pub server_ as defined by the
@@ -44,8 +47,8 @@
 /// unless the path is merely `/`, in which case we normalize to the bare
 /// domain.
 ///
-/// We change `https://pub.dev` to `https://pub.dartlang.org`, this  maintains
-/// avoids churn for `pubspec.lock`-files which contain
+/// We change `https://pub.dartlang.org` to `https://pub.dev`, this  maintains
+/// backwards compatibility with `pubspec.lock`-files which contain
 /// `https://pub.dartlang.org`.
 ///
 /// Throws [FormatException] if there is anything wrong [hostedUrl].
@@ -102,9 +105,9 @@
   //
   // Clearly, a bit of investigation is necessary before we update this to
   // pub.dev, it might be attractive to do next time we change the server API.
-  if (u == Uri.parse('https://pub.dev')) {
-    log.fine('Using https://pub.dartlang.org instead of https://pub.dev.');
-    u = Uri.parse('https://pub.dartlang.org');
+  if (u == Uri.parse('https://pub.dartlang.org')) {
+    log.fine('Using https://pub.dev instead of https://pub.dartlang.org.');
+    u = Uri.parse('https://pub.dev');
   }
   return u;
 }
@@ -148,7 +151,7 @@
     // Clearly, a bit of investigation is necessary before we update this to
     // pub.dev, it might be attractive to do next time we change the server API.
     try {
-      var defaultHostedUrl = 'https://pub.dartlang.org';
+      var defaultHostedUrl = 'https://pub.dev';
       // Allow the defaultHostedUrl to be overriden when running from tests
       if (runningFromTest) {
         defaultHostedUrl =
@@ -174,24 +177,6 @@
     return PackageRef(name, d);
   }
 
-  /// Returns an ID for a hosted package named [name] at [version].
-  ///
-  /// If [url] is passed, it's the URL of the pub server from which the package
-  /// should be downloaded. [url] most be normalized and validated using
-  /// [validateAndNormalizeHostedUrl].
-  PackageId idFor(
-    String name,
-    Version version, {
-    String? url,
-  }) =>
-      PackageId(
-        name,
-        version,
-        ResolvedHostedDescription(
-          HostedDescription(name, url ?? defaultUrl.toString()),
-        ),
-      );
-
   /// Ensures that [description] is a valid hosted package description.
   ///
   /// Simple hosted dependencies only consist of a plain string, which is
@@ -221,7 +206,10 @@
       return PackageId(
         name,
         version,
-        ResolvedHostedDescription(HostedDescription(name, defaultUrl)),
+        ResolvedHostedDescription(
+          HostedDescription(name, defaultUrl),
+          sha256: null,
+        ),
       );
     }
     if (description is! Map) {
@@ -231,6 +219,10 @@
     if (url is! String) {
       throw FormatException('The url should be a string.');
     }
+    final sha256 = description['sha256'];
+    if (sha256 != null && sha256 is! String) {
+      throw FormatException('The sha256 should be a string.');
+    }
     final foundName = description['name'];
     if (foundName is! String) {
       throw FormatException('The name should be a string.');
@@ -243,6 +235,7 @@
       version,
       ResolvedHostedDescription(
         HostedDescription(name, Uri.parse(url).toString()),
+        sha256: sha256 == null ? null : hexDecode(sha256),
       ),
     );
   }
@@ -319,48 +312,53 @@
   static final RegExp _looksLikePackageName =
       RegExp(r'^[a-zA-Z_]+[a-zA-Z0-9_]*$');
 
-  late final RateLimitedScheduler<_RefAndCache, Map<PackageId, _VersionInfo>?>
-      _scheduler = RateLimitedScheduler(
+  late final RateLimitedScheduler<_RefAndCache, List<_VersionInfo>> _scheduler =
+      RateLimitedScheduler(
     _fetchVersions,
     maxConcurrentOperations: 10,
   );
 
-  Map<PackageId, _VersionInfo> _versionInfoFromPackageListing(
+  List<_VersionInfo> _versionInfoFromPackageListing(
       Map body, PackageRef ref, Uri location, SystemCache cache) {
     final description = ref.description;
     if (description is! HostedDescription) {
       throw ArgumentError('Wrong source');
     }
     final versions = body['versions'];
-    if (versions is List) {
-      return Map.fromEntries(versions.map((map) {
-        final pubspecData = map['pubspec'];
-        if (pubspecData is Map) {
-          var pubspec = Pubspec.fromMap(pubspecData, cache.sources,
-              expectedName: ref.name, location: location);
-          var id = idFor(
-            ref.name,
-            pubspec.version,
-            url: description.url,
-          );
-          var archiveUrl = map['archive_url'];
-          if (archiveUrl is String) {
-            final status = PackageStatus(
-                isDiscontinued: body['isDiscontinued'] ?? false,
-                discontinuedReplacedBy: body['replacedBy'],
-                isRetracted: map['retracted'] ?? false);
-            return MapEntry(
-                id, _VersionInfo(pubspec, Uri.parse(archiveUrl), status));
-          }
-          throw FormatException('archive_url must be a String');
-        }
-        throw FormatException('pubspec must be a map');
-      }));
+    if (versions is! List) {
+      throw FormatException('versions must be a list');
     }
-    throw FormatException('versions must be a list');
+    return versions.map((map) {
+      final pubspecData = map['pubspec'];
+      if (pubspecData is! Map) {
+        throw FormatException('pubspec must be a map');
+      }
+      var pubspec = Pubspec.fromMap(pubspecData, cache.sources,
+          expectedName: ref.name, location: location);
+      final archiveSha256 = map['archive_sha256'];
+      if (archiveSha256 != null && archiveSha256 is! String) {
+        throw FormatException('archive_sha256 must be a String');
+      }
+      final archiveUrl = map['archive_url'];
+      if (archiveUrl is! String) {
+        throw FormatException('archive_url must be a String');
+      }
+      final status = PackageStatus(
+        isDiscontinued: body['isDiscontinued'] ?? false,
+        discontinuedReplacedBy: body['replacedBy'],
+        isRetracted: map['retracted'] ?? false,
+      );
+      return _VersionInfo(
+        pubspec.version,
+        pubspec,
+        Uri.parse(archiveUrl),
+        status,
+        archiveSha256 == null ? null : hexDecode(archiveSha256),
+      );
+    }).toList();
   }
 
-  Future<Map<PackageId, _VersionInfo>?> _fetchVersionsNoPrefetching(
+  Future<List<_VersionInfo>> _fetchVersionsNoPrefetching(
       PackageRef ref, SystemCache cache) async {
     final description = ref.description;
 
@@ -371,9 +369,9 @@
     final url = _listVersionsUrl(ref);
     log.io('Get versions from $url.');
 
-    late final String bodyText;
-    late final dynamic body;
-    late final Map<PackageId, _VersionInfo> result;
+    final String bodyText;
+    final dynamic body;
+    final List<_VersionInfo> result;
     try {
       // TODO(sigurdm): Implement cancellation of requests. This probably
       // requires resolution of: https://github.com/dart-lang/sdk/issues/22265.
@@ -401,8 +399,7 @@
     return result;
   }
 
-  Future<Map<PackageId, _VersionInfo>?> _fetchVersions(
-      _RefAndCache refAndCache) async {
+  Future<List<_VersionInfo>> _fetchVersions(_RefAndCache refAndCache) async {
     final ref = refAndCache.ref;
     final description = ref.description;
     if (description is! HostedDescription) {
@@ -414,16 +411,13 @@
     /// Prefetch the dependencies of the latest version, we are likely to need
     /// them later.
     void prescheduleDependenciesOfLatest(
-      Map<PackageId, _VersionInfo>? listing,
+      List<_VersionInfo>? listing,
       SystemCache cache,
     ) {
-      if (listing == null) return;
+      if (listing == null || listing.isEmpty) return;
       final latestVersion =
-          maxBy(listing.keys.map((id) => id.version), (e) => e)!;
-      final latestVersionId = PackageId(
-          ref.name, latestVersion, ResolvedHostedDescription(description));
-      final dependencies =
-          listing[latestVersionId]?.pubspec.dependencies.values ?? [];
+          maxBy<_VersionInfo, Version>(listing, (e) => e.version)!;
+      final dependencies = latestVersion.pubspec.dependencies.values;
       unawaited(withDependencyType(DependencyType.none, () async {
         for (final packageRange in dependencies) {
           if (packageRange.source is HostedSource) {
@@ -456,8 +450,7 @@
   /// Invariant: Entries in this cache are the parsed version of the exact same
   ///  information cached on disk. I.e. if the entry is present in this cache,
   /// there will not be a newer version on disk.
-  final Map<PackageRef, Pair<DateTime, Map<PackageId, _VersionInfo>>>
-      _responseCache = {};
+  final Map<PackageRef, Pair<DateTime, List<_VersionInfo>>> _responseCache = {};
 
   /// If a cached version listing response for [ref] exists on disk and is less
   /// than [maxAge] old it is parsed and returned.
@@ -466,7 +459,7 @@
   ///
   /// If [maxAge] is not given, we will try to get the cached version no matter
   /// how old it is.
-  Future<Map<PackageId, _VersionInfo>?> _cachedVersionListingResponse(
+  Future<List<_VersionInfo>?> _cachedVersionListingResponse(
       PackageRef ref, SystemCache cache,
       {Duration? maxAge}) async {
     if (_responseCache.containsKey(ref)) {
@@ -544,26 +537,39 @@
   }
 
   @override
-  Future<PackageStatus> status(PackageId id, SystemCache cache,
-      {Duration? maxAge}) async {
+  Future<PackageStatus> status(
+    PackageRef ref,
+    Version version,
+    SystemCache cache, {
+    Duration? maxAge,
+  }) async {
+    // If we don't have the specific version we return the empty response, since
+    // it is more or less harmless..
+    //
+    // This can happen if the connection is broken, or the server is faulty.
+    // We want to avoid a crash
+    //
+    // TODO(sigurdm): Consider representing the non-existence of the
+    // package-version in the return value.
+    return (await _versionInfo(ref, version, cache, maxAge: maxAge))?.status ??
+        PackageStatus();
+  }
+
+  Future<_VersionInfo?> _versionInfo(
+    PackageRef ref,
+    Version version,
+    SystemCache cache, {
+    Duration? maxAge,
+  }) async {
     if (cache.isOffline) {
       // Do we have a cached version response on disk?
-      final versionListing =
-          await _cachedVersionListingResponse(id.toRef(), cache);
+      final versionListing = await _cachedVersionListingResponse(ref, cache);
 
       if (versionListing == null) {
-        return PackageStatus();
+        return null;
       }
-      // If we don't have the specific version we return the empty response.
-      //
-      // This should not happen. But in production we want to avoid a crash, since
-      // it is more or less harmless.
-      //
-      // TODO(sigurdm): Consider representing the non-existence of the
-      // package-version in the return value.
-      return versionListing[id]?.status ?? PackageStatus();
+      return versionListing.firstWhereOrNull((l) => l.version == version);
     }
-    final ref = id.toRef();
     // Did we already get info for this package?
     var versionListing = _scheduler.peek(_RefAndCache(ref, cache));
     if (maxAge != null) {
@@ -576,20 +582,11 @@
         .schedule(_RefAndCache(ref, cache))
         // Failures retrieving the listing here should just be ignored.
         .catchError(
-          (_) => <PackageId, _VersionInfo>{},
+          (_) async => <_VersionInfo>[],
           test: (error) => error is Exception,
         );
 
-    final listing = versionListing![id];
-    // If we don't have the specific version we return the empty response, since
-    // it is more or less harmless..
-    //
-    // This can happen if the connection is broken, or the server is faulty.
-    // We want to avoid a crash
-    //
-    // TODO(sigurdm): Consider representing the non-existence of the
-    // package-version in the return value.
-    return listing?.status ?? PackageStatus();
+    return versionListing.firstWhereOrNull((l) => l.version == version);
   }
 
   // The path where the response from the package-listing api is cached.
@@ -652,7 +649,18 @@
           await _cachedVersionListingResponse(ref, cache, maxAge: maxAge);
     }
     versionListing ??= await _scheduler.schedule(_RefAndCache(ref, cache));
-    return versionListing!.keys.toList();
+    return versionListing
+        .map(
+          (i) => PackageId(
+            ref.name,
+            i.version,
+            ResolvedHostedDescription(
+              ref.description as HostedDescription,
+              sha256: i.archiveSha256,
+            ),
+          ),
+        )
+        .toList();
   }
 
   /// Parses [description] into its server and package name components, then
@@ -678,23 +686,102 @@
     }
     final versions = await _scheduler.schedule(_RefAndCache(id.toRef(), cache));
     final url = _listVersionsUrl(id.toRef());
-    return versions![id]?.pubspec ??
+    return versions.firstWhereOrNull((i) => i.version == id.version)?.pubspec ??
         (throw PackageNotFoundException('Could not find package $id at $url'));
   }
 
-  /// Downloads the package identified by [id] to the system cache.
+  /// Downloads the package identified by [id] to the system cache if needed.
+  ///
+  /// Validates that the content hash of [id] corresponds to what is already in
+  /// cache, if not the file is redownloaded.
+  ///
+  /// If [allowOutdatedHashChecks] is `true` we use a cached version listing
+  /// response if present instead of probing the server. Not probing allows for
+  /// `pub get` with a filled cache to be a fast case that doesn't require any
+  /// new version-listings.
   @override
-  Future<Package> downloadToSystemCache(PackageId id, SystemCache cache) async {
-    if (!isInSystemCache(id, cache)) {
-      if (cache.isOffline) {
-        throw StateError('Cannot download packages when offline.');
-      }
-      var packageDir = getDirectoryInCache(id, cache);
-      ensureDir(p.dirname(packageDir));
-      await _download(id, packageDir, cache);
-    }
+  Future<PackageId> downloadToSystemCache(
+      PackageId id, SystemCache cache) async {
+    final packageDir = getDirectoryInCache(id, cache);
 
-    return Package.load(id.name, getDirectoryInCache(id, cache), cache.sources);
+    // Use the content-hash from the version-info to compare with what we
+    // already downloaded.
+    //
+    // The content-hash from [id] will be compared with that when the lockfile
+    // is written.
+    //
+    // We allow the version-listing to be a few days outdated in order for `pub
+    // get` with an existing working resolution and everything in cache to be
+    // fast.
+    final versionInfo = await _versionInfo(
+      id.toRef(),
+      id.version,
+      cache,
+      maxAge: Duration(days: 3),
+    );
+
+    final expectedContentHash = versionInfo?.archiveSha256 ??
+        // Handling of legacy server - we use the hash from the id (typically
+        // from the lockfile) to compare to the existing download.
+        (id.description as ResolvedHostedDescription).sha256;
+    Uint8List? contentHash;
+    if (!fileExists(hashPath(id, cache))) {
+      if (dirExists(packageDir) && !cache.isOffline) {
+        log.fine(
+            'Cache entry for ${id.name}-${id.version} has no content-hash - redownloading.');
+        deleteEntry(packageDir);
+      }
+    } else if (expectedContentHash == null) {
+      // Can happen with a legacy server combined with a legacy lock file.
+      log.fine(
+          'Content-hash of ${id.name}-${id.version} not known from resolution.');
+    } else {
+      final hashFromCache = sha256FromCache(id, cache);
+      if (!fixedTimeBytesEquals(hashFromCache, expectedContentHash)) {
+        log.warning(
+            'Cached version of ${id.name}-${id.version} has wrong hash - redownloading.');
+        if (cache.isOffline) {
+          fail('Cannot redownload while offline. Try again without --offline.');
+        }
+        deleteEntry(packageDir);
+      } else {
+        contentHash = hashFromCache;
+      }
+    }
+    if (dirExists(packageDir)) {
+      contentHash ??= sha256FromCache(id, cache);
+    } else {
+      if (cache.isOffline) {
+        fail(
+            'Missing package ${id.name}-${id.version}. Try again without --offline.');
+      }
+      contentHash = await _download(id, packageDir, cache);
+    }
+    return PackageId(
+      id.name,
+      id.version,
+      (id.description as ResolvedHostedDescription).withSha256(contentHash),
+    );
+  }
+
+  /// Determines if the package identified by [id] is already downloaded to the
+  /// system cache and has the expected content-hash.
+  @override
+  bool isInSystemCache(PackageId id, SystemCache cache) {
+    if ((id.description as ResolvedHostedDescription).sha256 != null) {
+      try {
+        final cachedSha256 = readTextFile(hashPath(id, cache));
+        if (!const ListEquality().equals(hexDecode(cachedSha256),
+            (id.description as ResolvedHostedDescription).sha256)) {
+          return false;
+        }
+      } on io.IOException {
+        // Most likely the hash file was not written, because we had a legacy
+        // entry.
+        return false;
+      }
+    }
+    return dirExists(getDirectoryInCache(id, cache));
   }
 
   /// The system cache directory for the hosted source contains subdirectories
@@ -714,6 +801,32 @@
     return p.join(rootDir, dir, '${id.name}-${id.version}');
   }
 
+  /// The system cache directory for the hosted source contains subdirectories
+  /// for each separate repository URL that's used on the system.
+  ///
+  /// Parallel to this there is a `hosted-hashes` directory with a stored hash
+  /// of all downloaded packages.
+  String hashPath(PackageId id, SystemCache cache) {
+    final description = id.description.description;
+    if (description is! HostedDescription) {
+      throw ArgumentError('Wrong source');
+    }
+    final rootDir = cache.rootDir;
+
+    var serverDir = _urlToDirectory(description.url);
+    return p.join(
+        rootDir, 'hosted-hashes', serverDir, '${id.name}-${id.version}.sha256');
+  }
+
+  /// Loads the hash at `hashPath(id)`.
+  Uint8List? sha256FromCache(PackageId id, SystemCache cache) {
+    try {
+      return hexDecode(readTextFile(hashPath(id, cache)));
+    } on io.IOException {
+      return null;
+    }
+  }
+
   /// Re-downloads all packages that have been previously downloaded into the
   /// system cache from any server.
   @override
@@ -766,7 +879,14 @@
       return results
         ..addAll(await Future.wait(
           packages.map((package) async {
-            var id = idFor(package.name, package.version, url: url);
+            var id = PackageId(
+              package.name,
+              package.version,
+              ResolvedHostedDescription(
+                HostedDescription(package.name, url),
+                sha256: null,
+              ),
+            );
             try {
               deleteEntry(package.dir);
               await _download(id, package.dir, cache);
@@ -803,7 +923,7 @@
     return PackageId(
       name,
       version,
-      ResolvedHostedDescription(HostedDescription(name, url)),
+      ResolvedHostedDescription(HostedDescription(name, url), sha256: null),
     );
   }
 
@@ -849,7 +969,9 @@
   /// If there is no archive_url, try to fetch it from
   /// `$server/packages/$package/versions/$version.tar.gz` where server comes
   /// from `id.description`.
-  Future _download(
+  ///
+  /// Returns the content-hash of the downloaded archive.
+  Future<Uint8List> _download(
     PackageId id,
     String destPath,
     SystemCache cache,
@@ -868,9 +990,11 @@
     // query-string as is the case with signed S3 URLs. And we wish to allow for
     // such URLs to be used.
     final versions = await _scheduler.schedule(_RefAndCache(id.toRef(), cache));
-    final versionInfo = versions![id];
+    final versionInfo =
+        versions.firstWhereOrNull((i) => i.version == id.version);
     final packageName = id.name;
     final version = id.version;
+    late Uint8List contentHash;
     if (versionInfo == null) {
       throw PackageNotFoundException(
           'Package $packageName has no version $version');
@@ -878,13 +1002,51 @@
 
     final archiveUrl = versionInfo.archiveUrl;
     log.io('Get package from $archiveUrl.');
-    log.message('Downloading ${log.bold(id.name)} ${id.version}...');
+    log.fine('Downloading ${log.bold(id.name)} ${id.version}...');
 
     // Download and extract the archive to a temp directory.
-    await withTempDir((tempDirForArchive) async {
+    return await withTempDir((tempDirForArchive) async {
       var fileName = '$packageName-$version.tar.gz';
       var archivePath = p.join(tempDirForArchive, fileName);
 
+      Stream<List<int>> validateSha256(
+        Stream<List<int>> stream,
+        Digest? expectedHash,
+      ) async* {
+        final output = _SingleValueSink<Digest>();
+        final input = sha256.startChunkedConversion(output);
+        await for (final v in stream) {
+          input.add(v);
+          yield v;
+        }
+        input.close();
+        final actualHash = output.value;
+        if (expectedHash != null && output.value != expectedHash) {
+          log.fine(
+              'Expected content-hash for ${id.name}-${id.version} $expectedHash actual: ${output.value}.');
+          throw PackageIntegrityException('''
+Downloaded archive for ${id.name}-${id.version} had wrong content-hash.
+
+This indicates a problem on the package repository: `${description.url}`.
+
+See $contentHashesDocumentationUrl.
+''');
+        }
+        final path = hashPath(id, cache);
+        ensureDir(p.dirname(path));
+        writeTextFile(
+          path,
+          hexEncode(actualHash.bytes),
+        );
+        contentHash = Uint8List.fromList(actualHash.bytes);
+      }
+
+      // It is important that we do not compare against id.description.sha256,
+      // as we need to check against the newly fetched version listing to ensure
+      // that content changes result in updated lockfiles, not failure to
+      // download.
+      final expectedSha256 = versionInfo.archiveSha256;
+
       // The client from `withAuthenticatedClient` will retry HTTP requests.
       // This wrapper is one layer up and will retry checksum validation errors.
       await retry(
@@ -893,14 +1055,16 @@
           final request = http.Request('GET', archiveUrl);
           final response = await withAuthenticatedClient(cache,
               Uri.parse(description.url), (client) => client.send(request));
-          final expectedChecksum = _parseCrc32c(response.headers, fileName);
+          final expectedCrc32Checksum =
+              _parseCrc32c(response.headers, fileName);
 
           Stream<List<int>> stream = response.stream;
-          if (expectedChecksum != null) {
-            stream = _validateStream(
-                response.stream, expectedChecksum, id, archiveUrl);
+          if (expectedCrc32Checksum != null) {
+            stream = _validateStreamCrc32Checksum(
+                response.stream, expectedCrc32Checksum, id, archiveUrl);
           }
-
+          stream = validateSha256(
+              stream, (expectedSha256 == null) ? null : Digest(expectedSha256));
           // We download the archive to disk instead of streaming it directly
           // into the tar unpacking. This simplifies stream handling.
           // Package:tar cancels the stream when it reaches end-of-archive, and
@@ -924,6 +1088,7 @@
       var tempDir = cache.createTempDir();
       await extractTarGz(readBinaryFileAsStream(archivePath), tempDir);
 
+      ensureDir(p.dirname(destPath));
       // Now that the get has succeeded, move it to the real location in the
       // cache.
       //
@@ -931,6 +1096,7 @@
       // another pub process has installed the same package version while we
       // downloaded.
       tryRenameDir(tempDir, destPath);
+      return contentHash;
     });
   }
 
@@ -971,12 +1137,12 @@
       if (error.statusCode == 401) {
         hint = '$hostedUrl package repository requested authentication!\n'
             'You can provide credentials using:\n'
-            '    pub token add $hostedUrl';
+            '    dart pub token add $hostedUrl';
       }
       if (error.statusCode == 403) {
         hint = 'Insufficient permissions to the resource at the $hostedUrl '
             'package repository.\nYou can modify credentials using:\n'
-            '    pub token add $hostedUrl';
+            '    dart pub token add $hostedUrl';
         message = 'authorization failed';
       }
 
@@ -1051,7 +1217,22 @@
   @override
   HostedDescription get description => super.description as HostedDescription;
 
-  ResolvedHostedDescription(HostedDescription description) : super(description);
+  /// The content hash of the package archive (the `tar.gz` file) of the
+  /// PackageId described by this.
+  ///
+  /// This can be obtained in several ways:
+  /// * Reported from a server in the archive_sha256 field.
+  ///   (will be null if the server does not report this.)
+  /// * Obtained from a pubspec.lock
+  ///   (will be null for legacy lock-files).
+  /// * Read from the <PUB_CACHE>/hosted-hashes/<server>/<package>-<version>.sha256 file.
+  ///   (will be null if the file doesn't exist for corrupt or legacy caches).
+  final Uint8List? sha256;
+
+  ResolvedHostedDescription(
+    HostedDescription description, {
+    required this.sha256,
+  }) : super(description);
 
   @override
   Object? serializeForLockfile({required String? containingDir}) {
@@ -1061,26 +1242,46 @@
     } on FormatException catch (e) {
       throw ArgumentError.value(url, 'url', 'url must be normalized: $e');
     }
-    return {'name': description.packageName, 'url': url.toString()};
+    final hash = sha256;
+    return {
+      'name': description.packageName,
+      'url': url.toString(),
+      if (hash != null) 'sha256': hexEncode(hash),
+    };
   }
 
   @override
+  // We do not include the sha256 in the hashCode because of the equality
+  // semantics.
   int get hashCode => description.hashCode;
 
   @override
   bool operator ==(Object other) {
     return other is ResolvedHostedDescription &&
-        other.description == description;
+        other.description == description &&
+        // A [sha256] of `null` means that we don't know the hash yet.
+        // Therefore we have to assume it is equal to any known value.
+        (sha256 == null ||
+            other.sha256 == null ||
+            fixedTimeBytesEquals(sha256, other.sha256));
   }
+
+  ResolvedHostedDescription withSha256(Uint8List? newSha256) =>
+      ResolvedHostedDescription(description, sha256: newSha256);
 }
 
 /// Information about a package version retrieved from /api/packages/$package<
 class _VersionInfo {
   final Pubspec pubspec;
   final Uri archiveUrl;
+  final Version version;
+
+  /// The sha256 digest of the archive according to the package-repository.
+  final Uint8List? archiveSha256;
   final PackageStatus status;
 
-  _VersionInfo(this.pubspec, this.archiveUrl, this.status);
+  _VersionInfo(this.version, this.pubspec, this.archiveUrl, this.status,
+      this.archiveSha256);
 }
 
 /// Given a URL, returns a "normalized" string to be used as a directory name
@@ -1152,6 +1353,20 @@
   bool operator ==(Object other) => other is _RefAndCache && other.ref == ref;
 }
 
+/// A sink that can only have `add` called once, and that can retrieve the
+/// value.
+class _SingleValueSink<T> implements Sink<T> {
+  late final T value;
+
+  @override
+  void add(T data) {
+    value = data;
+  }
+
+  @override
+  void close() {}
+}
+
 @visibleForTesting
 const checksumHeaderName = 'x-goog-hash';
 
@@ -1163,7 +1378,7 @@
 /// the one present in the checksum response header.
 ///
 /// Throws [PackageIntegrityException] if there is a checksum mismatch.
-Stream<List<int>> _validateStream(Stream<List<int>> stream,
+Stream<List<int>> _validateStreamCrc32Checksum(Stream<List<int>> stream,
     int expectedChecksum, PackageId id, Uri archiveUrl) async* {
   final crc32c = Crc32c();
 
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart
index ebbaa97..11a10a1 100644
--- a/lib/src/system_cache.dart
+++ b/lib/src/system_cache.dart
@@ -186,7 +186,12 @@
     var versions = await ref.source.doGetVersions(ref, maxAge, this);
 
     versions = (await Future.wait(versions.map((id) async {
-      final packageStatus = await ref.source.status(id, this, maxAge: maxAge);
+      final packageStatus = await ref.source.status(
+        id.toRef(),
+        id.version,
+        this,
+        maxAge: maxAge,
+      );
       if (!packageStatus.isRetracted || id.version == allowedRetractedVersion) {
         return id;
       }
@@ -208,10 +213,24 @@
     return id.source.doGetDirectory(id, this, relativeFrom: relativeFrom);
   }
 
-  Future<void> downloadPackage(PackageId id) async {
+  /// Downloads a cached package identified by [id] to the cache.
+  ///
+  /// [id] must refer to a cached package.
+  ///
+  /// If [allowOutdatedHashChecks] is `true` we use a cached version listing
+  /// response if present instead of probing the server. Not probing allows for
+  /// `pub get` with a filled cache to be a fast case that doesn't require any
+  /// new version-listings.
+  ///
+  /// Returns [id] with an updated [ResolvedDescription], this can be different
+  /// if the content-hash changed while downloading.
+  Future<PackageId> downloadPackage(PackageId id) async {
     final source = id.source;
     assert(source is CachedSource);
-    await (source as CachedSource).downloadToSystemCache(id, this);
+    return await (source as CachedSource).downloadToSystemCache(
+      id,
+      this,
+    );
   }
 
   /// Get the latest version of [package].
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index dbc7622..0f1bf51 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -7,7 +7,9 @@
 import 'dart:convert';
 import 'dart:io';
 import 'dart:math' as math;
+import 'dart:typed_data';
 
+import 'package:convert/convert.dart';
 import 'package:crypto/crypto.dart' as crypto;
 import 'package:pub_semver/pub_semver.dart';
 import 'package:stack_trace/stack_trace.dart';
@@ -328,6 +330,10 @@
 String sha1(String source) =>
     crypto.sha1.convert(utf8.encode(source)).toString();
 
+String hexEncode(List<int> bytes) => hex.encode(bytes);
+
+Uint8List hexDecode(String string) => hex.decode(string) as Uint8List;
+
 /// A regular expression matching a trailing CR character.
 final _trailingCR = RegExp(r'\r$');
 
@@ -639,6 +645,19 @@
   };
 }
 
+/// Compares two lists. If the lists have equal length this comparison will
+/// iterate all elements, thus taking a fixed amount of time making timing
+/// attacks harder.
+bool fixedTimeBytesEquals(List<int>? a, List<int>? b) {
+  if (a == null || b == null) return a == b;
+  if (a.length != b.length) return false;
+  var e = 0;
+  for (var i = 0; i < a.length; i++) {
+    e |= a[i] ^ b[i];
+  }
+  return e == 0;
+}
+
 /// Call [fn] retrying so long as [retryIf] return `true` for the exception
 /// thrown, up-to [maxAttempts] times.
 ///
diff --git a/pubspec.yaml b/pubspec.yaml
index eb2983f..f7c1077 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -11,6 +11,7 @@
   async: ^2.6.1
   cli_util: ^0.3.5
   collection: ^1.15.0
+  convert: ^3.0.2
   crypto: ^3.0.1
   frontend_server_client: ^3.0.0
   http: ^0.13.3
diff --git a/test/cache/add/adds_latest_matching_version_test.dart b/test/cache/add/adds_latest_matching_version_test.dart
index 7bf9625..9e06bf7 100644
--- a/test/cache/add/adds_latest_matching_version_test.dart
+++ b/test/cache/add/adds_latest_matching_version_test.dart
@@ -21,8 +21,8 @@
 
     await runPub(
         args: ['cache', 'add', 'foo', '-v', '>=1.0.0 <2.0.0'],
-        output: 'Downloading foo 1.2.3...',
         silent: allOf([
+          contains('Downloading foo 1.2.3...'),
           contains('X-Pub-OS: ${Platform.operatingSystem}'),
           contains('X-Pub-Command: cache add'),
           contains('X-Pub-Session-ID:'),
diff --git a/test/cache/add/adds_latest_version_test.dart b/test/cache/add/adds_latest_version_test.dart
index cf34857..d302039 100644
--- a/test/cache/add/adds_latest_version_test.dart
+++ b/test/cache/add/adds_latest_version_test.dart
@@ -15,7 +15,9 @@
       ..serve('foo', '1.2.4-dev');
 
     await runPub(
-        args: ['cache', 'add', 'foo'], output: 'Downloading foo 1.2.3...');
+      args: ['cache', 'add', 'foo'],
+      silent: contains('Downloading foo 1.2.3...'),
+    );
 
     await d.cacheDir({'foo': '1.2.3'}).validate();
   });
diff --git a/test/cache/add/all_adds_all_matching_versions_test.dart b/test/cache/add/all_adds_all_matching_versions_test.dart
index a05e8dc..b7c884f 100644
--- a/test/cache/add/all_adds_all_matching_versions_test.dart
+++ b/test/cache/add/all_adds_all_matching_versions_test.dart
@@ -16,11 +16,13 @@
       ..serve('foo', '2.0.0');
 
     await runPub(
-        args: ['cache', 'add', 'foo', '-v', '>=1.0.0 <2.0.0', '--all'],
-        output: '''
-          Downloading foo 1.2.2...
-          Downloading foo 1.2.3-dev...
-          Downloading foo 1.2.3...''');
+      args: ['cache', 'add', 'foo', '-v', '>=1.0.0 <2.0.0', '--all'],
+      silent: allOf([
+        contains('Downloading foo 1.2.2...'),
+        contains('Downloading foo 1.2.3-dev...'),
+        contains('Downloading foo 1.2.3...'),
+      ]),
+    );
 
     await d.cacheDir({'foo': '1.2.2'}).validate();
     await d.cacheDir({'foo': '1.2.3-dev'}).validate();
diff --git a/test/cache/add/all_with_some_versions_present_test.dart b/test/cache/add/all_with_some_versions_present_test.dart
index 7b5b36d..82d9fd5 100644
--- a/test/cache/add/all_with_some_versions_present_test.dart
+++ b/test/cache/add/all_with_some_versions_present_test.dart
@@ -18,18 +18,22 @@
     // Install a couple of versions first.
     await runPub(
         args: ['cache', 'add', 'foo', '-v', '1.2.1'],
-        output: 'Downloading foo 1.2.1...');
+        silent: contains('Downloading foo 1.2.1...'));
 
     await runPub(
         args: ['cache', 'add', 'foo', '-v', '1.2.3'],
-        output: 'Downloading foo 1.2.3...');
+        silent: contains('Downloading foo 1.2.3...'));
 
     // They should show up as already installed now.
-    await runPub(args: ['cache', 'add', 'foo', '--all'], output: '''
-          Already cached foo 1.2.1.
-          Downloading foo 1.2.2...
-          Already cached foo 1.2.3.
-          Downloading foo 2.0.0...''');
+    await runPub(
+        args: ['cache', 'add', 'foo', '--all'],
+        silent: allOf([
+          contains('Downloading foo 1.2.2...'),
+          contains('Downloading foo 2.0.0...')
+        ]),
+        output: '''
+Already cached foo 1.2.1.
+Already cached foo 1.2.3.''');
 
     await d.cacheDir({'foo': '1.2.1'}).validate();
     await d.cacheDir({'foo': '1.2.2'}).validate();
diff --git a/test/cache/add/already_cached_test.dart b/test/cache/add/already_cached_test.dart
index 8c74da9..ef9452e 100644
--- a/test/cache/add/already_cached_test.dart
+++ b/test/cache/add/already_cached_test.dart
@@ -14,7 +14,8 @@
 
     // Run once to put it in the cache.
     await runPub(
-        args: ['cache', 'add', 'foo'], output: 'Downloading foo 1.2.3...');
+        args: ['cache', 'add', 'foo'],
+        silent: contains('Downloading foo 1.2.3...'));
 
     // Should be in the cache now.
     await runPub(
diff --git a/test/cache/list_test.dart b/test/cache/list_test.dart
index a2c26a4..c554c22 100644
--- a/test/cache/list_test.dart
+++ b/test/cache/list_test.dart
@@ -10,8 +10,7 @@
 
 void main() {
   String hostedDir(package) {
-    return path.join(
-        d.sandbox, cachePath, 'hosted', 'pub.dartlang.org', package);
+    return path.join(d.sandbox, cachePath, 'hosted', 'pub.dev', package);
   }
 
   test('running pub cache list when there is no cache', () async {
@@ -21,7 +20,7 @@
   test('running pub cache list on empty cache', () async {
     // Set up a cache.
     await d.dir(cachePath, [
-      d.dir('hosted', [d.dir('pub.dartlang.org', [])])
+      d.dir('hosted', [d.dir('pub.dev', [])])
     ]).create();
 
     await runPub(args: ['cache', 'list'], outputJson: {'packages': {}});
@@ -31,7 +30,7 @@
     // Set up a cache.
     await d.dir(cachePath, [
       d.dir('hosted', [
-        d.dir('pub.dartlang.org', [
+        d.dir('pub.dev', [
           d.dir('foo-1.2.3', [d.libPubspec('foo', '1.2.3'), d.libDir('foo')]),
           d.dir('bar-2.0.0', [d.libPubspec('bar', '2.0.0'), d.libDir('bar')])
         ])
@@ -57,7 +56,7 @@
     // Set up a cache.
     await d.dir(cachePath, [
       d.dir('hosted', [
-        d.dir('pub.dartlang.org', [
+        d.dir('pub.dev', [
           d.dir('foo-1.2.3', [
             d.libPubspec('foo', '1.2.3', deps: {
               'bar': {'bad': 'bar'}
diff --git a/test/cache/repair/handles_failure_test.dart b/test/cache/repair/handles_failure_test.dart
index d5637ca..f44a42d 100644
--- a/test/cache/repair/handles_failure_test.dart
+++ b/test/cache/repair/handles_failure_test.dart
@@ -32,9 +32,6 @@
     // Repair them.
     var pub = await startPub(args: ['cache', 'repair']);
 
-    expect(pub.stdout, emits('Downloading foo 1.2.3...'));
-    expect(pub.stdout, emits('Downloading foo 1.2.5...'));
-
     expect(pub.stderr, emits(startsWith('Failed to repair foo 1.2.4. Error:')));
     expect(
         pub.stderr,
diff --git a/test/cache/repair/hosted.dart b/test/cache/repair/hosted.dart
index ec3786b..a826fad 100644
--- a/test/cache/repair/hosted.dart
+++ b/test/cache/repair/hosted.dart
@@ -39,11 +39,12 @@
     await runPub(
         args: ['cache', 'repair'],
         output: '''
-          Downloading bar 1.2.4...
-          Downloading foo 1.2.3...
-          Downloading foo 1.2.5...
+
           Reinstalled 3 packages.''',
         silent: allOf([
+          contains('Downloading bar 1.2.4...'),
+          contains('Downloading foo 1.2.3...'),
+          contains('Downloading foo 1.2.5...'),
           contains('X-Pub-OS: ${Platform.operatingSystem}'),
           contains('X-Pub-Command: cache repair'),
           contains('X-Pub-Session-ID:'),
diff --git a/test/cache/repair/recompiles_snapshots_test.dart b/test/cache/repair/recompiles_snapshots_test.dart
index f3c3d6c..6fa4ca2 100644
--- a/test/cache/repair/recompiles_snapshots_test.dart
+++ b/test/cache/repair/recompiles_snapshots_test.dart
@@ -21,7 +21,6 @@
     ]).create();
 
     await runPub(args: ['cache', 'repair'], output: '''
-          Downloading foo 1.0.0...
           Reinstalled 1 package.
           Reactivating foo 1.0.0...
           Building package executables...
diff --git a/test/cache/repair/updates_binstubs_test.dart b/test/cache/repair/updates_binstubs_test.dart
index e8cbfb4..65a574c 100644
--- a/test/cache/repair/updates_binstubs_test.dart
+++ b/test/cache/repair/updates_binstubs_test.dart
@@ -34,7 +34,6 @@
 
     // Repair them.
     await runPub(args: ['cache', 'repair'], output: '''
-          Downloading foo 1.0.0...
           Reinstalled 1 package.
           Reactivating foo 1.0.0...
           Building package executables...
diff --git a/test/content_hash_test.dart b/test/content_hash_test.dart
new file mode 100644
index 0000000..6a0725b
--- /dev/null
+++ b/test/content_hash_test.dart
@@ -0,0 +1,221 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'descriptor.dart';
+import 'test_pub.dart';
+
+Future<void> main() async {
+  test('archive_sha256 is stored in lockfile and cache upon download',
+      () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    server.serveContentHashes = true;
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    final lockfile = loadYaml(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final sha256 = lockfile['packages']['foo']['description']['sha256'];
+    expect(sha256, hasLength(64));
+    await hostedHashesCache([
+      file('foo-1.0.0.sha256', sha256),
+    ]).validate();
+  });
+
+  test(
+      'archive_sha256 is stored in lockfile upon download on legacy server without content hashes',
+      () async {
+    final server = await servePackages();
+    server.serveContentHashes = false;
+    server.serve('foo', '1.0.0');
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    final lockfile = loadYaml(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final sha256 = lockfile['packages']['foo']['description']['sha256'];
+    expect(sha256, hasLength(64));
+    await hostedHashesCache([
+      file('foo-1.0.0.sha256', sha256),
+    ]).validate();
+  });
+
+  test('archive_sha256 is checked on download', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    server.overrideArchiveSha256('foo', '1.0.0',
+        'e7a7a0f6d9873e4c40cf68cc3cc9ca5b6c8cef6a2220241bdada4b9cb0083279');
+    await appDir({'foo': 'any'}).create();
+    await pubGet(
+      silent: contains('Retry #2'),
+      error:
+          contains('Downloaded archive for foo-1.0.0 had wrong content-hash.'),
+      environment: {
+        'PUB_MAX_HTTP_RETRIES': '2',
+      },
+    );
+  });
+
+  test('If content is updated on server we warn and update the lockfile',
+      () async {
+    final server = await servePackages();
+    server.serveContentHashes = true;
+    server.serve('foo', '1.0.0');
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    server.serve('foo', '1.0.0',
+        contents: [file('new_file.txt', 'This file could be malicious.')]);
+    // Pub get will not revisit the file-listing if everything resolves, and only compare with a cached value.
+    await pubGet();
+    // Deleting the version-listing cache will cause it to be refetched, and the
+    // warning will happen.
+    File(p.join(globalServer.cachingPath, '.cache', 'foo-versions.json'))
+        .deleteSync();
+    await pubGet(
+      warning: allOf(
+        contains('Cached version of foo-1.0.0 has wrong hash - redownloading.'),
+        contains(
+            'The existing content-hash from pubspec.lock doesn\'t match contents for:'),
+        contains('* foo-1.0.0 from "${server.url}"\n'),
+      ),
+      exitCode: exit_codes.SUCCESS,
+    );
+    final lockfile = loadYaml(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final newHash = lockfile['packages']['foo']['description']['sha256'];
+    expect(newHash, await server.peekArchiveSha256('foo', '1.0.0'));
+  });
+
+  test(
+      'If content is updated on legacy server, and the download needs refreshing we warn and update the lockfile',
+      () async {
+    final server = await servePackages();
+    server.serveContentHashes = false;
+    server.serve('foo', '1.0.0');
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    server.serve('foo', '1.0.0',
+        contents: [file('new_file.txt', 'This file could be malicious.')]);
+    // Deleting the hash-file cache will cause it to be refetched, and the
+    // warning will happen.
+    File(p.join(globalServer.hashesCachingPath, 'foo-1.0.0.sha256'))
+        .deleteSync();
+
+    await pubGet(
+      warning: allOf([
+        contains(
+          'The existing content-hash from pubspec.lock doesn\'t match contents for:',
+        ),
+        contains('* foo-1.0.0 from "${globalServer.url}"'),
+      ]),
+      exitCode: exit_codes.SUCCESS,
+    );
+    final lockfile = loadYaml(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final newHash = lockfile['packages']['foo']['description']['sha256'];
+    expect(newHash, await server.peekArchiveSha256('foo', '1.0.0'));
+  });
+
+  test(
+      'sha256 in cache is checked on pub get - warning and redownload on legacy server without content-hashes',
+      () async {
+    final server = await servePackages();
+    server.serveContentHashes = false;
+    server.serve('foo', '1.0.0');
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    final lockfile = loadYaml(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final originalHash = lockfile['packages']['foo']['description']['sha256'];
+    // Create wrong hash on disk.
+    await hostedHashesCache([
+      file('foo-1.0.0.sha256',
+          'e7a7a0f6d9873e4c40cf68cc3cc9ca5b6c8cef6a2220241bdada4b9cb0083279'),
+    ]).create();
+
+    await pubGet(
+        warning: 'Cached version of foo-1.0.0 has wrong hash - redownloading.');
+    await hostedHashesCache([
+      file('foo-1.0.0.sha256', originalHash),
+    ]).validate();
+  });
+
+  test('sha256 in cache is checked on pub get - warning and redownload',
+      () async {
+    final server = await servePackages();
+    server.serveContentHashes = true;
+    server.serve('foo', '1.0.0');
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    final lockfile = loadYaml(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final originalHash = lockfile['packages']['foo']['description']['sha256'];
+    await hostedHashesCache([
+      file('foo-1.0.0.sha256',
+          'e7a7a0f6d9873e4c40cf68cc3cc9ca5b6c8cef6a2220241bdada4b9cb0083279'),
+    ]).create();
+
+    await pubGet(
+        warning: 'Cached version of foo-1.0.0 has wrong hash - redownloading.');
+    await hostedHashesCache([
+      file('foo-1.0.0.sha256', originalHash),
+    ]).validate();
+  });
+
+  test(
+      'Legacy lockfile without content-hashes is updated with the hash on pub get on legacy server without content-hashes',
+      () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    server.serveContentHashes = false;
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    // Pretend we had no hash in the lockfile.
+    final lockfile = YamlEditor(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final originalContentHash = lockfile
+        .remove(['packages', 'foo', 'description', 'sha256']).value as String;
+    File(p.join(sandbox, appPath, 'pubspec.lock')).writeAsStringSync(
+      lockfile.toString(),
+    );
+    await pubGet();
+    final lockfile2 = YamlEditor(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    expect(
+      lockfile2.parseAt(['packages', 'foo', 'description', 'sha256']).value,
+      originalContentHash,
+    );
+  });
+
+  test(
+      'Legacy lockfile without content-hashes is updated with the hash on pub get',
+      () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    server.serveContentHashes = true;
+    await appDir({'foo': 'any'}).create();
+    await pubGet();
+    // Pretend we had no hash in the lockfile.
+    final lockfile = YamlEditor(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    final originalContentHash = lockfile
+        .remove(['packages', 'foo', 'description', 'sha256']).value as String;
+    File(p.join(sandbox, appPath, 'pubspec.lock')).writeAsStringSync(
+      lockfile.toString(),
+    );
+    await pubGet();
+    final lockfile2 = YamlEditor(
+        File(p.join(sandbox, appPath, 'pubspec.lock')).readAsStringSync());
+    expect(
+      lockfile2.parseAt(['packages', 'foo', 'description', 'sha256']).value,
+      originalContentHash,
+    );
+  });
+}
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart
index 7228b36..02a32a0 100644
--- a/test/dependency_services/dependency_services_test.dart
+++ b/test/dependency_services/dependency_services_test.dart
@@ -10,8 +10,10 @@
 import 'package:pub_semver/pub_semver.dart';
 import 'package:shelf/shelf.dart' as shelf;
 import 'package:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
 
 import '../descriptor.dart' as d;
+import '../descriptor.dart';
 import '../golden_file.dart';
 import '../test_pub.dart';
 
@@ -49,6 +51,7 @@
       Platform.resolvedExecutable,
       [
         snapshot,
+        '--verbose',
         ...args,
       ],
       environment: getPubTestEnvironment(),
@@ -120,7 +123,8 @@
     final server = (await servePackages())
       ..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
       ..serve('foo', '2.2.3')
-      ..serve('transitive', '1.0.0');
+      ..serve('transitive', '1.0.0')
+      ..serveContentHashes = true;
 
     await d.dir(appPath, [
       d.pubspec({
@@ -151,7 +155,8 @@
     final server = (await servePackages())
       ..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
       ..serve('foo', '2.2.3')
-      ..serve('transitive', '1.0.0');
+      ..serve('transitive', '1.0.0')
+      ..serveContentHashes = true;
 
     await d.git('bar.git', [d.libPubspec('bar', '1.0.0')]).create();
 
@@ -183,7 +188,8 @@
       ..serve('foo', '2.2.3')
       ..serve('bar', '1.2.3')
       ..serve('bar', '2.2.3')
-      ..serve('boo', '1.2.3');
+      ..serve('boo', '1.2.3')
+      ..serveContentHashes = true;
 
     await d.dir(appPath, [
       d.pubspec({
@@ -211,11 +217,51 @@
     });
   });
 
+  testWithGolden('Preserves no content-hashes', (context) async {
+    final server = (await servePackages())
+      ..serve('foo', '1.2.3')
+      ..serve('foo', '2.2.3')
+      ..serve('bar', '1.2.3')
+      ..serve('bar', '2.2.3')
+      ..serve('boo', '1.2.3')
+      ..serveContentHashes = true;
+
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'app',
+        'dependencies': {
+          'foo': '^1.0.0',
+          'bar': '^1.0.0',
+          'boo': '^1.0.0',
+        },
+      })
+    ]).create();
+    await pubGet();
+    final lockFile = File(path(p.join(appPath, 'pubspec.lock')));
+    final lockFileYaml = YamlEditor(
+      lockFile.readAsStringSync(),
+    );
+    for (final p in lockFileYaml.parseAt(['packages']).value.entries) {
+      lockFileYaml.remove(['packages', p.key, 'description', 'sha256']);
+    }
+    lockFile.writeAsStringSync(lockFileYaml.toString());
+
+    server.serve('foo', '1.2.4');
+    server.serve('boo', '1.2.4');
+
+    server.dontAllowDownloads();
+
+    await _listReportApply(context, [
+      _PackageVersion('foo', '1.2.4'),
+    ]);
+  });
+
   testWithGolden('Adding transitive', (context) async {
     final server = (await servePackages())
       ..serve('foo', '1.2.3')
       ..serve('foo', '2.2.3', deps: {'transitive': '^1.0.0'})
-      ..serve('transitive', '1.0.0');
+      ..serve('transitive', '1.0.0')
+      ..serveContentHashes = true;
 
     await d.dir(appPath, [
       d.pubspec({
@@ -247,7 +293,8 @@
     final server = (await servePackages())
       ..serve('foo', '1.0.0')
       ..serve('bar', '1.0.0')
-      ..serve('baz', '1.0.0');
+      ..serve('baz', '1.0.0')
+      ..serveContentHashes = true;
 
     await d.dir(appPath, [
       d.pubspec({
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 669d8ef..4ced393 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -191,6 +191,20 @@
   return dir(hostedCachePath(port: port), contents);
 }
 
+/// Describes the hosted-hashes cache directory containing hashes of the hosted
+/// packages downloaded from the mock package server.
+///
+/// If [port] is passed, it's used as the port number of the local hosted server
+/// that this cache represents. It defaults to [globalServer.port].
+Descriptor hostedHashesCache(Iterable<Descriptor> contents, {int? port}) {
+  return dir(cachePath, [
+    dir(
+      'hosted-hashes',
+      [dir('localhost%58${port ?? globalServer.port}', contents)],
+    )
+  ]);
+}
+
 String hostedCachePath({int? port}) =>
     p.join(cachePath, 'hosted', 'localhost%58${port ?? globalServer.port}');
 
diff --git a/test/get/preserve_lock_file_line_endings_test.dart b/test/get/preserve_lock_file_line_endings_test.dart
index bfd015a..4484fc5 100644
--- a/test/get/preserve_lock_file_line_endings_test.dart
+++ b/test/get/preserve_lock_file_line_endings_test.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:path/path.dart' as path;
-import 'package:pub/src/entrypoint.dart';
+import 'package:pub/src/lock_file.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
diff --git a/test/global/activate/activate_git_after_hosted_test.dart b/test/global/activate/activate_git_after_hosted_test.dart
index 390aa87..99f2fcb 100644
--- a/test/global/activate/activate_git_after_hosted_test.dart
+++ b/test/global/activate/activate_git_after_hosted_test.dart
@@ -29,7 +29,7 @@
         output: allOf(
             startsWith('Package foo is currently active at version 1.0.0.\n'
                 'Resolving dependencies...\n'
-                '+ foo 1.0.0 from git ..${separator}foo.git at '),
+                '* foo 1.0.0 from git ..${separator}foo.git at '),
             // Specific revision number goes here.
             endsWith('Building package executables...\n'
                 'Built foo:foo.\n'
diff --git a/test/global/activate/activate_hosted_after_git_test.dart b/test/global/activate/activate_hosted_after_git_test.dart
index 7ac174a..15819ca 100644
--- a/test/global/activate/activate_hosted_after_git_test.dart
+++ b/test/global/activate/activate_hosted_after_git_test.dart
@@ -22,14 +22,14 @@
 
     await runPub(args: ['global', 'activate', '-sgit', '../foo.git']);
 
-    await runPub(args: ['global', 'activate', 'foo'], output: '''
-        Package foo is currently active from Git repository "..${separator}foo.git".
-        Resolving dependencies...
-        + foo 2.0.0
-        Downloading foo 2.0.0...
-        Building package executables...
-        Built foo:foo.
-        Activated foo 2.0.0.''');
+    await runPub(
+        args: ['global', 'activate', 'foo'],
+        output: allOf([
+          contains(
+              'Package foo is currently active from Git repository "..${separator}foo.git".'),
+          contains('* foo 2.0.0 (was 1.0.0 from git ..${separator}foo.git at'),
+          contains('Activated foo 2.0.0.')
+        ]));
 
     // Should now run the hosted one.
     var pub = await pubRun(global: true, args: ['foo']);
diff --git a/test/global/activate/activate_hosted_after_path_test.dart b/test/global/activate/activate_hosted_after_path_test.dart
index 4cd753f..8f91a1f 100644
--- a/test/global/activate/activate_hosted_after_path_test.dart
+++ b/test/global/activate/activate_hosted_after_path_test.dart
@@ -27,8 +27,7 @@
     await runPub(args: ['global', 'activate', 'foo'], output: '''
         Package foo is currently active at path "$path".
         Resolving dependencies...
-        + foo 2.0.0
-        Downloading foo 2.0.0...
+        * foo 2.0.0 (was 1.0.0 from path $path)
         Building package executables...
         Built foo:foo.
         Activated foo 2.0.0.''');
diff --git a/test/global/activate/activate_hosted_twice_test.dart b/test/global/activate/activate_hosted_twice_test.dart
index a4d1337..fb4ce92 100644
--- a/test/global/activate/activate_hosted_twice_test.dart
+++ b/test/global/activate/activate_hosted_twice_test.dart
@@ -46,9 +46,7 @@
     await runPub(args: ['global', 'activate', 'foo'], output: '''
 Package foo is currently active at version 1.0.0.
 Resolving dependencies...
-+ bar 2.0.0
-+ foo 1.0.0
-Downloading bar 2.0.0...
+> bar 2.0.0 (was 1.0.0)
 Building package executables...
 Built foo:foo.
 Activated foo 1.0.0.''');
diff --git a/test/global/activate/custom_hosted_url_test.dart b/test/global/activate/custom_hosted_url_test.dart
index 1c0bc04..cd5c2dc 100644
--- a/test/global/activate/custom_hosted_url_test.dart
+++ b/test/global/activate/custom_hosted_url_test.dart
@@ -26,12 +26,13 @@
     customServer.serve('bar', '1.0.0', deps: {'baz': 'any'});
 
     await runPub(
-        args: ['global', 'activate', 'foo', '-u', customServer.url],
-        output: allOf([
-          contains('Downloading bar 1.0.0...'),
-          contains('Downloading baz 1.0.0...'),
-          contains('Downloading foo 1.0.0...'),
-          contains('Activated foo 1.0.0')
-        ]));
+      args: ['global', 'activate', 'foo', '-u', customServer.url],
+      silent: allOf([
+        contains('Downloading bar 1.0.0...'),
+        contains('Downloading baz 1.0.0...'),
+        contains('Downloading foo 1.0.0...'),
+      ]),
+      output: contains('Activated foo 1.0.0'),
+    );
   });
 }
diff --git a/test/global/activate/different_version_test.dart b/test/global/activate/different_version_test.dart
index 8406b92..d84ed8c 100644
--- a/test/global/activate/different_version_test.dart
+++ b/test/global/activate/different_version_test.dart
@@ -26,8 +26,7 @@
     await runPub(args: ['global', 'activate', 'foo', '>1.0.0'], output: '''
         Package foo is currently active at version 1.0.0.
         Resolving dependencies...
-        + foo 2.0.0
-        Downloading foo 2.0.0...
+        > foo 2.0.0 (was 1.0.0)
         Building package executables...
         Built foo:foo.
         Activated foo 2.0.0.''');
diff --git a/test/global/activate/ignores_active_version_test.dart b/test/global/activate/ignores_active_version_test.dart
index 777a631..d3e6094 100644
--- a/test/global/activate/ignores_active_version_test.dart
+++ b/test/global/activate/ignores_active_version_test.dart
@@ -25,8 +25,7 @@
     await runPub(args: ['global', 'activate', 'foo', '>1.0.0'], output: '''
         Package foo is currently active at version 1.2.3.
         Resolving dependencies...
-        + foo 1.3.0
-        Downloading foo 1.3.0...
+        > foo 1.3.0 (was 1.2.3)
         Building package executables...
         Built foo:foo.
         Activated foo 1.3.0.''');
diff --git a/test/global/activate/installs_dependencies_for_git_test.dart b/test/global/activate/installs_dependencies_for_git_test.dart
index 6018f50..04bdc53 100644
--- a/test/global/activate/installs_dependencies_for_git_test.dart
+++ b/test/global/activate/installs_dependencies_for_git_test.dart
@@ -20,7 +20,7 @@
 
     await runPub(
         args: ['global', 'activate', '-sgit', '../foo.git'],
-        output: allOf([
+        silent: allOf([
           contains('Downloading bar 1.0.0...'),
           contains('Downloading baz 1.0.0...')
         ]));
diff --git a/test/global/activate/installs_dependencies_for_path_test.dart b/test/global/activate/installs_dependencies_for_path_test.dart
index 4688f07..ae90242 100644
--- a/test/global/activate/installs_dependencies_for_path_test.dart
+++ b/test/global/activate/installs_dependencies_for_path_test.dart
@@ -20,8 +20,6 @@
 
     var pub = await startPub(args: ['global', 'activate', '-spath', '../foo']);
     expect(pub.stdout, emitsThrough('Resolving dependencies in ../foo...'));
-    expect(pub.stdout, emitsThrough('Downloading bar 1.0.0...'));
-    expect(pub.stdout, emitsThrough('Downloading baz 2.0.0...'));
     expect(pub.stdout, emitsThrough(startsWith('Activated foo 0.0.0 at path')));
     await pub.shouldExit();
 
diff --git a/test/global/activate/installs_dependencies_test.dart b/test/global/activate/installs_dependencies_test.dart
index 770ebcf..9cb2354 100644
--- a/test/global/activate/installs_dependencies_test.dart
+++ b/test/global/activate/installs_dependencies_test.dart
@@ -15,7 +15,7 @@
 
     await runPub(
         args: ['global', 'activate', 'foo'],
-        output: allOf([
+        silent: allOf([
           contains('Downloading bar 1.0.0...'),
           contains('Downloading baz 1.0.0...')
         ]));
diff --git a/test/global/activate/reactivating_git_upgrades_test.dart b/test/global/activate/reactivating_git_upgrades_test.dart
index 5eb8d5f..6ce628d 100644
--- a/test/global/activate/reactivating_git_upgrades_test.dart
+++ b/test/global/activate/reactivating_git_upgrades_test.dart
@@ -36,7 +36,7 @@
             startsWith('Package foo is currently active from Git repository '
                 '"..${separator}foo.git".\n'
                 'Resolving dependencies...\n'
-                '+ foo 1.0.1 from git ..${separator}foo.git at '),
+                '> foo 1.0.1 from git ..${separator}foo.git at '),
             // Specific revision number goes here.
             endsWith('Building package executables...\n'
                 'Built foo:foo.\n'
diff --git a/test/global/activate/uncached_package_test.dart b/test/global/activate/uncached_package_test.dart
index 1a18d70..cb8d26d 100644
--- a/test/global/activate/uncached_package_test.dart
+++ b/test/global/activate/uncached_package_test.dart
@@ -23,7 +23,6 @@
     await runPub(args: ['global', 'activate', 'foo'], output: '''
         Resolving dependencies...
         + foo 1.2.3
-        Downloading foo 1.2.3...
         Building package executables...
         Built foo:foo.
         Activated foo 1.2.3.''');
diff --git a/test/global/deactivate/deactivate_and_reactivate_package_test.dart b/test/global/deactivate/deactivate_and_reactivate_package_test.dart
index da33486..5a6f6e3 100644
--- a/test/global/deactivate/deactivate_and_reactivate_package_test.dart
+++ b/test/global/deactivate/deactivate_and_reactivate_package_test.dart
@@ -20,10 +20,12 @@
         output: 'Deactivated package foo 1.0.0.');
 
     // Activating again should forget the old version.
-    await runPub(args: ['global', 'activate', 'foo'], output: '''
+    await runPub(
+        args: ['global', 'activate', 'foo'],
+        silent: contains('Downloading foo 2.0.0...'),
+        output: '''
         Resolving dependencies...
         + foo 2.0.0
-        Downloading foo 2.0.0...
         Activated foo 2.0.0.''');
   });
 }
diff --git a/test/hosted/short_syntax_test.dart b/test/hosted/short_syntax_test.dart
index 5d4cf28..da6d754 100644
--- a/test/hosted/short_syntax_test.dart
+++ b/test/hosted/short_syntax_test.dart
@@ -45,6 +45,7 @@
         'description': {
           'name': 'foo',
           'url': globalServer.url,
+          'sha256': matches(RegExp(r'[0-9a-f]{64}'))
         },
         'version': '1.2.3',
       });
diff --git a/test/lock_file_test.dart b/test/lock_file_test.dart
index 1bfac5e..20e3976 100644
--- a/test/lock_file_test.dart
+++ b/test/lock_file_test.dart
@@ -222,21 +222,27 @@
     test('serialize() dumps the lockfile to YAML', () {
       var lockfile = LockFile([
         PackageId(
-            'foo',
-            Version.parse('1.2.3'),
-            ResolvedHostedDescription(
-                HostedDescription('foo', 'https://foo.com'))),
+          'foo',
+          Version.parse('1.2.3'),
+          ResolvedHostedDescription(
+            HostedDescription('foo', 'https://foo.com'),
+            sha256: null,
+          ),
+        ),
         PackageId(
-            'bar',
-            Version.parse('3.2.1'),
-            ResolvedHostedDescription(
-                HostedDescription('bar', 'https://bar.com'))),
+          'bar',
+          Version.parse('3.2.1'),
+          ResolvedHostedDescription(
+            HostedDescription('bar', 'https://bar.com'),
+            sha256: null,
+          ),
+        ),
       ], devDependencies: {
         'bar'
       });
 
       expect(
-          loadYaml(lockfile.serialize('')),
+          loadYaml(lockfile.serialize('', cache)),
           equals({
             'sdks': {'dart': 'any'},
             'packages': {
diff --git a/test/oauth2/utils.dart b/test/oauth2/utils.dart
index dab8c92..b486f56 100644
--- a/test/oauth2/utils.dart
+++ b/test/oauth2/utils.dart
@@ -35,8 +35,7 @@
   // sign-in with Google account.
   var response =
       await (http.Request('GET', redirectUrl)..followRedirects = false).send();
-  expect(response.headers['location'],
-      equals('https://pub.dartlang.org/authorized'));
+  expect(response.headers['location'], equals('https://pub.dev/authorized'));
 }
 
 void handleAccessTokenRequest(PackageServer server, String accessToken) {
diff --git a/test/package_server.dart b/test/package_server.dart
index b7587e7..06a8d1b 100644
--- a/test/package_server.dart
+++ b/test/package_server.dart
@@ -7,10 +7,12 @@
 import 'dart:io';
 import 'dart:typed_data';
 
+import 'package:crypto/crypto.dart';
 import 'package:path/path.dart' as p;
 import 'package:pub/src/crc32c.dart';
 import 'package:pub/src/source/hosted.dart';
 import 'package:pub/src/third_party/tar/tar.dart';
+import 'package:pub/src/utils.dart' show hexEncode;
 import 'package:pub_semver/pub_semver.dart';
 import 'package:shelf/shelf.dart' as shelf;
 import 'package:shelf/shelf_io.dart' as shelf_io;
@@ -27,9 +29,12 @@
   /// Handlers of requests. Last matching handler will be used.
   final List<_PatternAndHandler> _handlers = [];
 
-  // A list of all the requests recieved up till now.
+  // A list of all the requests received up till now.
   final List<String> requestedPaths = <String>[];
 
+  // Setting this to false will disable automatic calculation of content-hashes.
+  bool serveContentHashes = true;
+
   /// Whether the [IOServer] should compress the content, if possible.
   /// The default value is `false` (compression disabled).
   /// See [HttpServer.autoCompress] for details.
@@ -66,7 +71,7 @@
         PackageServer._(await shelf_io.IOServer.bind('localhost', 0));
     server.handle(
       _versionInfoPattern,
-      (shelf.Request request) {
+      (shelf.Request request) async {
         final parts = request.url.pathSegments;
         assert(parts[0] == 'api');
         assert(parts[1] == 'packages');
@@ -76,17 +81,26 @@
         if (package == null) {
           return shelf.Response.notFound('No package named $name');
         }
+
         return shelf.Response.ok(
             jsonEncode({
               'name': name,
               'uploaders': ['nweiz@google.com'],
-              'versions': package.versions.values
-                  .map((version) => packageVersionApiMap(
-                        server._inner.url.toString(),
-                        version.pubspec,
-                        retracted: version.isRetracted,
-                      ))
-                  .toList(),
+              'versions': [
+                for (final version in package.versions.values)
+                  {
+                    'pubspec': version.pubspec,
+                    'version': version.version.toString(),
+                    'archive_url':
+                        '${server.url}/packages/$name/versions/${version.version}.tar.gz',
+                    if (version.isRetracted) 'retracted': true,
+                    if (version.sha256 != null || server.serveContentHashes)
+                      'archive_sha256': version.sha256 ??
+                          hexEncode(
+                              (await sha256.bind(version.contents()).first)
+                                  .bytes)
+                  }
+              ],
               if (package.isDiscontinued) 'isDiscontinued': true,
               if (package.discontinuedReplacementText != null)
                 'replacedBy': package.discontinuedReplacementText,
@@ -196,6 +210,9 @@
   String get cachingPath =>
       p.join(d.sandbox, cachePath, 'hosted', 'localhost%58$port');
 
+  String get hashesCachingPath =>
+      p.join(d.sandbox, cachePath, 'hosted-hashes', 'localhost%58$port');
+
   /// A map from package names to the concrete packages to serve.
   final _packages = <String, _ServedPackage>{};
 
@@ -240,7 +257,7 @@
                   // file mode
                   mode: 420,
                   // size: 100,
-                  modified: DateTime.now(),
+                  modified: DateTime.fromMicrosecondsSinceEpoch(0),
                   userName: 'pub',
                   groupName: 'pub',
                 ),
@@ -253,13 +270,29 @@
         for (final e in contents ?? <d.Descriptor>[]) {
           addDescriptor(e, '');
         }
-        return Stream.fromIterable(entries)
+        return _replaceOs(Stream.fromIterable(entries)
             .transform(tarWriterWith(format: OutputFormat.gnuLongName))
-            .transform(gzip.encoder);
+            .transform(gzip.encoder));
       },
     );
   }
 
+  /// Replaces the entry at index 9 in [stream] with a 0. This replaces the os
+  /// entry of a gzip stream, giving us the same stream and thius stable testing
+  /// on all platforms.
+  ///
+  /// See https://www.rfc-editor.org/rfc/rfc1952 section 2.3 for information
+  /// about the OS header.
+  Stream<List<int>> _replaceOs(Stream<List<int>> stream) async* {
+    final bytesBuilder = BytesBuilder();
+    await for (final t in stream) {
+      bytesBuilder.add(t);
+    }
+    final result = bytesBuilder.toBytes();
+    result[9] = 0;
+    yield result;
+  }
+
   // Mark a package discontinued.
   void discontinue(String name,
       {bool isDiscontinued = true, String? replacementText}) {
@@ -277,6 +310,16 @@
     _packages[name]!.versions[version]!.isRetracted = true;
   }
 
+  /// Useful for testing handling of a wrong hash.
+  void overrideArchiveSha256(String name, String version, String sha256) {
+    _packages[name]!.versions[version]!.sha256 = sha256;
+  }
+
+  Future<String> peekArchiveSha256(String name, String version) async {
+    final v = _packages[name]!.versions[version]!;
+    return v.sha256 ?? hexEncode((await sha256.bind(v.contents()).first).bytes);
+  }
+
   Future<String?> peekArchiveChecksumHeader(String name, String version) async {
     final v = _packages[name]!.versions[version]!;
 
@@ -321,6 +364,8 @@
   final Stream<List<int>> Function() contents;
   final Map<String, List<String>>? headers;
   bool isRetracted = false;
+  // Overrides the calculated sha256.
+  String? sha256;
 
   Version get version => Version.parse(pubspec['version']);
 
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index 23079c5..c4e3112 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -292,8 +292,10 @@
         expect(foo.name, equals('foo'));
         expect(foo.source.name, 'hosted');
         expect(
-            ResolvedHostedDescription(foo.description as HostedDescription)
-                .serializeForLockfile(containingDir: null),
+            ResolvedHostedDescription(
+              foo.description as HostedDescription,
+              sha256: null,
+            ).serializeForLockfile(containingDir: null),
             {
               'url': 'https://example.org/pub/',
               'name': 'bar',
@@ -318,8 +320,10 @@
         expect(foo.name, equals('foo'));
         expect(foo.source.name, 'hosted');
         expect(
-            ResolvedHostedDescription(foo.description as HostedDescription)
-                .serializeForLockfile(containingDir: null),
+            ResolvedHostedDescription(
+              foo.description as HostedDescription,
+              sha256: null,
+            ).serializeForLockfile(containingDir: null),
             {
               'url': 'https://example.org/pub/',
               'name': 'foo',
@@ -343,8 +347,10 @@
         expect(foo.name, equals('foo'));
         expect(foo.source.name, 'hosted');
         expect(
-            ResolvedHostedDescription(foo.description as HostedDescription)
-                .serializeForLockfile(containingDir: null),
+            ResolvedHostedDescription(
+              foo.description as HostedDescription,
+              sha256: null,
+            ).serializeForLockfile(containingDir: null),
             {
               'url': 'https://example.org/pub/',
               'name': 'foo',
@@ -368,10 +374,12 @@
         expect(foo.name, equals('foo'));
         expect(foo.source.name, 'hosted');
         expect(
-            ResolvedHostedDescription(foo.description as HostedDescription)
-                .serializeForLockfile(containingDir: null),
+            ResolvedHostedDescription(
+              foo.description as HostedDescription,
+              sha256: null,
+            ).serializeForLockfile(containingDir: null),
             {
-              'url': 'https://pub.dartlang.org',
+              'url': 'https://pub.dev',
               'name': 'bar',
             });
       });
@@ -412,10 +420,12 @@
         expect(foo.name, equals('foo'));
         expect(foo.source.name, 'hosted');
         expect(
-            ResolvedHostedDescription(foo.description as HostedDescription)
-                .serializeForLockfile(containingDir: null),
+            ResolvedHostedDescription(
+              foo.description as HostedDescription,
+              sha256: null,
+            ).serializeForLockfile(containingDir: null),
             {
-              'url': 'https://pub.dartlang.org',
+              'url': 'https://pub.dev',
               'name': 'foo',
             });
       });
diff --git a/test/reformat_ranges_test.dart b/test/reformat_ranges_test.dart
index d2c91ca..2ead04d 100644
--- a/test/reformat_ranges_test.dart
+++ b/test/reformat_ranges_test.dart
@@ -12,6 +12,7 @@
 void main() {
   final description = ResolvedHostedDescription(
     HostedDescription('foo', 'https://pub.dev'),
+    sha256: null,
   );
   test('reformatMax when max has a build identifier', () {
     expect(
diff --git a/test/test_pub.dart b/test/test_pub.dart
index be89bda..58da455 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -24,6 +24,7 @@
 import 'package:pub/src/lock_file.dart';
 import 'package:pub/src/log.dart' as log;
 import 'package:pub/src/package_name.dart';
+import 'package:pub/src/source/hosted.dart';
 import 'package:pub/src/system_cache.dart';
 import 'package:pub/src/utils.dart';
 import 'package:pub/src/validator.dart';
@@ -207,6 +208,7 @@
         Object? output,
         Object? error,
         Object? warning,
+        Object? silent,
         int? exitCode,
         Map<String, String>? environment,
         String? workingDirectory}) async =>
@@ -216,6 +218,7 @@
       output: output,
       error: error,
       warning: warning,
+      silent: silent,
       exitCode: exitCode,
       environment: environment,
       workingDirectory: workingDirectory,
@@ -626,7 +629,8 @@
       _createLockFile(cache, sandbox: dependenciesInSandBox, hosted: hosted);
 
   await d.dir(package, [
-    d.file('pubspec.lock', lockFile.serialize(p.join(d.sandbox, package)))
+    d.file(
+        'pubspec.lock', lockFile.serialize(p.join(d.sandbox, package), cache))
   ]).create();
 }
 
@@ -653,7 +657,17 @@
         containingDir: p.join(d.sandbox, appPath))),
     if (hosted != null)
       ...hosted.entries.map(
-          (entry) => cache.hosted.idFor(entry.key, Version.parse(entry.value)))
+        (entry) => PackageId(
+          entry.key,
+          Version.parse(entry.value),
+          ResolvedHostedDescription(
+              HostedDescription(
+                entry.key,
+                'https://pub.dev',
+              ),
+              sha256: null),
+        ),
+      )
   ];
 
   return LockFile(packages);
@@ -693,38 +707,6 @@
   return package;
 }
 
-/// Returns a Map in the format used by the pub.dev API to represent a
-/// package version.
-///
-/// [pubspec] is the parsed pubspec of the package version. If [full] is true,
-/// this returns the complete map, including metadata that's only included when
-/// requesting the package version directly.
-Map packageVersionApiMap(String hostedUrl, Map pubspec,
-    {bool retracted = false, bool full = false}) {
-  var name = pubspec['name'];
-  var version = pubspec['version'];
-  var map = {
-    'pubspec': pubspec,
-    'version': version,
-    'archive_url': '$hostedUrl/packages/$name/versions/$version.tar.gz',
-  };
-
-  if (retracted) {
-    map['retracted'] = true;
-  }
-
-  if (full) {
-    map.addAll({
-      'downloads': 0,
-      'created': '2012-09-25T18:38:28.685260',
-      'libraries': ['$name.dart'],
-      'uploader': ['nweiz@google.com']
-    });
-  }
-
-  return map;
-}
-
 /// Returns the name of the shell script for a binstub named [name].
 ///
 /// Adds a ".bat" extension on Windows.
@@ -884,9 +866,6 @@
 /// Removes output from pub known to be unstable.
 Iterable<String> filterUnstableLines(List<String> input) {
   return input
-      // Downloading order is not deterministic, so to avoid flakiness we filter
-      // out these lines.
-      .where((line) => !line.startsWith('Downloading '))
       // Any paths in output should be relative to the sandbox and with forward
       // slashes to be stable across platforms.
       .map((line) {
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
index 7b01ac0..3b16db1 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
@@ -10,6 +10,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -30,7 +31,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
         }
       }
     }
@@ -51,7 +53,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
         }
       },
       "latest": "2.2.3",
@@ -66,7 +69,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "fc06d01652f7b73f789abeb5b61aeb68b13cd472f87610cb8fb80e402a9139ff"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -78,7 +82,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
             }
           }
         },
@@ -90,7 +95,8 @@
             "type": "hosted",
             "description": {
               "name": "transitive",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
             }
           },
           "constraintBumped": null,
@@ -110,7 +116,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "fc06d01652f7b73f789abeb5b61aeb68b13cd472f87610cb8fb80e402a9139ff"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -122,7 +129,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
             }
           }
         },
@@ -134,7 +142,8 @@
             "type": "hosted",
             "description": {
               "name": "transitive",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
             }
           },
           "constraintBumped": null,
@@ -167,6 +176,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: fc06d01652f7b73f789abeb5b61aeb68b13cd472f87610cb8fb80e402a9139ff
       url: "http://localhost:$PORT"
     source: hosted
     version: "2.2.3"
@@ -174,6 +184,7 @@
     dependency: transitive
     description:
       name: transitive
+      sha256: "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
index b4726c5..4fbaffb 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
@@ -10,6 +10,7 @@
     dependency: "direct main"
     description:
       name: bar
+      sha256: ea004e8b0069df9e9827b101b64aaad455cc358849f1801dc48a41111cabbe20
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -17,6 +18,7 @@
     dependency: "direct main"
     description:
       name: boo
+      sha256: "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -24,6 +26,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -44,7 +47,8 @@
         "type": "hosted",
         "description": {
           "name": "bar",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "ea004e8b0069df9e9827b101b64aaad455cc358849f1801dc48a41111cabbe20"
         }
       }
     },
@@ -57,7 +61,8 @@
         "type": "hosted",
         "description": {
           "name": "boo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
         }
       }
     },
@@ -70,7 +75,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
         }
       }
     }
@@ -91,7 +97,8 @@
         "type": "hosted",
         "description": {
           "name": "bar",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "ea004e8b0069df9e9827b101b64aaad455cc358849f1801dc48a41111cabbe20"
         }
       },
       "latest": "2.2.3",
@@ -106,7 +113,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "adcfe9ac3d6955fd4332f29f47bf3e814e388e2da7c2bc55d4561971bf8b5335"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -118,7 +126,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "ea004e8b0069df9e9827b101b64aaad455cc358849f1801dc48a41111cabbe20"
             }
           }
         }
@@ -132,7 +141,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "adcfe9ac3d6955fd4332f29f47bf3e814e388e2da7c2bc55d4561971bf8b5335"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -144,7 +154,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "ea004e8b0069df9e9827b101b64aaad455cc358849f1801dc48a41111cabbe20"
             }
           }
         }
@@ -158,7 +169,8 @@
         "type": "hosted",
         "description": {
           "name": "boo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
         }
       },
       "latest": "1.2.4",
@@ -172,7 +184,8 @@
             "type": "hosted",
             "description": {
               "name": "boo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "b060c0315b77c8383da5f9a7eee7667dbdc8108969e0a7855e294e35e7f42230"
             }
           },
           "constraintBumped": "^1.0.0",
@@ -184,7 +197,8 @@
             "type": "hosted",
             "description": {
               "name": "boo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
             }
           }
         }
@@ -198,7 +212,8 @@
             "type": "hosted",
             "description": {
               "name": "boo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "b060c0315b77c8383da5f9a7eee7667dbdc8108969e0a7855e294e35e7f42230"
             }
           },
           "constraintBumped": "^1.2.4",
@@ -210,7 +225,8 @@
             "type": "hosted",
             "description": {
               "name": "boo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
             }
           }
         }
@@ -224,7 +240,8 @@
             "type": "hosted",
             "description": {
               "name": "boo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "b060c0315b77c8383da5f9a7eee7667dbdc8108969e0a7855e294e35e7f42230"
             }
           },
           "constraintBumped": "^1.2.4",
@@ -236,7 +253,8 @@
             "type": "hosted",
             "description": {
               "name": "boo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
             }
           }
         }
@@ -250,7 +268,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
         }
       },
       "latest": "2.2.3",
@@ -264,7 +283,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "88f2f9251967bf04bd478873f074b9d8df9f1c959afc150ba3b0ea813d48161e"
             }
           },
           "constraintBumped": "^1.0.0",
@@ -276,7 +296,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
             }
           }
         }
@@ -290,7 +311,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -302,7 +324,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
             }
           }
         }
@@ -316,7 +339,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -328,7 +352,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "1614d63c0867d0994f75a231be7ee394a4f30cdeede4c7ea471fcad354c23d1f"
             }
           }
         }
@@ -355,6 +380,7 @@
     dependency: "direct main"
     description:
       name: bar
+      sha256: ea004e8b0069df9e9827b101b64aaad455cc358849f1801dc48a41111cabbe20
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -362,6 +388,7 @@
     dependency: "direct main"
     description:
       name: boo
+      sha256: "7971e197614f18130070007a54f446366c6e594f0ed159ae2c4e2b42972c426b"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -369,6 +396,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "88f2f9251967bf04bd478873f074b9d8df9f1c959afc150ba3b0ea813d48161e"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.4"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt b/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
index e5ab484..a6709fc 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
@@ -34,7 +34,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
         }
       }
     },
@@ -47,7 +48,8 @@
         "type": "hosted",
         "description": {
           "name": "transitive",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
         }
       }
     }
@@ -86,7 +88,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -98,7 +101,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
             }
           }
         }
@@ -112,7 +116,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
         }
       },
       "latest": "2.2.3",
@@ -127,7 +132,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -139,7 +145,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
             }
           }
         }
@@ -153,7 +160,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -165,7 +173,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
             }
           }
         }
@@ -179,7 +188,8 @@
         "type": "hosted",
         "description": {
           "name": "transitive",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
         }
       },
       "latest": "1.0.0",
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt
new file mode 100644
index 0000000..bd707b4
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt
@@ -0,0 +1,384 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0","boo":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      name: bar
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+  boo:
+    dependency: "direct main"
+    description:
+      name: boo
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+  foo:
+    dependency: "direct main"
+    description:
+      name: foo
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+sdks:
+  dart: ">=0.1.2 <1.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT"
+        }
+      }
+    },
+    {
+      "name": "boo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "boo",
+          "url": "http://localhost:$PORT"
+        }
+      }
+    },
+    {
+      "name": "foo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      }
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT"
+        }
+      },
+      "latest": "2.2.3",
+      "constraint": "^1.0.0",
+      "compatible": [],
+      "singleBreaking": [
+        {
+          "name": "bar",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "adcfe9ac3d6955fd4332f29f47bf3e814e388e2da7c2bc55d4561971bf8b5335"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "bar",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "adcfe9ac3d6955fd4332f29f47bf3e814e388e2da7c2bc55d4561971bf8b5335"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "boo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "boo",
+          "url": "http://localhost:$PORT"
+        }
+      },
+      "latest": "1.2.4",
+      "constraint": "^1.0.0",
+      "compatible": [
+        {
+          "name": "boo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT",
+              "sha256": "b060c0315b77c8383da5f9a7eee7667dbdc8108969e0a7855e294e35e7f42230"
+            }
+          },
+          "constraintBumped": "^1.0.0",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ],
+      "singleBreaking": [
+        {
+          "name": "boo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT",
+              "sha256": "b060c0315b77c8383da5f9a7eee7667dbdc8108969e0a7855e294e35e7f42230"
+            }
+          },
+          "constraintBumped": "^1.2.4",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "boo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT",
+              "sha256": "b060c0315b77c8383da5f9a7eee7667dbdc8108969e0a7855e294e35e7f42230"
+            }
+          },
+          "constraintBumped": "^1.2.4",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      },
+      "latest": "2.2.3",
+      "constraint": "^1.0.0",
+      "compatible": [
+        {
+          "name": "foo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "88f2f9251967bf04bd478873f074b9d8df9f1c959afc150ba3b0ea813d48161e"
+            }
+          },
+          "constraintBumped": "^1.0.0",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ],
+      "singleBreaking": [
+        {
+          "name": "foo",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "foo",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
+        }
+      ]
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"1.2.4"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0","boo":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      name: bar
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+  boo:
+    dependency: "direct main"
+    description:
+      name: boo
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+  foo:
+    dependency: "direct main"
+    description:
+      name: foo
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.4"
+sdks:
+  dart: ">=0.1.2 <1.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
index 69a06d4..2e38773 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
@@ -17,6 +17,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
@@ -50,7 +51,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
         }
       }
     }
@@ -88,7 +90,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
         }
       },
       "latest": "2.0.0",
@@ -103,7 +106,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "c3bda774737102f799574749076544dea1a4745b5c38d590d4f206f997bfe8a0"
             }
           },
           "constraintBumped": "^2.0.0",
@@ -115,7 +119,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
             }
           }
         }
@@ -129,7 +134,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "c3bda774737102f799574749076544dea1a4745b5c38d590d4f206f997bfe8a0"
             }
           },
           "constraintBumped": "^2.0.0",
@@ -141,7 +147,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
             }
           }
         }
@@ -175,6 +182,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: c3bda774737102f799574749076544dea1a4745b5c38d590d4f206f997bfe8a0
       url: "http://localhost:$PORT"
     source: hosted
     version: "2.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
index 50ee46f..0498611 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
@@ -10,6 +10,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.2.3"
@@ -17,6 +18,7 @@
     dependency: transitive
     description:
       name: transitive
+      sha256: "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
@@ -37,7 +39,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
         }
       }
     },
@@ -50,7 +53,8 @@
         "type": "hosted",
         "description": {
           "name": "transitive",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
         }
       }
     }
@@ -71,7 +75,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
         }
       },
       "latest": "2.2.3",
@@ -86,7 +91,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -98,7 +104,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
             }
           }
         },
@@ -115,7 +122,8 @@
             "type": "hosted",
             "description": {
               "name": "transitive",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
             }
           }
         }
@@ -129,7 +137,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e"
             }
           },
           "constraintBumped": "^2.2.3",
@@ -141,7 +150,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "72f6a04c4af0d78e4f1a1e2eb00a850843e6c0c5233ac2ca911aa061cbd5f8f1"
             }
           }
         },
@@ -158,7 +168,8 @@
             "type": "hosted",
             "description": {
               "name": "transitive",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
             }
           }
         }
@@ -172,7 +183,8 @@
         "type": "hosted",
         "description": {
           "name": "transitive",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "8d245de5cde3ab3293e4cdea516c6a0395e24d338688279bab5f6c97bffa0915"
         }
       },
       "latest": "1.0.0",
@@ -202,6 +214,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: bf378a3f6c4840f911d66ab375f6d3eae78a015a41f0b8b202c31d4af010892e
       url: "http://localhost:$PORT"
     source: hosted
     version: "2.2.3"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
index da9222d..3767075 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
@@ -10,6 +10,7 @@
     dependency: "direct main"
     description:
       name: bar
+      sha256: "4de00552ae3719481f5f0e30b82ecb8b14a62907553b217e7ca178e80625329a"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
@@ -17,6 +18,7 @@
     dependency: "direct main"
     description:
       name: baz
+      sha256: "377433f0e0aff092191e57de97f5869cad0dd0779ee6d31e7096b84878ca41e8"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
@@ -24,6 +26,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
@@ -44,7 +47,8 @@
         "type": "hosted",
         "description": {
           "name": "bar",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "4de00552ae3719481f5f0e30b82ecb8b14a62907553b217e7ca178e80625329a"
         }
       }
     },
@@ -57,7 +61,8 @@
         "type": "hosted",
         "description": {
           "name": "baz",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "377433f0e0aff092191e57de97f5869cad0dd0779ee6d31e7096b84878ca41e8"
         }
       }
     },
@@ -70,7 +75,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
         }
       }
     }
@@ -91,7 +97,8 @@
         "type": "hosted",
         "description": {
           "name": "bar",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "4de00552ae3719481f5f0e30b82ecb8b14a62907553b217e7ca178e80625329a"
         }
       },
       "latest": "2.0.0",
@@ -107,7 +114,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "b8187621010649d6385788d7630adcd88d6548a7938899b6f18820961df3b879"
             }
           },
           "constraintBumped": "^2.0.0",
@@ -119,7 +127,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "4de00552ae3719481f5f0e30b82ecb8b14a62907553b217e7ca178e80625329a"
             }
           }
         },
@@ -131,7 +140,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "2347a7792f73d0f8cc8aa41d4895317bd1745724b8bc77d8c03faf821c9059b7"
             }
           },
           "constraintBumped": "^3.0.1",
@@ -143,7 +153,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
             }
           }
         }
@@ -157,7 +168,8 @@
         "type": "hosted",
         "description": {
           "name": "baz",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "377433f0e0aff092191e57de97f5869cad0dd0779ee6d31e7096b84878ca41e8"
         }
       },
       "latest": "1.1.0",
@@ -172,7 +184,8 @@
             "type": "hosted",
             "description": {
               "name": "baz",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "7474da026b513eafecba9d1c79a8a3b4a9ef5158730e0968383063b3237c5dec"
             }
           },
           "constraintBumped": "^1.1.0",
@@ -184,7 +197,8 @@
             "type": "hosted",
             "description": {
               "name": "baz",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "377433f0e0aff092191e57de97f5869cad0dd0779ee6d31e7096b84878ca41e8"
             }
           }
         }
@@ -198,7 +212,8 @@
             "type": "hosted",
             "description": {
               "name": "baz",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "7474da026b513eafecba9d1c79a8a3b4a9ef5158730e0968383063b3237c5dec"
             }
           },
           "constraintBumped": "^1.1.0",
@@ -210,7 +225,8 @@
             "type": "hosted",
             "description": {
               "name": "baz",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "377433f0e0aff092191e57de97f5869cad0dd0779ee6d31e7096b84878ca41e8"
             }
           }
         }
@@ -224,7 +240,8 @@
         "type": "hosted",
         "description": {
           "name": "foo",
-          "url": "http://localhost:$PORT"
+          "url": "http://localhost:$PORT",
+          "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
         }
       },
       "latest": "3.0.1",
@@ -238,7 +255,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "efa386ac7cc7698525e2e820a90e6bcee5d6c071de4315051a0fb2f3aff5d084"
             }
           },
           "constraintBumped": "^1.0.0",
@@ -250,7 +268,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
             }
           }
         }
@@ -264,7 +283,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "c3bda774737102f799574749076544dea1a4745b5c38d590d4f206f997bfe8a0"
             }
           },
           "constraintBumped": "^2.0.0",
@@ -276,7 +296,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
             }
           }
         }
@@ -290,7 +311,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "2347a7792f73d0f8cc8aa41d4895317bd1745724b8bc77d8c03faf821c9059b7"
             }
           },
           "constraintBumped": "^3.0.1",
@@ -302,7 +324,8 @@
             "type": "hosted",
             "description": {
               "name": "foo",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
             }
           }
         },
@@ -314,7 +337,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "b8187621010649d6385788d7630adcd88d6548a7938899b6f18820961df3b879"
             }
           },
           "constraintBumped": "^2.0.0",
@@ -326,7 +350,8 @@
             "type": "hosted",
             "description": {
               "name": "bar",
-              "url": "http://localhost:$PORT"
+              "url": "http://localhost:$PORT",
+              "sha256": "4de00552ae3719481f5f0e30b82ecb8b14a62907553b217e7ca178e80625329a"
             }
           }
         }
@@ -353,6 +378,7 @@
     dependency: "direct main"
     description:
       name: bar
+      sha256: b8187621010649d6385788d7630adcd88d6548a7938899b6f18820961df3b879
       url: "http://localhost:$PORT"
     source: hosted
     version: "2.0.0"
@@ -360,6 +386,7 @@
     dependency: "direct main"
     description:
       name: baz
+      sha256: "377433f0e0aff092191e57de97f5869cad0dd0779ee6d31e7096b84878ca41e8"
       url: "http://localhost:$PORT"
     source: hosted
     version: "1.0.0"
@@ -367,6 +394,7 @@
     dependency: "direct main"
     description:
       name: foo
+      sha256: "2347a7792f73d0f8cc8aa41d4895317bd1745724b8bc77d8c03faf821c9059b7"
       url: "http://localhost:$PORT"
     source: hosted
     version: "3.0.1"
diff --git a/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt b/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
index d468889..e4ad1d1 100644
--- a/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
+++ b/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
@@ -3,7 +3,6 @@
 $ tool/test-bin/pub_command_runner.dart pub --no-color get
 Resolving dependencies...
 + foo 1.0.0 (2.0.0 available)
-Downloading foo 1.0.0...
 Changed 1 dependency!
 
 -------------------------------- END OF OUTPUT ---------------------------------
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index dc09988..219820e 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -3,7 +3,6 @@
 $ tool/test-bin/pub_command_runner.dart pub --verbose get
 MSG : Resolving dependencies...
 MSG : + foo 1.0.0
-MSG : Downloading foo 1.0.0...
 MSG : Changed 1 dependency!
 MSG : Logs written to $SANDBOX/cache/log/pub_log.txt.
 [E] FINE: Pub 0.1.2+3
@@ -25,19 +24,20 @@
 [E]    | took: $TIME
 [E]    | x-powered-by: Dart with package:shelf
 [E]    | date: $TIME
-[E]    | content-length: 197
+[E]    | content-length: 281
 [E]    | x-frame-options: SAMEORIGIN
 [E]    | content-type: application/vnd.pub.v2+json
 [E]    | x-xss-protection: 1; mode=block
 [E]    | x-content-type-options: nosniff
 [E] IO  : Writing $N characters to text file $SANDBOX/cache/hosted/localhost%58$PORT/.cache/foo-versions.json.
 [E] FINE: Contents:
-[E]    | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz"}],"_fetchedAt": "$TIME"}
+[E]    | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz","archive_sha256":"439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"}],"_fetchedAt": "$TIME"}
 [E] SLVR:   selecting foo 1.0.0
 [E] SLVR: Version solving took: $TIME
 [E]    | Tried 1 solutions.
 [E] FINE: Resolving dependencies finished ($TIME)
 [E] IO  : Get package from http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz.
+[E] FINE: Downloading foo 1.0.0...
 [E] IO  : Created temp directory $DIR
 [E] IO  : HTTP GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz
 [E]    | X-Pub-OS: $OS
@@ -58,6 +58,9 @@
 [E]    | x-content-type-options: nosniff
 [E] IO  : Creating $FILE from stream
 [E] FINE: Computed checksum $CRC32C for foo 1.0.0 with expected CRC32C of $CRC32C.
+[E] IO  : Writing $N characters to text file $SANDBOX/cache/hosted-hashes/localhost%58$PORT/foo-1.0.0.sha256.
+[E] FINE: Contents:
+[E]    | 439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb
 [E] FINE: Created $FILE from stream
 [E] IO  : Created temp directory $DIR
 [E] IO  : Reading binary file $FILE.
@@ -78,6 +81,7 @@
 [E]    |   dependency: "direct main"
 [E]    |   description:
 [E]    |   name: foo
+[E]    |   sha256: "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
 [E]    |   url: "http://localhost:$PORT"
 [E]    |   source: hosted
 [E]    |   version: "1.0.0"
@@ -135,6 +139,7 @@
    dependency: "direct main"
    description:
    name: foo
+   sha256: "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
    url: "http://localhost:$PORT"
    source: hosted
    version: "1.0.0"
@@ -163,21 +168,20 @@
    | took: $TIME
    | x-powered-by: Dart with package:shelf
    | date: $TIME
-   | content-length: 197
+   | content-length: 281
    | x-frame-options: SAMEORIGIN
    | content-type: application/vnd.pub.v2+json
    | x-xss-protection: 1; mode=block
    | x-content-type-options: nosniff
 IO  : Writing $N characters to text file $SANDBOX/cache/hosted/localhost%58$PORT/.cache/foo-versions.json.
 FINE: Contents:
-   | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz"}],"_fetchedAt": "$TIME"}
+   | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz","archive_sha256":"439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"}],"_fetchedAt": "$TIME"}
 SLVR:   selecting foo 1.0.0
 SLVR: Version solving took: $TIME
    | Tried 1 solutions.
 FINE: Resolving dependencies finished ($TIME)
-MSG : + foo 1.0.0
 IO  : Get package from http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz.
-MSG : Downloading foo 1.0.0...
+FINE: Downloading foo 1.0.0...
 IO  : Created temp directory $DIR
 IO  : HTTP GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz
    | X-Pub-OS: $OS
@@ -198,6 +202,9 @@
    | x-content-type-options: nosniff
 IO  : Creating $FILE from stream
 FINE: Computed checksum $CRC32C for foo 1.0.0 with expected CRC32C of $CRC32C.
+IO  : Writing $N characters to text file $SANDBOX/cache/hosted-hashes/localhost%58$PORT/foo-1.0.0.sha256.
+FINE: Contents:
+   | 439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb
 FINE: Created $FILE from stream
 IO  : Created temp directory $DIR
 IO  : Reading binary file $FILE.
@@ -209,6 +216,7 @@
 FINE: Extracted .tar.gz to $DIR
 IO  : Renaming directory $A to $B
 IO  : Deleting directory $DIR
+MSG : + foo 1.0.0
 IO  : Writing $N characters to text file pubspec.lock.
 FINE: Contents:
    | # Generated by pub
@@ -218,6 +226,7 @@
    |   dependency: "direct main"
    |   description:
    |   name: foo
+   |   sha256: "439814f59cbc73e1c28ca5ac6e437d5f2af10dfd18db786ce46fe0663e605ccb"
    |   url: "http://localhost:$PORT"
    |   source: hosted
    |   version: "1.0.0"
diff --git a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401-with-message.txt b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401-with-message.txt
index 8dcaaf5..61985a1 100644
--- a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401-with-message.txt
+++ b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401-with-message.txt
@@ -7,7 +7,7 @@
 [STDERR] 
 [STDERR] http://localhost:$PORT package repository requested authentication!
 [STDERR] You can provide credentials using:
-[STDERR]     pub token add http://localhost:$PORT
+[STDERR]     dart pub token add http://localhost:$PORT
 [STDERR] <message>
 [EXIT CODE] 69
 
diff --git a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401.txt b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401.txt
index 68d5bd0..d6f0005 100644
--- a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401.txt
+++ b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/401.txt
@@ -7,6 +7,6 @@
 [STDERR] 
 [STDERR] http://localhost:$PORT package repository requested authentication!
 [STDERR] You can provide credentials using:
-[STDERR]     pub token add http://localhost:$PORT
+[STDERR]     dart pub token add http://localhost:$PORT
 [EXIT CODE] 69
 
diff --git a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403-with-message.txt b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403-with-message.txt
index 882660c..55363f5 100644
--- a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403-with-message.txt
+++ b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403-with-message.txt
@@ -7,7 +7,7 @@
 [STDERR] 
 [STDERR] Insufficient permissions to the resource at the http://localhost:$PORT package repository.
 [STDERR] You can modify credentials using:
-[STDERR]     pub token add http://localhost:$PORT
+[STDERR]     dart pub token add http://localhost:$PORT
 [STDERR] <message>
 [EXIT CODE] 69
 
diff --git a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403.txt b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403.txt
index f8a5af9..67ff232 100644
--- a/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403.txt
+++ b/test/testdata/goldens/hosted/fail_gracefully_on_bad_version_listing_response_test/403.txt
@@ -7,6 +7,6 @@
 [STDERR] 
 [STDERR] Insufficient permissions to the resource at the http://localhost:$PORT package repository.
 [STDERR] You can modify credentials using:
-[STDERR]     pub token add http://localhost:$PORT
+[STDERR]     dart pub token add http://localhost:$PORT
 [EXIT CODE] 69
 
diff --git a/test/token/add_token_test.dart b/test/token/add_token_test.dart
index e8d0dc4..3fa0d7f 100644
--- a/test/token/add_token_test.dart
+++ b/test/token/add_token_test.dart
@@ -151,18 +151,18 @@
     );
   });
 
-  test('with https://pub.dev rewrites to https://pub.dartlang.org', () async {
+  test('with https://pub.dartlang.org rewrites to https://pub.dev', () async {
     await runPub(
-      args: ['token', 'add', 'https://pub.dev'],
+      args: ['token', 'add', 'https://pub.dartlang.org'],
       input: ['auth-token'],
       silent: contains(
-          'Using https://pub.dartlang.org instead of https://pub.dev.'),
+          'Using https://pub.dev instead of https://pub.dartlang.org.'),
     );
 
     await d.tokensFile({
       'version': 1,
       'hosted': [
-        {'url': 'https://pub.dartlang.org', 'token': 'auth-token'}
+        {'url': 'https://pub.dev', 'token': 'auth-token'}
       ]
     }).validate();
   });
diff --git a/test/upgrade/dry_run_does_not_apply_changes_test.dart b/test/upgrade/dry_run_does_not_apply_changes_test.dart
index 8c942ca..4f514f9 100644
--- a/test/upgrade/dry_run_does_not_apply_changes_test.dart
+++ b/test/upgrade/dry_run_does_not_apply_changes_test.dart
@@ -68,6 +68,7 @@
     // Do the dry run.
     await pubUpgrade(
       args: ['--dry-run', '--major-versions'],
+      silent: contains('Downloading foo 2.0.0...'),
       output: allOf([
         contains('Resolving dependencies...'),
         contains('> foo 2.0.0 (was 1.0.0)'),
@@ -92,7 +93,6 @@
       output: allOf([
         contains('Resolving dependencies...'),
         contains('> foo 2.0.0 (was 1.0.0)'),
-        contains('Downloading foo 2.0.0...'),
         contains('Changed 1 dependency!'),
         contains('Changed 1 constraint in pubspec.yaml:'),
         contains('foo: ^1.0.0 -> ^2.0.0'),
diff --git a/test/upgrade/report/describes_change_test.dart b/test/upgrade/report/describes_change_test.dart
index 96410cb..2f92618 100644
--- a/test/upgrade/report/describes_change_test.dart
+++ b/test/upgrade/report/describes_change_test.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:path/path.dart';
 import 'package:test/test.dart';
 
 import '../../descriptor.dart' as d;
@@ -33,11 +34,17 @@
   });
 
   test('shows how package changed from previous lockfile', () async {
-    await servePackages()
-      ..serve('unchanged', '1.0.0')
-      ..serve('version_changed', '1.0.0')
-      ..serve('version_changed', '2.0.0')
-      ..serve('source_changed', '1.0.0');
+    final server = await servePackages();
+
+    server.serve('unchanged', '1.0.0');
+    server.serve('version_upgraded', '1.0.0');
+    server.serve('version_upgraded', '2.0.0');
+    server.serve('version_downgraded', '1.0.0');
+    server.serve('version_downgraded', '2.0.0');
+    server.serve('contents_changed', '1.0.0');
+    server.serve('source_changed', '1.0.0');
+    server.serve('package_added', '1.0.0');
+    server.serve('package_removed', '1.0.0');
 
     await d.dir('source_changed', [
       d.libDir('source_changed'),
@@ -57,28 +64,47 @@
     // Create the first lockfile.
     await d.appDir({
       'unchanged': 'any',
-      'version_changed': '1.0.0',
+      'contents_changed': '1.0.0',
+      'version_upgraded': '1.0.0',
+      'version_downgraded': '2.0.0',
       'source_changed': 'any',
+      'package_removed': 'any',
       'description_changed': {'path': '../description_changed_1'}
     }).create();
 
     await pubGet();
+    server.serve(
+      'contents_changed',
+      '1.0.0',
+      contents: [d.file('Sneaky.txt', 'Very sneaky attack on integrity.')],
+    );
 
     // Change the pubspec.
     await d.appDir({
       'unchanged': 'any',
-      'version_changed': 'any',
+      'version_upgraded': 'any',
+      'version_downgraded': '1.0.0',
       'source_changed': {'path': '../source_changed'},
-      'description_changed': {'path': '../description_changed_2'}
+      'package_added': 'any',
+      'description_changed': {'path': '../description_changed_2'},
+      'contents_changed': '1.0.0',
     }).create();
 
     // Upgrade everything.
-    await pubUpgrade(output: RegExp(r'''
-Resolving dependencies\.\.\..*
-. description_changed 1\.0\.0 from path \.\.[/\\]description_changed_2 \(was 1\.0\.0 from path \.\.[/\\]description_changed_1\)
-. source_changed 2\.0\.0 from path \.\.[/\\]source_changed \(was 1\.0\.0\)
-. unchanged 1\.0\.0
-. version_changed 2\.0\.0 \(was 1\.0\.0\)
-''', multiLine: true), environment: {'PUB_ALLOW_PRERELEASE_SDK': 'false'});
+    await pubUpgrade(
+        output: allOf([
+          contains('Resolving dependencies...'),
+          contains(
+              '* description_changed 1.0.0 from path ..${separator}description_changed_2 (was 1.0.0 from path ..${separator}description_changed_1)'),
+          contains('  unchanged 1.0.0'),
+          contains(
+              '* source_changed 2.0.0 from path ..${separator}source_changed (was 1.0.0)'),
+          contains('> version_upgraded 2.0.0 (was 1.0.0'),
+          contains('< version_downgraded 1.0.0 (was 2.0.0'),
+          contains('+ package_added 1.0.0'),
+          contains('- package_removed 1.0.0'),
+          contains('~ contents_changed 1.0.0 (was 1.0.0)'),
+        ]),
+        environment: {'PUB_ALLOW_PRERELEASE_SDK': 'false'});
   });
 }
diff --git a/test/utils_test.dart b/test/utils_test.dart
index 78ccbaf..cc51066 100644
--- a/test/utils_test.dart
+++ b/test/utils_test.dart
@@ -162,4 +162,18 @@
       }
     });
   });
+
+  test('hexEncode', () {
+    expect(hexEncode([]), '');
+    expect(hexEncode([255, 0, 1, 240]), 'ff0001f0');
+    expect(() => hexEncode([256, 0, 1]), throwsA(isA<FormatException>()));
+  });
+  test('hexDecode', () {
+    expect(hexDecode(''), []);
+    expect(hexDecode('ff0001f0abcdef'), [255, 0, 1, 240, 171, 205, 239]);
+    expect(hexDecode('FF0001F0ABCDEF'), [255, 0, 1, 240, 171, 205, 239]);
+    expect(() => hexDecode('F'), throwsA(isA<FormatException>()));
+    expect(() => hexDecode('0p'), throwsA(isA<FormatException>()));
+    expect(() => hexDecode('p0'), throwsA(isA<FormatException>()));
+  });
 }
diff --git a/tool/test.dart b/tool/test.dart
index 57ab554..e0ef4b0 100755
--- a/tool/test.dart
+++ b/tool/test.dart
@@ -39,7 +39,7 @@
         packageConfigPath: path.join('.dart_tool', 'package_config.json'));
     testProcess = await Process.start(
       Platform.resolvedExecutable,
-      ['run', 'test', '--chain-stack-traces', ...args],
+      ['run', 'test', ...args],
       environment: {'_PUB_TEST_SNAPSHOT': pubSnapshotFilename},
       mode: ProcessStartMode.inheritStdio,
     );