diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/HttpMetaFetcher.java b/sigstore-java/src/main/java/dev/sigstore/tuf/HttpMetaFetcher.java new file mode 100644 index 00000000..d82f9ca2 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/HttpMetaFetcher.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 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 static dev.sigstore.json.GsonSupplier.GSON; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.json.gson.GsonFactory; +import dev.sigstore.http.HttpClients; +import dev.sigstore.http.ImmutableHttpParams; +import dev.sigstore.tuf.model.Root; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +public class HttpMetaFetcher implements MetaFetcher { + + private static final int MAX_META_BYTES = 99 * 1024; // 99 KB + private URL mirror; + + HttpMetaFetcher(URL mirror) { + this.mirror = mirror; + } + + public static HttpMetaFetcher newFetcher(URL mirror) { + return new HttpMetaFetcher(mirror); + } + + @Override + public String getSource() { + return mirror.toString(); + } + + @Override + public Optional getRootAtVersion(int version) + throws IOException, MetaFileExceedsMaxException { + String versionFileName = version + ".root.json"; + GenericUrl nextVersionUrl = new GenericUrl(mirror + "/" + versionFileName); + var req = + HttpClients.newHttpTransport(ImmutableHttpParams.builder().build()) + .createRequestFactory( + request -> { + request.setParser(GsonFactory.getDefaultInstance().createJsonObjectParser()); + }) + .buildGetRequest(nextVersionUrl); + req.getHeaders().setAccept("application/json; api-version=2.0"); + req.getHeaders().setContentType("application/json"); + req.setThrowExceptionOnExecuteError(false); + var resp = req.execute(); + if (resp.getStatusCode() == 404) { + return Optional.empty(); + } + if (resp.getStatusCode() != 200) { + throw new TufException( + String.format( + "Unexpected return from mirror. Status code: %s, status message: %s" + + resp.getStatusCode() + + resp.getStatusMessage())); + } + byte[] rootBytes = resp.getContent().readNBytes(MAX_META_BYTES); + if (rootBytes.length == MAX_META_BYTES && resp.getContent().read() != -1) { + throw new MetaFileExceedsMaxException(nextVersionUrl.toString(), MAX_META_BYTES); + } + return Optional.of( + GSON.get().fromJson(new String(rootBytes, StandardCharsets.UTF_8), Root.class)); + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java new file mode 100644 index 00000000..8f1b3cee --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 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 dev.sigstore.tuf.model.Root; +import java.io.IOException; +import java.util.Optional; + +/** Retrieves TUF metadata. */ +public interface MetaFetcher { + + /** + * Describes the source of the metadata being fetched from. e.g "http://mirror.bla/mirror", + * "mock", "c:/tmp". + */ + String getSource(); + + /** + * Fetch the {@link Root} at the specified {@code version}. + * + * @throws MetaFileExceedsMaxException when the retrieved file is larger than the maximum allowed + * by the client + */ + Optional getRootAtVersion(int version) throws IOException, MetaFileExceedsMaxException; +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TufClient.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TufClient.java index 633618c0..c204347c 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TufClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TufClient.java @@ -17,17 +17,11 @@ 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.annotations.VisibleForTesting; import dev.sigstore.encryption.Keys; import dev.sigstore.encryption.signers.Verifiers; -import dev.sigstore.http.HttpClients; -import dev.sigstore.http.ImmutableHttpParams; import dev.sigstore.tuf.model.*; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.InvalidKeyException; @@ -52,31 +46,37 @@ */ public class TufClient { - private static final int MAX_META_BYTES = 99 * 1024; // 99 KB private static final int MAX_UPDATES = 1024; // Limit the update loop to retrieve a max of 1024 subsequent versions as expressed in // 5.3.3 of spec. private Clock clock; private Verifiers.Supplier verifiers; - - TufClient(Clock clock, Verifiers.Supplier verifiers) { + private MetaFetcher fetcher; + private ZonedDateTime updateStartTime; + private Path trustedRootPath; + private TufLocalStore localStore; + + TufClient( + Clock clock, + Verifiers.Supplier verifiers, + MetaFetcher fetcher, + Path trustedRootPath, + TufLocalStore localStore) { this.clock = clock; this.verifiers = verifiers; + this.fetcher = fetcher; + this.trustedRootPath = trustedRootPath; + this.localStore = localStore; + this.fetcher = fetcher; } - TufClient(Verifiers.Supplier verifiers) { - this(Clock.systemUTC(), verifiers); - } - - TufClient() { - this(Clock.systemUTC(), Verifiers::newVerifier); + public static Builder builder() { + return new Builder(); } - private ZonedDateTime updateStartTime; - // https://theupdateframework.github.io/specification/latest/#detailed-client-workflow - public void updateRoot(Path trustedRootPath, URL mirror, TufLocalStore localStore) + public void updateRoot() throws IOException, RootExpiredException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, MetaFileExceedsMaxException, RoleVersionException, SignatureVerificationException { @@ -98,36 +98,12 @@ public void updateRoot(Path trustedRootPath, URL mirror, TufLocalStore localStor // 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 - String nextVersionFileName = nextVersion + ".root.json"; - GenericUrl nextVersionUrl = new GenericUrl(mirror + "/" + nextVersionFileName); - var req = - HttpClients.newHttpTransport(ImmutableHttpParams.builder().build()) - .createRequestFactory( - request -> { - request.setParser(GsonFactory.getDefaultInstance().createJsonObjectParser()); - }) - .buildGetRequest(nextVersionUrl); - req.getHeaders().setAccept("application/json; api-version=2.0"); - req.getHeaders().setContentType("application/json"); - req.setThrowExceptionOnExecuteError(false); - var resp = req.execute(); - if (resp.getStatusCode() == 404) { + var newRootMaybe = fetcher.getRootAtVersion(nextVersion); + if (newRootMaybe.isEmpty()) { // No newer versions, go to 5.3.10. break; } - if (resp.getStatusCode() != 200) { - throw new TufException( - String.format( - "Unexpected return from mirror. Status code: %s, status message: %s" - + resp.getStatusCode() - + resp.getStatusMessage())); - } - byte[] rootBytes = resp.getContent().readNBytes(MAX_META_BYTES); - if (rootBytes.length == MAX_META_BYTES && resp.getContent().read() != -1) { - throw new MetaFileExceedsMaxException(nextVersionUrl.toString(), MAX_META_BYTES); - } - var newRoot = GSON.get().fromJson(new String(rootBytes, StandardCharsets.UTF_8), Root.class); - + var newRoot = newRootMaybe.get(); // 5.3.4) we have a valid next version of the root.json. Check that the file has been signed // by: // a) a threshold (from step 2) of keys specified in the trusted metadata @@ -163,7 +139,7 @@ public void updateRoot(Path trustedRootPath, URL mirror, TufLocalStore localStor // otherwise throw error. ZonedDateTime expires = trustedRoot.getSignedMeta().getExpiresAsDate(); if (expires.isBefore(updateStartTime)) { - throw new RootExpiredException(mirror.toString(), updateStartTime, expires); + throw new RootExpiredException(fetcher.getSource(), updateStartTime, expires); } // 5.3.11) If the timestamp and / or snapshot keys have been rotated, then delete the trusted // timestamp and snapshot metadata files. @@ -230,7 +206,7 @@ void verifyDelegate( public void updateTimestamp() { // 1) download the timestamp.json bytes up to few 10s of K max. - // 2) verify against threshold of keys as specified in trusted root,json + // 2) verify against threshold of keys as specified in trusted root.json // 3) check that version of new timestamp.json is higher or equal than current, else fail. // 3.2) check that timestamp.snapshot.version <= timestamp.version or fail @@ -283,4 +259,42 @@ public void updateTargets() { // Done!! } + + public static class Builder { + private Clock clock = Clock.systemUTC(); + private Verifiers.Supplier verifiers = Verifiers::newVerifier; + + private MetaFetcher fetcher; + private Path trustedRootPath; + private TufLocalStore localStore; + + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + public Builder setVerifiers(Verifiers.Supplier verifiers) { + this.verifiers = verifiers; + return this; + } + + public Builder setLocalStore(TufLocalStore store) { + this.localStore = store; + return this; + } + + public Builder setTrustedRootPath(Path trustedRootPath) { + this.trustedRootPath = trustedRootPath; + return this; + } + + public Builder setFetcher(MetaFetcher fetcher) { + this.fetcher = fetcher; + return this; + } + + public TufClient build() { + return new TufClient(clock, verifiers, fetcher, trustedRootPath, localStore); + } + } } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/TufClientTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/TufClientTest.java index 546e713b..5267ec84 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/TufClientTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/TufClientTest.java @@ -53,9 +53,10 @@ class TufClientTest { public static final String TEST_STATIC_UPDATE_TIME = "2022-09-09T13:37:00.00Z"; + static Server remote; static String remoteUrl; - private final Path trustedRoot = TestResources.CLIENT_TRUSTED_ROOT; + private static final Path trustedRoot = TestResources.CLIENT_TRUSTED_ROOT; @TempDir Path localStore; @TempDir static Path localMirror; Path tufTestData = Paths.get("src/test/resources/dev/sigstore/tuf/"); @@ -80,10 +81,8 @@ static void startRemoteResourceServer() throws Exception { public void testRootUpdate_fromProdData() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { setupMirror("remote-repo-prod", "1.root.json", "2.root.json", "3.root.json", "4.root.json"); - var client = createTimeStaticTufClient(); - Path trustedRoot = tufTestData.resolve("trusted-root.json"); - client.updateRoot( - trustedRoot, new URL(remoteUrl), FileSystemTufStore.newFileSystemStore(localStore)); + var client = createTimeStaticTufClient(localStore); + client.updateRoot(); assertStoreContains("root.json"); Root oldRoot = TestResources.loadRoot(trustedRoot); Root newRoot = TestResources.loadRoot(localStore.resolve("root.json")); @@ -95,10 +94,9 @@ public void testRootUpdate_fromProdData() public void testRootUpdate_notEnoughSignatures() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { setupMirror("remote-repo-unsigned", "2.root.json"); - var client = createTimeStaticTufClient(); + var client = createTimeStaticTufClient(localStore); try { - client.updateRoot( - trustedRoot, new URL(remoteUrl), FileSystemTufStore.newFileSystemStore(localStore)); + client.updateRoot(); fail(); } catch (SignatureVerificationException e) { assertEquals(3, e.getRequiredSignatures()); @@ -110,10 +108,9 @@ public void testRootUpdate_notEnoughSignatures() public void testRootUpdate_expiredRoot() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { setupMirror("remote-repo-expired", "2.root.json"); - var client = createTimeStaticTufClient(); + var client = createTimeStaticTufClient(localStore); try { - client.updateRoot( - trustedRoot, new URL(remoteUrl), FileSystemTufStore.newFileSystemStore(localStore)); + client.updateRoot(); fail(); } catch (RootExpiredException e) { assertEquals(ZonedDateTime.parse(TEST_STATIC_UPDATE_TIME), e.getUpdateTime()); @@ -127,10 +124,9 @@ public void testRootUpdate_expiredRoot() public void testRootUpdate_inconsistentVersion() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { setupMirror("remote-repo-inconsistent-version", "2.root.json"); - var client = createTimeStaticTufClient(); + var client = createTimeStaticTufClient(localStore); try { - client.updateRoot( - trustedRoot, new URL(remoteUrl), FileSystemTufStore.newFileSystemStore(localStore)); + client.updateRoot(); fail(); } catch (RoleVersionException e) { assertEquals(2, e.getExpectedVersion()); @@ -142,10 +138,9 @@ public void testRootUpdate_inconsistentVersion() public void testRootUpdate_metaFileTooBig() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { setupMirror("remote-repo-meta-file-too-big", "2.root.json"); - var client = createTimeStaticTufClient(); + var client = createTimeStaticTufClient(localStore); try { - client.updateRoot( - trustedRoot, new URL(remoteUrl), FileSystemTufStore.newFileSystemStore(localStore)); + client.updateRoot(); fail(); } catch (MetaFileExceedsMaxException e) { } @@ -209,27 +204,7 @@ public void testVerifyDelegate_verificationFailed() Map publicKeys = ImmutableMap.of(PUB_KEY_1.getLeft(), PUB_KEY_1.getRight()); Role delegate = ImmutableRootRole.builder().addKeyids(PUB_KEY_1.getLeft()).threshold(1).build(); byte[] verificationMaterial = "alksdjfas".getBytes(StandardCharsets.UTF_8); - var client = - new TufClient( - publicKey -> - new Verifier() { - @Override - public PublicKey getPublicKey() { - return null; - } - - @Override - public boolean verify(byte[] artifact, byte[] signature) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - return false; - } - - @Override - public boolean verifyDigest(byte[] artifactDigest, byte[] signature) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - return false; - } - }); + var client = TufClient.builder().setVerifiers(ALWAYS_FAILS).build(); try { client.verifyDelegate(sigs, publicKeys, delegate, verificationMaterial); fail("This should have failed since the public key for PUB_KEY_1 should fail to verify."); @@ -323,34 +298,19 @@ static Key newKey(String keyContents) { } @NotNull - private static TufClient createTimeStaticTufClient() { - return new TufClient( - Clock.fixed(Instant.parse(TEST_STATIC_UPDATE_TIME), ZoneOffset.UTC), - Verifiers::newVerifier); + private static TufClient createTimeStaticTufClient(Path localStore) throws IOException { + return TufClient.builder() + .setClock(Clock.fixed(Instant.parse(TEST_STATIC_UPDATE_TIME), ZoneOffset.UTC)) + .setVerifiers(Verifiers::newVerifier) + .setFetcher(HttpMetaFetcher.newFetcher(new URL(remoteUrl))) + .setTrustedRootPath(trustedRoot) + .setLocalStore(FileSystemTufStore.newFileSystemStore(localStore)) + .build(); } @NotNull private static TufClient createAlwaysVerifyingTufClient() { - return new TufClient( - publicKey -> - new Verifier() { - @Override - public PublicKey getPublicKey() { - return null; - } - - @Override - public boolean verify(byte[] artifact, byte[] signature) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - return true; - } - - @Override - public boolean verifyDigest(byte[] artifactDigest, byte[] signature) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - return true; - } - }); + return TufClient.builder().setVerifiers(ALWAYS_VERIFIES).build(); } private static void setupMirror(String repoName, String... files) throws IOException { @@ -383,4 +343,45 @@ void clearLocalMirror() { static void shutdownRemoteResourceServer() throws Exception { remote.stop(); } + + public static final Verifiers.Supplier ALWAYS_VERIFIES = + publicKey -> + new Verifier() { + @Override + public PublicKey getPublicKey() { + return null; + } + + @Override + public boolean verify(byte[] artifact, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return true; + } + + @Override + public boolean verifyDigest(byte[] artifactDigest, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return true; + } + }; + public static final Verifiers.Supplier ALWAYS_FAILS = + publicKey -> + new Verifier() { + @Override + public PublicKey getPublicKey() { + return null; + } + + @Override + public boolean verify(byte[] artifact, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return false; + } + + @Override + public boolean verifyDigest(byte[] artifactDigest, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return false; + } + }; }