diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/Fetcher.java b/sigstore-java/src/main/java/dev/sigstore/tuf/Fetcher.java new file mode 100644 index 00000000..6a020763 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Fetcher.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 The Sigstore Authors. + * + * 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 dev.sigstore.tuf; + +import java.io.IOException; + +public interface Fetcher { + + String getSource(); + + byte[] fetchResource(String filename, int maxLength) + throws IOException, FileExceedsMaxLengthException; +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java index ae507588..22b870a4 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java @@ -53,7 +53,7 @@ public static MutableTufStore newFileSystemStore(Path repoBaseDir) throws IOExce return newFileSystemStore(repoBaseDir, defaultTargetsCache); } - static MutableTufStore newFileSystemStore(Path repoBaseDir, Path targetsCache) { + public static MutableTufStore newFileSystemStore(Path repoBaseDir, Path targetsCache) { if (!Files.isDirectory(repoBaseDir)) { throw new IllegalArgumentException(repoBaseDir + " must be a file system directory."); } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/HttpMetaFetcher.java b/sigstore-java/src/main/java/dev/sigstore/tuf/HttpFetcher.java similarity index 50% rename from sigstore-java/src/main/java/dev/sigstore/tuf/HttpMetaFetcher.java rename to sigstore-java/src/main/java/dev/sigstore/tuf/HttpFetcher.java index 23cc30b7..8e767009 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/HttpMetaFetcher.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/HttpFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Sigstore Authors. + * Copyright 2024 The Sigstore Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,38 +15,28 @@ */ package dev.sigstore.tuf; -import static dev.sigstore.json.GsonSupplier.GSON; - import com.google.api.client.http.GenericUrl; import com.google.api.client.json.gson.GsonFactory; -import com.google.common.base.Preconditions; import dev.sigstore.http.HttpClients; import dev.sigstore.http.ImmutableHttpParams; -import dev.sigstore.tuf.model.Root; -import dev.sigstore.tuf.model.SignedTufMeta; -import dev.sigstore.tuf.model.TufMeta; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.util.Locale; -import java.util.Optional; -import javax.annotation.Nullable; -public class HttpMetaFetcher implements MetaFetcher { +public class HttpFetcher implements Fetcher { - private static final int MAX_META_BYTES = 99 * 1024; // 99 KB private final URL mirror; - HttpMetaFetcher(URL mirror) { + private HttpFetcher(URL mirror) { this.mirror = mirror; } - public static HttpMetaFetcher newFetcher(URL mirror) throws MalformedURLException { + public static HttpFetcher newFetcher(URL mirror) throws MalformedURLException { if (mirror.toString().endsWith("/")) { - return new HttpMetaFetcher(mirror); + return new HttpFetcher(mirror); } - return new HttpMetaFetcher(new URL(mirror.toExternalForm() + "/")); + return new HttpFetcher(new URL(mirror.toExternalForm() + "/")); } @Override @@ -54,46 +44,6 @@ public String getSource() { return mirror.toString(); } - @Override - public Optional> getRootAtVersion(int version) - throws IOException, FileExceedsMaxLengthException { - String versionFileName = version + ".root.json"; - return getMeta(versionFileName, Root.class, null); - } - - @Override - public > Optional> getMeta( - String role, Class t) throws IOException, FileExceedsMaxLengthException { - return getMeta(getFileName(role, null), t, null); - } - - @Override - public > Optional> getMeta( - String role, int version, Class t, Integer maxSize) - throws IOException, FileExceedsMaxLengthException { - Preconditions.checkArgument(version > 0, "version should be positive, got: %s", version); - return getMeta(getFileName(role, version), t, maxSize); - } - - private static String getFileName(String role, @Nullable Integer version) { - return version == null - ? role + ".json" - : String.format(Locale.ROOT, "%d.%s.json", version, role); - } - - Optional> getMeta( - String filename, Class t, Integer maxSize) - throws IOException, FileExceedsMaxLengthException { - byte[] roleBytes = fetchResource(filename, maxSize == null ? MAX_META_BYTES : maxSize); - if (roleBytes == null) { - return Optional.empty(); - } - var result = - new MetaFetchResult( - roleBytes, GSON.get().fromJson(new String(roleBytes, StandardCharsets.UTF_8), t)); - return Optional.of(result); - } - @Override public byte[] fetchResource(String filename, int maxLength) throws IOException, FileExceedsMaxLengthException { diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java index e66b2a1a..8b626774 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java @@ -15,59 +15,69 @@ */ package dev.sigstore.tuf; +import static dev.sigstore.json.GsonSupplier.GSON; + +import com.google.common.base.Preconditions; import dev.sigstore.tuf.model.Root; import dev.sigstore.tuf.model.SignedTufMeta; import dev.sigstore.tuf.model.TufMeta; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Locale; import java.util.Optional; +import javax.annotation.Nullable; -/** Retrieves TUF metadata. */ -public interface MetaFetcher { +public class MetaFetcher { - /** - * Describes the source of the metadata being fetched from. e.g "http://mirror.bla/mirror", - * "mock", "c:/tmp". - */ - String getSource(); + private static final int MAX_META_BYTES = 99 * 1024; // 99 KB + private final Fetcher fetcher; - /** - * Fetch the {@link Root} at the specified {@code version}. - * - * @throws FileExceedsMaxLengthException when the retrieved file is larger than the maximum - * allowed by the client - */ - Optional> getRootAtVersion(int version) - throws IOException, FileExceedsMaxLengthException; + private MetaFetcher(Fetcher fetcher) { + this.fetcher = fetcher; + } - /** - * Fetches the unversioned specified role meta from the source - * - * @param name TUF role name - * @param roleType this should be the type you expect in return - * @return the latest fully de-serialized role if it was present at the source - * @throws IOException in case of IO errors - * @throws FileExceedsMaxLengthException if the role meta at source exceeds client specified max - * size - */ - > Optional> getMeta( - String name, Class roleType) throws IOException, FileExceedsMaxLengthException; + public static MetaFetcher newFetcher(Fetcher fetcher) { + return new MetaFetcher(fetcher); + } - /** - * Fetches the specified role meta from the source - * - * @param name TUF role name - * @param version the version of the file to download - * @param roleType this should be the type you expect in return - * @param maxSize max file size in bytes - * @return the fully de-serialized role if it was present at the source - * @throws IOException in case of IO errors - * @throws FileExceedsMaxLengthException if the role meta at source exceeds client specified max - * size - */ - > Optional> getMeta( - String name, int version, Class roleType, Integer maxSize) - throws IOException, FileExceedsMaxLengthException; + public String getSource() { + return fetcher.getSource(); + } + + public Optional> getRootAtVersion(int version) + throws IOException, FileExceedsMaxLengthException { + String versionFileName = version + ".root.json"; + return getMeta(versionFileName, Root.class, null); + } - byte[] fetchResource(String filename, int maxLength) - throws IOException, FileExceedsMaxLengthException; + public > Optional> getMeta( + String role, Class t) throws IOException, FileExceedsMaxLengthException { + return getMeta(getFileName(role, null), t, null); + } + + public > Optional> getMeta( + String role, int version, Class t, Integer maxSize) + throws IOException, FileExceedsMaxLengthException { + Preconditions.checkArgument(version > 0, "version should be positive, got: %s", version); + return getMeta(getFileName(role, version), t, maxSize); + } + + private static String getFileName(String role, @Nullable Integer version) { + return version == null + ? role + ".json" + : String.format(Locale.ROOT, "%d.%s.json", version, role); + } + + > Optional> getMeta( + String filename, Class t, Integer maxSize) + throws IOException, FileExceedsMaxLengthException { + byte[] roleBytes = fetcher.fetchResource(filename, maxSize == null ? MAX_META_BYTES : maxSize); + if (roleBytes == null) { + return Optional.empty(); + } + var result = + new MetaFetchResult( + roleBytes, GSON.get().fromJson(new String(roleBytes, StandardCharsets.UTF_8), t)); + return Optional.of(result); + } } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java b/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java index f8ecccd9..503994cc 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java @@ -130,7 +130,8 @@ public SigstoreTufClient build() throws IOException { Updater.builder() .setTrustedRootPath(trustedRoot) .setLocalStore(FileSystemTufStore.newFileSystemStore(tufCacheLocation)) - .setFetcher(HttpMetaFetcher.newFetcher(remoteMirror)) + .setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(remoteMirror))) + .setTargetFetcher(HttpFetcher.newFetcher(remoteMirror)) .build(); return new SigstoreTufClient(tufUpdater, cacheValidity); } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java index 0808b539..8b4219e7 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java @@ -59,7 +59,8 @@ public class Updater { private Clock clock; private Verifiers.Supplier verifiers; - private MetaFetcher fetcher; + private MetaFetcher metaFetcher; + private Fetcher targetFetcher; private ZonedDateTime updateStartTime; private RootProvider trustedRootPath; private MutableTufStore localStore; @@ -67,14 +68,16 @@ public class Updater { Updater( Clock clock, Verifiers.Supplier verifiers, - MetaFetcher fetcher, + MetaFetcher metaFetcher, + Fetcher targetFetcher, RootProvider trustedRootPath, MutableTufStore localStore) { this.clock = clock; this.verifiers = verifiers; this.trustedRootPath = trustedRootPath; this.localStore = localStore; - this.fetcher = fetcher; + this.metaFetcher = metaFetcher; + this.targetFetcher = targetFetcher; } public static Builder builder() { @@ -121,7 +124,7 @@ Root updateRoot() // 5.3.3) download $version+1.root.json from mirror url (eventually obtained from remote.json // or map.json) up MAX_META_BYTES. If the file is not available, or we have reached // MAX_UPDATES number of root metadata files go to step 5.3.10 - var newRootMaybe = fetcher.getRootAtVersion(nextVersion); + var newRootMaybe = metaFetcher.getRootAtVersion(nextVersion); if (newRootMaybe.isEmpty()) { // No newer versions, go to 5.3.10. break; @@ -168,7 +171,7 @@ Root updateRoot() private void throwIfExpired(ZonedDateTime expires) { if (expires.isBefore(updateStartTime)) { - throw new RoleExpiredException(fetcher.getSource(), updateStartTime, expires); + throw new RoleExpiredException(metaFetcher.getSource(), updateStartTime, expires); } } @@ -267,9 +270,9 @@ Optional updateTimestamp(Root root) FileNotFoundException, SignatureVerificationException { // 1) download the timestamp.json bytes. var timestamp = - fetcher + metaFetcher .getMeta(RootRole.TIMESTAMP, Timestamp.class) - .orElseThrow(() -> new FileNotFoundException("timestamp.json", fetcher.getSource())) + .orElseThrow(() -> new FileNotFoundException("timestamp.json", metaFetcher.getSource())) .getMetaResource(); // 2) verify against threshold of keys as specified in trusted root.json @@ -303,14 +306,14 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp) // 1) download the snapshot.json bytes up to timestamp's snapshot length. int timestampSnapshotVersion = timestamp.getSignedMeta().getSnapshotMeta().getVersion(); var snapshotResult = - fetcher.getMeta( + metaFetcher.getMeta( RootRole.SNAPSHOT, timestampSnapshotVersion, Snapshot.class, timestamp.getSignedMeta().getSnapshotMeta().getLengthOrDefault()); if (snapshotResult.isEmpty()) { throw new FileNotFoundException( - timestampSnapshotVersion + ".snapshot.json", fetcher.getSource()); + timestampSnapshotVersion + ".snapshot.json", metaFetcher.getSource()); } // 2) check against timestamp.snapshot.hash, this is optional, the fallback is // that the version must match, which is handled in (4). @@ -393,14 +396,14 @@ Targets updateTargets(Root root, Snapshot snapshot) // 1) download the targets.json up to targets.json length in bytes. SnapshotMeta.SnapshotTarget targetMeta = snapshot.getSignedMeta().getTargetMeta("targets.json"); var targetsResultMaybe = - fetcher.getMeta( + metaFetcher.getMeta( RootRole.TARGETS, targetMeta.getVersion(), Targets.class, targetMeta.getLengthOrDefault()); if (targetsResultMaybe.isEmpty()) { throw new FileNotFoundException( - targetMeta.getVersion() + ".targets.json", fetcher.getSource()); + targetMeta.getVersion() + ".targets.json", metaFetcher.getSource()); } var targetsResult = targetsResultMaybe.get(); // 2) check hash against snapshot.targets.hash, else just make sure versions match, handled @@ -451,9 +454,9 @@ void downloadTargets(Targets targets) } var targetBytes = - fetcher.fetchResource("targets/" + versionedTargetName, targetData.getLength()); + targetFetcher.fetchResource("targets/" + versionedTargetName, targetData.getLength()); if (targetBytes == null) { - throw new FileNotFoundException(targetName, fetcher.getSource()); + throw new FileNotFoundException(targetName, targetFetcher.getSource()); } verifyHashes(entry.getKey(), targetBytes, targetData.getHashes()); @@ -472,7 +475,8 @@ public static class Builder { private Clock clock = Clock.systemUTC(); private Verifiers.Supplier verifiers = Verifiers::newVerifier; - private MetaFetcher fetcher; + private MetaFetcher metaFetcher; + private Fetcher targetFetcher; private RootProvider trustedRootPath; private MutableTufStore localStore; @@ -496,13 +500,18 @@ public Builder setTrustedRootPath(RootProvider trustedRootPath) { return this; } - public Builder setFetcher(MetaFetcher fetcher) { - this.fetcher = fetcher; + public Builder setMetaFetcher(MetaFetcher metaFetcher) { + this.metaFetcher = metaFetcher; + return this; + } + + public Builder setTargetFetcher(Fetcher fetcher) { + this.targetFetcher = fetcher; return this; } public Updater build() { - return new Updater(clock, verifiers, fetcher, trustedRootPath, localStore); + return new Updater(clock, verifiers, metaFetcher, targetFetcher, trustedRootPath, localStore); } } } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/HttpMetaFetcherTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/HttpFetcherTest.java similarity index 92% rename from sigstore-java/src/test/java/dev/sigstore/tuf/HttpMetaFetcherTest.java rename to sigstore-java/src/test/java/dev/sigstore/tuf/HttpFetcherTest.java index 7b740984..fc235f14 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/HttpMetaFetcherTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/HttpFetcherTest.java @@ -20,12 +20,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -class HttpMetaFetcherTest { +class HttpFetcherTest { @ParameterizedTest @CsvSource({"http://example.com", "http://example.com/"}) public void newFetcher_urlNoTrailingSlash(String url) throws Exception { - var fetcher = HttpMetaFetcher.newFetcher(new URL(url)); + var fetcher = HttpFetcher.newFetcher(new URL(url)); Assertions.assertEquals("http://example.com/", fetcher.getSource()); } } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java index 8b045e85..fb3ebb9d 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java @@ -1048,7 +1048,8 @@ private static Updater createTimeStaticUpdater(Path localStore, Path trustedRoot return Updater.builder() .setClock(Clock.fixed(Instant.parse(time), ZoneOffset.UTC)) .setVerifiers(Verifiers::newVerifier) - .setFetcher(HttpMetaFetcher.newFetcher(new URL(remoteUrl))) + .setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(new URL(remoteUrl)))) + .setTargetFetcher(HttpFetcher.newFetcher(new URL(remoteUrl))) .setTrustedRootPath(RootProvider.fromFile(trustedRootFile)) .setLocalStore(FileSystemTufStore.newFileSystemStore(localStore)) .build();