Add a Checksum class for plumbing HTTP downloader content checksums.

This is extracted from https://github.com/bazelbuild/bazel/pull/7208 ("Support subresource integrity format for `download()` checksums.")

I'm trying to revive that PR and rebase to master, but the replacement of `String sha256` through the HTTP downloader code is causing a lot of Git conflicts. I expect that portion of the change to require less discussion than the proposed content integrity changes, so I'm moving it to a separate PR to simplify review.

@dslomov or @aehlig, could one of you take a look? Once this is merged, I will rebase PR 7208 onto it.

Closes #8714.

PiperOrigin-RevId: 256188026
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java
new file mode 100644
index 0000000..07ca16d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java
@@ -0,0 +1,50 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.bazel.repository.downloader;
+
+import com.google.common.hash.HashCode;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
+
+/** The content checksum for an HTTP download, which knows its own type. */
+public class Checksum {
+  private final KeyType keyType;
+  private final HashCode hashCode;
+
+  private Checksum(KeyType keyType, HashCode hashCode) {
+    this.keyType = keyType;
+    this.hashCode = hashCode;
+  }
+
+  /** Constructs a new Checksum for a given key type and hash, in hex format. */
+  public static Checksum fromString(KeyType keyType, String hash) {
+    if (!keyType.isValid(hash)) {
+      throw new IllegalArgumentException("Invalid " + keyType + " checksum '" + hash + "'");
+    }
+    return new Checksum(keyType, HashCode.fromString(hash));
+  }
+
+  @Override
+  public String toString() {
+    return hashCode.toString();
+  }
+
+  public HashCode getHashCode() {
+    return hashCode;
+  }
+
+  public KeyType getKeyType() {
+    return keyType;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStream.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStream.java
index ee7b1bd..51695a5 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStream.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStream.java
@@ -15,7 +15,6 @@
 package com.google.devtools.build.lib.bazel.repository.downloader;
 
 import com.google.common.hash.HashCode;
-import com.google.common.hash.HashFunction;
 import com.google.common.hash.Hasher;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
 import java.io.IOException;
@@ -41,11 +40,10 @@
   private final HashCode code;
   @Nullable private volatile HashCode actual;
 
-  HashInputStream(
-      @WillCloseWhenClosed InputStream delegate, HashFunction function, HashCode code) {
+  HashInputStream(@WillCloseWhenClosed InputStream delegate, Checksum checksum) {
     this.delegate = delegate;
-    this.hasher = function.newHasher();
-    this.code = code;
+    this.hasher = checksum.getKeyType().newHasher();
+    this.code = checksum.getHashCode();
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java
index 36328ae..b5bcabf 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
 
-import com.google.common.base.Preconditions;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Ordering;
@@ -95,8 +95,8 @@
     this.sleeper = sleeper;
   }
 
-  public HttpStream connect(List<URL> urls, String sha256) throws IOException {
-    return connect(urls, sha256, ImmutableMap.<URI, Map<String, String>>of());
+  public HttpStream connect(List<URL> urls, Optional<Checksum> checksum) throws IOException {
+    return connect(urls, checksum, ImmutableMap.<URI, Map<String, String>>of());
   }
 
   /**
@@ -116,22 +116,22 @@
    * and block until the connection can be renegotiated transparently right where it left off.
    *
    * @param urls mirrors by preference; each URL can be: file, http, or https
-   * @param sha256 hex checksum lazily checked on entire payload, or empty to disable
+   * @param checksum checksum lazily checked on entire payload, or empty to disable
    * @return an {@link InputStream} of response payload
    * @throws IOException if all mirrors are down and contains suppressed exception of each attempt
    * @throws InterruptedIOException if current thread is being cast into oblivion
    * @throws IllegalArgumentException if {@code urls} is empty or has an unsupported protocol
    */
   public HttpStream connect(
-      List<URL> urls, String sha256, Map<URI, Map<String, String>> authHeaders) throws IOException {
-    Preconditions.checkNotNull(sha256);
+      List<URL> urls, Optional<Checksum> checksum, Map<URI, Map<String, String>> authHeaders)
+      throws IOException {
     HttpUtils.checkUrlsArgument(urls);
     if (Thread.interrupted()) {
       throw new InterruptedIOException();
     }
     // If there's only one URL then there's no need for us to run all our fancy thread stuff.
     if (urls.size() == 1) {
-      return establishConnection(urls.get(0), sha256, authHeaders);
+      return establishConnection(urls.get(0), checksum, authHeaders);
     }
     MutexConditionSharedMemory context = new MutexConditionSharedMemory();
     // The parent thread always holds the lock except when released by wait().
@@ -140,7 +140,7 @@
       long now = clock.currentTimeMillis();
       long startAtTime = now;
       for (URL url : urls) {
-        context.jobs.add(new WorkItem(url, sha256, startAtTime, authHeaders));
+        context.jobs.add(new WorkItem(url, checksum, startAtTime, authHeaders));
         startAtTime += FAILOVER_DELAY_MS;
       }
       // Create the worker thread pool.
@@ -210,13 +210,17 @@
 
   private static class WorkItem {
     final URL url;
-    final String sha256;
+    final Optional<Checksum> checksum;
     final long startAtTime;
     final Map<URI, Map<String, String>> authHeaders;
 
-    WorkItem(URL url, String sha256, long startAtTime, Map<URI, Map<String, String>> authHeaders) {
+    WorkItem(
+        URL url,
+        Optional<Checksum> checksum,
+        long startAtTime,
+        Map<URI, Map<String, String>> authHeaders) {
       this.url = url;
-      this.sha256 = sha256;
+      this.checksum = checksum;
       this.startAtTime = startAtTime;
       this.authHeaders = authHeaders;
     }
@@ -263,7 +267,7 @@
         // Now we're actually going to attempt to connect to the remote server.
         HttpStream result;
         try {
-          result = establishConnection(work.url, work.sha256, work.authHeaders);
+          result = establishConnection(work.url, work.checksum, work.authHeaders);
         } catch (InterruptedIOException e) {
           // The parent thread got its result from another thread and killed this one.
           synchronized (context) {
@@ -307,7 +311,7 @@
   }
 
   private HttpStream establishConnection(
-      final URL url, String sha256, Map<URI, Map<String, String>> additionalHeaders)
+      final URL url, Optional<Checksum> checksum, Map<URI, Map<String, String>> additionalHeaders)
       throws IOException {
     ImmutableMap<String, String> headers = REQUEST_HEADERS;
     try {
@@ -327,7 +331,7 @@
     return httpStreamFactory.create(
         connection,
         url,
-        sha256,
+        checksum,
         new Reconnector() {
           @Override
           public URLConnection connect(Throwable cause, ImmutableMap<String, String> extraHeaders)
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java
index d9fad055..5efe7b5 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java
@@ -72,12 +72,12 @@
   /**
    * Downloads file to disk and returns path.
    *
-   * <p>If the SHA256 checksum and path to the repository cache is specified, attempt to load the
-   * file from the {@link RepositoryCache}. If it doesn't exist, proceed to download the file and
-   * load it into the cache prior to returning the value.
+   * <p>If the checksum and path to the repository cache is specified, attempt to load the file from
+   * the {@link RepositoryCache}. If it doesn't exist, proceed to download the file and load it into
+   * the cache prior to returning the value.
    *
    * @param urls list of mirror URLs with identical content
-   * @param sha256 valid SHA256 hex checksum string which is checked, or empty to disable
+   * @param checksum valid checksum which is checked, or empty to disable
    * @param type extension, e.g. "tar.gz" to force on downloaded filename, or empty to not do this
    * @param output destination filename if {@code type} is <i>absent</i>, otherwise output directory
    * @param eventHandler CLI progress reporter
@@ -91,7 +91,7 @@
   public Path download(
       List<URL> urls,
       Map<URI, Map<String, String>> authHeaders,
-      String sha256,
+      Optional<Checksum> checksum,
       String canonicalId,
       Optional<String> type,
       Path output,
@@ -116,14 +116,15 @@
     }
     Path destination = getDownloadDestination(mainUrl, type, output);
 
-    // Is set to true if the value should be cached by the sha256 value provided
-    boolean isCachingByProvidedSha256 = false;
+    // Is set to true if the value should be cached by the checksum value provided
+    boolean isCachingByProvidedChecksum = false;
 
-    if (!sha256.isEmpty()) {
+    if (checksum.isPresent()) {
+      String cacheKey = checksum.get().toString();
+      KeyType cacheKeyType = checksum.get().getKeyType();
       try {
-        String currentSha256 =
-            RepositoryCache.getChecksum(KeyType.SHA256, destination);
-        if (currentSha256.equals(sha256)) {
+        String currentChecksum = RepositoryCache.getChecksum(cacheKeyType, destination);
+        if (currentChecksum.equals(cacheKey)) {
           // No need to download.
           return destination;
         }
@@ -132,14 +133,14 @@
       }
 
       if (repositoryCache.isEnabled()) {
-        isCachingByProvidedSha256 = true;
+        isCachingByProvidedChecksum = true;
 
         try {
           Path cachedDestination =
-              repositoryCache.get(sha256, destination, KeyType.SHA256, canonicalId);
+              repositoryCache.get(cacheKey, destination, cacheKeyType, canonicalId);
           if (cachedDestination != null) {
             // Cache hit!
-            eventHandler.post(new RepositoryCacheHitEvent(repo, sha256, mainUrl));
+            eventHandler.post(new RepositoryCacheHitEvent(repo, cacheKey, mainUrl));
             return cachedDestination;
           }
         } catch (IOException e) {
@@ -163,16 +164,16 @@
           boolean match = false;
           Path candidate = dir.getRelative(destination.getBaseName());
           try {
-            match = RepositoryCache.getChecksum(KeyType.SHA256, candidate).equals(sha256);
+            match = RepositoryCache.getChecksum(cacheKeyType, candidate).equals(cacheKey);
           } catch (IOException e) {
             // Not finding anything in a distdir is a normal case, so handle it absolutely
             // quietly. In fact, it is not uncommon to specify a whole list of dist dirs,
             // with the asumption that only one will contain an entry.
           }
           if (match) {
-            if (isCachingByProvidedSha256) {
+            if (isCachingByProvidedChecksum) {
               try {
-                repositoryCache.put(sha256, candidate, KeyType.SHA256, canonicalId);
+                repositoryCache.put(cacheKey, candidate, cacheKeyType, canonicalId);
               } catch (IOException e) {
                 eventHandler.handle(
                     Event.warn("Failed to copy " + candidate + " to repository cache: " + e));
@@ -201,7 +202,7 @@
     // Connect to the best mirror and download the file, while reporting progress to the CLI.
     semaphore.acquire();
     boolean success = false;
-    try (HttpStream payload = multiplexer.connect(urls, sha256, authHeaders);
+    try (HttpStream payload = multiplexer.connect(urls, checksum, authHeaders);
         OutputStream out = destination.getOutputStream()) {
       ByteStreams.copy(payload, out);
       success = true;
@@ -215,8 +216,9 @@
       eventHandler.post(new FetchEvent(urls.get(0).toString(), success));
     }
 
-    if (isCachingByProvidedSha256) {
-      repositoryCache.put(sha256, destination, KeyType.SHA256, canonicalId);
+    if (isCachingByProvidedChecksum) {
+      repositoryCache.put(
+          checksum.get().toString(), destination, checksum.get().getKeyType(), canonicalId);
     } else if (repositoryCache.isEnabled()) {
       String newSha256 = repositoryCache.put(destination, KeyType.SHA256, canonicalId);
       eventHandler.handle(Event.info("SHA256 (" + urls.get(0) + ") = " + newSha256));
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStream.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStream.java
index 4921b15..718ed5b 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStream.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStream.java
@@ -14,12 +14,11 @@
 
 package com.google.devtools.build.lib.bazel.repository.downloader;
 
+import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-import com.google.common.hash.HashCode;
-import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
 import com.google.devtools.build.lib.bazel.repository.downloader.RetryingInputStream.Reconnector;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
@@ -62,9 +61,9 @@
     HttpStream create(
         @WillCloseWhenClosed URLConnection connection,
         URL originalUrl,
-        String sha256,
+        Optional<Checksum> checksum,
         Reconnector reconnector)
-            throws IOException {
+        throws IOException {
       InputStream stream = new InterruptibleInputStream(connection.getInputStream());
       try {
         // If server supports range requests, we can retry on read errors. See RFC7233 ยง 2.3.
@@ -89,8 +88,8 @@
           stream = new GZIPInputStream(stream, GZIP_BUFFER_BYTES);
         }
 
-        if (!sha256.isEmpty()) {
-          stream = new HashInputStream(stream, Hashing.sha256(), HashCode.fromString(sha256));
+        if (checksum.isPresent()) {
+          stream = new HashInputStream(stream, checksum.get());
           if (retrier != null) {
             retrier.disabled = true;
           }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
index 25b521a..2e10b6a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.bazel.repository.DecompressorValue;
 import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
 import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
 import com.google.devtools.build.lib.bazel.repository.downloader.HttpUtils;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -474,6 +475,12 @@
       warnAboutSha256Error(urls, sha256);
       sha256 = "";
     }
+    Optional<Checksum> checksum;
+    if (sha256.isEmpty()) {
+      checksum = Optional.absent();
+    } else {
+      checksum = Optional.of(Checksum.fromString(KeyType.SHA256, sha256));
+    }
     SkylarkPath outputPath = getPath("download()", output);
     WorkspaceRuleEvent w =
         WorkspaceRuleEvent.newDownloadEvent(
@@ -487,7 +494,7 @@
           httpDownloader.download(
               urls,
               authHeaders,
-              sha256,
+              checksum,
               canonicalId,
               Optional.<String>absent(),
               outputPath.getPath(),
@@ -579,6 +586,12 @@
       warnAboutSha256Error(urls, sha256);
       sha256 = "";
     }
+    Optional<Checksum> checksum;
+    if (sha256.isEmpty()) {
+      checksum = Optional.absent();
+    } else {
+      checksum = Optional.of(Checksum.fromString(KeyType.SHA256, sha256));
+    }
 
     WorkspaceRuleEvent w =
         WorkspaceRuleEvent.newDownloadAndExtractEvent(
@@ -601,7 +614,7 @@
           httpDownloader.download(
               urls,
               authHeaders,
-              sha256,
+              checksum,
               canonicalId,
               Optional.of(type),
               outputPath.getPath(),
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/BUILD
index cdd0374..e5873d4 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/BUILD
@@ -19,6 +19,7 @@
     deps = [
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:util",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/cache",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/test/java/com/google/devtools/build/lib:foundations_testutil",
         "//src/test/java/com/google/devtools/build/lib:test_runner",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStreamTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStreamTest.java
index 3c97cf3..9c5d5ae 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStreamTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HashInputStreamTest.java
@@ -17,9 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.hash.HashCode;
-import com.google.common.hash.Hashing;
 import com.google.common.io.CharStreams;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -43,8 +42,7 @@
         new InputStreamReader(
             new HashInputStream(
                 new ByteArrayInputStream("hello".getBytes(UTF_8)),
-                Hashing.sha1(),
-                HashCode.fromString("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")),
+                Checksum.fromString(KeyType.SHA1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")),
             UTF_8)) {
       assertThat(CharStreams.toString(reader)).isEqualTo("hello");
     }
@@ -58,8 +56,7 @@
         new InputStreamReader(
             new HashInputStream(
                 new ByteArrayInputStream("hello".getBytes(UTF_8)),
-                Hashing.sha1(),
-                HashCode.fromString("0000000000000000000000000000000000000000")),
+                Checksum.fromString(KeyType.SHA1, "0000000000000000000000000000000000000000")),
             UTF_8)) {
       assertThat(CharStreams.toString(reader))
           .isNull(); // Only here to make @CheckReturnValue happy.
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerIntegrationTest.java
index 4490bb0..f5dccf87 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerIntegrationTest.java
@@ -26,7 +26,9 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
 import com.google.devtools.build.lib.testutil.ManualClock;
 import com.google.devtools.build.lib.util.Sleeper;
@@ -79,6 +81,11 @@
   private final HttpConnectorMultiplexer multiplexer =
       new HttpConnectorMultiplexer(eventHandler, connector, httpStreamFactory, clock, sleeper);
 
+  private static final Optional<Checksum> HELLO_SHA256 =
+      Optional.of(
+          Checksum.fromString(
+              KeyType.SHA256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"));
+
   @Before
   public void before() throws Exception {
     when(proxyHelper.createProxyIfNeeded(any(URL.class))).thenReturn(Proxy.NO_PROXY);
@@ -121,11 +128,11 @@
       phaser.arriveAndAwaitAdvance();
       phaser.arriveAndDeregister();
       try (HttpStream stream =
-              multiplexer.connect(
-                  ImmutableList.of(
-                      new URL(String.format("http://localhost:%d", server1.getLocalPort())),
-                      new URL(String.format("http://localhost:%d", server2.getLocalPort()))),
-                  "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")) {
+          multiplexer.connect(
+              ImmutableList.of(
+                  new URL(String.format("http://localhost:%d", server1.getLocalPort())),
+                  new URL(String.format("http://localhost:%d", server2.getLocalPort()))),
+              HELLO_SHA256)) {
         assertThat(toByteArray(stream)).isEqualTo("hello".getBytes(US_ASCII));
       }
     }
@@ -186,11 +193,11 @@
                 }
               });
       try (HttpStream stream =
-              multiplexer.connect(
-                  ImmutableList.of(
-                      new URL(String.format("http://localhost:%d", server1.getLocalPort())),
-                      new URL(String.format("http://localhost:%d", server2.getLocalPort()))),
-                  "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")) {
+          multiplexer.connect(
+              ImmutableList.of(
+                  new URL(String.format("http://localhost:%d", server1.getLocalPort())),
+                  new URL(String.format("http://localhost:%d", server2.getLocalPort()))),
+              HELLO_SHA256)) {
         assertThat(toByteArray(stream)).isEqualTo("hello".getBytes(US_ASCII));
       }
     }
@@ -231,7 +238,7 @@
               new URL(String.format("http://localhost:%d", server1.getLocalPort())),
               new URL(String.format("http://localhost:%d", server2.getLocalPort())),
               new URL(String.format("http://localhost:%d", server3.getLocalPort()))),
-          "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9825");
+          HELLO_SHA256);
     }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerTest.java
index a93e50f..511d08c 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexerTest.java
@@ -22,7 +22,6 @@
 import static java.util.Arrays.asList;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.same;
 import static org.mockito.Mockito.doAnswer;
@@ -33,8 +32,10 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
 import com.google.devtools.build.lib.bazel.repository.downloader.RetryingInputStream.Reconnector;
 import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.testutil.ManualClock;
@@ -69,6 +70,11 @@
   private static final byte[] data2 = "second".getBytes(UTF_8);
   private static final byte[] data3 = "third".getBytes(UTF_8);
 
+  private static final Optional<Checksum> DUMMY_CHECKSUM =
+      Optional.of(
+          Checksum.fromString(
+              KeyType.SHA256, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"));
+
   @Rule
   public final ExpectedException thrown = ExpectedException.none();
 
@@ -94,47 +100,48 @@
     when(connector.connect(eq(URL1), any(ImmutableMap.class))).thenReturn(connection1);
     when(connector.connect(eq(URL2), any(ImmutableMap.class))).thenReturn(connection2);
     when(connector.connect(eq(URL3), any(ImmutableMap.class))).thenReturn(connection3);
-    when(streamFactory
-            .create(same(connection1), any(URL.class), anyString(), any(Reconnector.class)))
+    when(streamFactory.create(
+            same(connection1), any(URL.class), any(Optional.class), any(Reconnector.class)))
         .thenReturn(stream1);
-    when(streamFactory
-            .create(same(connection2), any(URL.class), anyString(), any(Reconnector.class)))
+    when(streamFactory.create(
+            same(connection2), any(URL.class), any(Optional.class), any(Reconnector.class)))
         .thenReturn(stream2);
-    when(streamFactory
-            .create(same(connection3), any(URL.class), anyString(), any(Reconnector.class)))
+    when(streamFactory.create(
+            same(connection3), any(URL.class), any(Optional.class), any(Reconnector.class)))
         .thenReturn(stream3);
   }
 
   @Test
   public void emptyList_throwsIae() throws Exception {
     thrown.expect(IllegalArgumentException.class);
-    multiplexer.connect(ImmutableList.<URL>of(), "");
+    multiplexer.connect(ImmutableList.<URL>of(), null);
   }
 
   @Test
   public void ftpUrl_throwsIae() throws Exception {
     thrown.expect(IllegalArgumentException.class);
-    multiplexer.connect(asList(new URL("ftp://lol.example")), "");
+    multiplexer.connect(asList(new URL("ftp://lol.example")), null);
   }
 
   @Test
   public void threadIsInterrupted_throwsIeProntoAndDoesNothingElse() throws Exception {
     final AtomicBoolean wasInterrupted = new AtomicBoolean(true);
-    Thread task = new Thread(
-        new Runnable() {
-          @Override
-          public void run() {
-            Thread.currentThread().interrupt();
-            try {
-              multiplexer.connect(asList(new URL("http://lol.example")), "");
-            } catch (InterruptedIOException ignored) {
-              return;
-            } catch (Exception ignored) {
-              // ignored
-            }
-            wasInterrupted.set(false);
-          }
-        });
+    Thread task =
+        new Thread(
+            new Runnable() {
+              @Override
+              public void run() {
+                Thread.currentThread().interrupt();
+                try {
+                  multiplexer.connect(asList(new URL("http://lol.example")), null);
+                } catch (InterruptedIOException ignored) {
+                  return;
+                } catch (Exception ignored) {
+                  // ignored
+                }
+                wasInterrupted.set(false);
+              }
+            });
     task.start();
     task.join();
     assertThat(wasInterrupted.get()).isTrue();
@@ -143,10 +150,11 @@
 
   @Test
   public void singleUrl_justCallsConnector() throws Exception {
-    assertThat(toByteArray(multiplexer.connect(asList(URL1), "abc"))).isEqualTo(data1);
+    assertThat(toByteArray(multiplexer.connect(asList(URL1), DUMMY_CHECKSUM))).isEqualTo(data1);
     verify(connector).connect(eq(URL1), any(ImmutableMap.class));
     verify(streamFactory)
-        .create(any(URLConnection.class), any(URL.class), eq("abc"), any(Reconnector.class));
+        .create(
+            any(URLConnection.class), any(URL.class), eq(DUMMY_CHECKSUM), any(Reconnector.class));
     verifyNoMoreInteractions(sleeper, connector, streamFactory);
   }
 
@@ -154,7 +162,7 @@
   public void multipleUrlsFail_throwsIOException() throws Exception {
     when(connector.connect(any(URL.class), any(ImmutableMap.class))).thenThrow(new IOException());
     IOException e =
-        assertThrows(IOException.class, () -> multiplexer.connect(asList(URL1, URL2, URL3), ""));
+        assertThrows(IOException.class, () -> multiplexer.connect(asList(URL1, URL2, URL3), null));
     assertThat(e).hasMessageThat().contains("All mirrors are down");
     verify(connector, times(3)).connect(any(URL.class), any(ImmutableMap.class));
     verify(sleeper, times(2)).sleepMillis(anyLong());
@@ -172,12 +180,14 @@
           }
         }).when(sleeper).sleepMillis(anyLong());
     when(connector.connect(eq(URL1), any(ImmutableMap.class))).thenThrow(new IOException());
-    assertThat(toByteArray(multiplexer.connect(asList(URL1, URL2), "abc"))).isEqualTo(data2);
+    assertThat(toByteArray(multiplexer.connect(asList(URL1, URL2), DUMMY_CHECKSUM)))
+        .isEqualTo(data2);
     assertThat(clock.currentTimeMillis()).isEqualTo(1000L);
     verify(connector).connect(eq(URL1), any(ImmutableMap.class));
     verify(connector).connect(eq(URL2), any(ImmutableMap.class));
     verify(streamFactory)
-        .create(any(URLConnection.class), any(URL.class), eq("abc"), any(Reconnector.class));
+        .create(
+            any(URLConnection.class), any(URL.class), eq(DUMMY_CHECKSUM), any(Reconnector.class));
     verify(sleeper).sleepMillis(anyLong());
     verifyNoMoreInteractions(sleeper, connector, streamFactory);
   }
@@ -204,7 +214,8 @@
             return null;
           }
         }).when(sleeper).sleepMillis(anyLong());
-    assertThat(toByteArray(multiplexer.connect(asList(URL1, URL2), "abc"))).isEqualTo(data1);
+    assertThat(toByteArray(multiplexer.connect(asList(URL1, URL2), DUMMY_CHECKSUM)))
+        .isEqualTo(data1);
     assertThat(wasInterrupted.get()).isTrue();
   }
 
@@ -234,20 +245,21 @@
             throw new RuntimeException();
           }
         });
-    Thread task = new Thread(
-        new Runnable() {
-          @Override
-          public void run() {
-            try {
-              multiplexer.connect(asList(URL1, URL2), "");
-            } catch (InterruptedIOException ignored) {
-              return;
-            } catch (Exception ignored) {
-              // ignored
-            }
-            wasInterrupted3.set(false);
-          }
-        });
+    Thread task =
+        new Thread(
+            new Runnable() {
+              @Override
+              public void run() {
+                try {
+                  multiplexer.connect(asList(URL1, URL2), null);
+                } catch (InterruptedIOException ignored) {
+                  return;
+                } catch (Exception ignored) {
+                  // ignored
+                }
+                wasInterrupted3.set(false);
+              }
+            });
     task.start();
     barrier.await();
     task.interrupt();
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStreamTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStreamTest.java
index 6af4caf..011ba7a 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStreamTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpStreamTest.java
@@ -23,8 +23,10 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import com.google.common.base.Optional;
 import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
 import com.google.devtools.build.lib.bazel.repository.downloader.RetryingInputStream.Reconnector;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -54,10 +56,14 @@
 
   private static final Random randoCalrissian = new Random();
   private static final byte[] data = "hello".getBytes(UTF_8);
-  private static final String GOOD_CHECKSUM =
-      "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
-  private static final String BAD_CHECKSUM =
-      "0000000000000000000000000000000000000000000000000000000000000000";
+  private static final Optional<Checksum> GOOD_CHECKSUM =
+      Optional.of(
+          Checksum.fromString(
+              KeyType.SHA256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"));
+  private static final Optional<Checksum> BAD_CHECKSUM =
+      Optional.of(
+          Checksum.fromString(
+              KeyType.SHA256, "0000000000000000000000000000000000000000000000000000000000000000"));
   private static final URL AURL = makeUrl("http://doodle.example");
 
   @Rule
@@ -86,7 +92,8 @@
 
   @Test
   public void noChecksum_readsOk() throws Exception {
-    try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
+    try (HttpStream stream =
+        streamFactory.create(connection, AURL, Optional.absent(), reconnector)) {
       assertThat(toByteArray(stream)).isEqualTo(data);
     }
   }
@@ -112,8 +119,13 @@
     randoCalrissian.nextBytes(bigData);
     when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(bigData));
     try (HttpStream stream =
-            streamFactory.create(
-                connection, AURL, Hashing.sha256().hashBytes(bigData).toString(), reconnector)) {
+        streamFactory.create(
+            connection,
+            AURL,
+            Optional.of(
+                Checksum.fromString(
+                    KeyType.SHA256, Hashing.sha256().hashBytes(bigData).toString())),
+            reconnector)) {
       assertThat(toByteArray(stream)).isEqualTo(bigData);
     }
   }
@@ -137,7 +149,7 @@
     when(connection.getURL()).thenReturn(AURL);
     when(connection.getContentEncoding()).thenReturn("gzip");
     thrown.expect(ZipException.class);
-    streamFactory.create(connection, AURL, "", reconnector);
+    streamFactory.create(connection, AURL, Optional.absent(), reconnector);
   }
 
   @Test
@@ -145,7 +157,8 @@
     when(connection.getURL()).thenReturn(AURL);
     when(connection.getContentEncoding()).thenReturn("x-gzip");
     when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(gzipData(data)));
-    try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
+    try (HttpStream stream =
+        streamFactory.create(connection, AURL, Optional.absent(), reconnector)) {
       assertThat(toByteArray(stream)).isEqualTo(data);
     }
   }
@@ -156,7 +169,8 @@
     when(connection.getURL()).thenReturn(new URL("http://doodle.example/foo.tar.gz"));
     when(connection.getContentEncoding()).thenReturn("gzip");
     when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(gzData));
-    try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
+    try (HttpStream stream =
+        streamFactory.create(connection, AURL, Optional.absent(), reconnector)) {
       assertThat(toByteArray(stream)).isEqualTo(gzData);
     }
   }
@@ -164,22 +178,24 @@
   @Test
   public void threadInterrupted_haltsReadingAndThrowsInterrupt() throws Exception {
     final AtomicBoolean wasInterrupted = new AtomicBoolean();
-    Thread thread = new Thread(
-        new Runnable() {
-          @Override
-          public void run() {
-            try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
-              stream.read();
-              Thread.currentThread().interrupt();
-              stream.read();
-              fail();
-            } catch (InterruptedIOException expected) {
-              wasInterrupted.set(true);
-            } catch (IOException ignored) {
-              // ignored
-            }
-          }
-        });
+    Thread thread =
+        new Thread(
+            new Runnable() {
+              @Override
+              public void run() {
+                try (HttpStream stream =
+                    streamFactory.create(connection, AURL, Optional.absent(), reconnector)) {
+                  stream.read();
+                  Thread.currentThread().interrupt();
+                  stream.read();
+                  fail();
+                } catch (InterruptedIOException expected) {
+                  wasInterrupted.set(true);
+                } catch (IOException ignored) {
+                  // ignored
+                }
+              }
+            });
     thread.start();
     thread.join();
     assertThat(wasInterrupted.get()).isTrue();