From 70ebdbead213fd34ba372eb7b31453257b2063d7 Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Thu, 24 Oct 2024 20:00:53 -0400 Subject: [PATCH 1/3] Store meta state in memory Delegated targets should be loaded at target search time, so keep state in memory so we can use it as necessary. Signed-off-by: Appu Goundan --- .../java/dev/sigstore/tuf/TrustedMeta.java | 114 +++++++ .../main/java/dev/sigstore/tuf/Updater.java | 112 ++++--- .../dev/sigstore/tuf/TrustedMetaTest.java | 182 +++++++++++ .../java/dev/sigstore/tuf/UpdaterTest.java | 300 +++++++----------- 4 files changed, 471 insertions(+), 237 deletions(-) create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java create mode 100644 sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java new file mode 100644 index 00000000..cd737d6e --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java @@ -0,0 +1,114 @@ +/* + * 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 dev.sigstore.tuf.model.Root; +import dev.sigstore.tuf.model.RootRole; +import dev.sigstore.tuf.model.Snapshot; +import dev.sigstore.tuf.model.Targets; +import dev.sigstore.tuf.model.Timestamp; +import java.io.IOException; +import java.util.Optional; + +// An in memory cache that will pass through to a provided local tuf store +class TrustedMeta { + private final MutableTufStore localStore; + private Root root; + private Snapshot snapshot; + private Timestamp timestamp; + private Targets targets; + + private TrustedMeta(MutableTufStore localStore) { + this.localStore = localStore; + } + + static TrustedMeta newTrustedMeta(MutableTufStore localStore) { + return new TrustedMeta(localStore); + } + + public void setRoot(Root root) throws IOException { + // call storeTrustedRoot instead of generic storeMeta because it does doesn't extra work + localStore.storeTrustedRoot(root); + this.root = root; + } + + public Root getRoot() throws IOException { + return findRoot().orElseThrow(() -> new IllegalStateException("No cached root to load")); + } + + public Optional findRoot() throws IOException { + if (root == null) { + root = localStore.loadTrustedRoot().orElse(null); + } + return Optional.ofNullable(root); + } + + public void setTimestamp(Timestamp timestamp) throws IOException { + localStore.storeMeta(RootRole.TIMESTAMP, timestamp); + this.timestamp = timestamp; + } + + public Timestamp getTimestamp() throws IOException { + return findTimestamp() + .orElseThrow(() -> new IllegalStateException("No cached timestamp to load")); + } + + public Optional findTimestamp() throws IOException { + if (timestamp == null) { + timestamp = localStore.loadTimestamp().orElse(null); + } + return Optional.ofNullable(timestamp); + } + + public void setSnapshot(Snapshot snapshot) throws IOException { + localStore.storeMeta(RootRole.SNAPSHOT, snapshot); + this.snapshot = snapshot; + } + + public Snapshot getSnapshot() throws IOException { + return findSnapshot() + .orElseThrow(() -> new IllegalStateException("No cached snapshot to load")); + } + + public Optional findSnapshot() throws IOException { + if (snapshot == null) { + snapshot = localStore.loadSnapshot().orElse(null); + } + return Optional.ofNullable(snapshot); + } + + public void setTargets(Targets targets) throws IOException { + localStore.storeMeta(RootRole.TARGETS, targets); + this.targets = targets; + } + + public Targets getTargets() throws IOException { + return findTargets().orElseThrow(() -> new IllegalStateException("No cached targets to load")); + } + + public Optional findTargets() throws IOException { + if (targets == null) { + targets = localStore.loadTargets().orElse(null); + } + return Optional.ofNullable(targets); + } + + public void clearMetaDueToKeyRotation() throws IOException { + localStore.clearMetaDueToKeyRotation(); + timestamp = null; + snapshot = null; + } +} 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 05f622c9..c2580b52 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -57,13 +58,17 @@ public class Updater { private static final Logger log = Logger.getLogger(Updater.class.getName()); - private Clock clock; - private Verifiers.Supplier verifiers; - private MetaFetcher metaFetcher; - private Fetcher targetFetcher; + private final Clock clock; + private final Verifiers.Supplier verifiers; + private final MetaFetcher metaFetcher; + private final Fetcher targetFetcher; + private final RootProvider trustedRootPath; + // TODO: this should be replaced by a dedicated target store + private final MutableTufStore localStore; + + // Mutable State private ZonedDateTime updateStartTime; - private RootProvider trustedRootPath; - private MutableTufStore localStore; + private TrustedMeta trustedMeta; Updater( Clock clock, @@ -78,6 +83,7 @@ public class Updater { this.localStore = localStore; this.metaFetcher = metaFetcher; this.targetFetcher = targetFetcher; + this.trustedMeta = TrustedMeta.newTrustedMeta(localStore); } public static Builder builder() { @@ -86,28 +92,36 @@ public static Builder builder() { public void update() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { - var root = updateRoot(); - // only returns a timestamp value if a more recent timestamp file has been found. - var timestampMaybe = updateTimestamp(root); - if (timestampMaybe.isPresent()) { - var snapshot = updateSnapshot(root, timestampMaybe.get()); - var targets = updateTargets(root, snapshot); - downloadTargets(targets); + updateMeta(); + downloadTargets(trustedMeta.getTargets()); + } + + void updateMeta() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + updateRoot(); + var oldTimestamp = trustedMeta.findTimestamp(); + updateTimestamp(); + if (Objects.equals(oldTimestamp.orElse(null), trustedMeta.getTimestamp()) + && trustedMeta.findSnapshot().isPresent() + && trustedMeta.findTargets().isPresent()) { + return; } + // if we need to update or we can't find targets/timestamps locally then grab new snapshot and + // targets from remote + updateSnapshot(); + updateTargets(); } // https://theupdateframework.github.io/specification/latest/#detailed-client-workflow - Root updateRoot() + void updateRoot() throws IOException, RoleExpiredException, NoSuchAlgorithmException, InvalidKeySpecException, - InvalidKeyException, FileExceedsMaxLengthException, RollbackVersionException, - SignatureVerificationException { + FileExceedsMaxLengthException, RollbackVersionException, SignatureVerificationException { // 5.3.1) record the time at start and use for expiration checks consistently throughout the // update. updateStartTime = ZonedDateTime.now(clock); // 5.3.2) load the trust metadata file (root.json), get version of root.json and the role // signature threshold value - Optional localRoot = localStore.loadTrustedRoot(); + Optional localRoot = trustedMeta.findRoot(); Root trustedRoot; if (localRoot.isPresent()) { trustedRoot = localRoot.get(); @@ -148,7 +162,7 @@ Root updateRoot() // 5.3.7) set the trusted root metadata to the new root trustedRoot = newRoot; // 5.3.8) persist to repo - localStore.storeTrustedRoot(trustedRoot); + trustedMeta.setRoot(trustedRoot); // 5.3.9) see if there are more versions go back 5.3.3 nextVersion++; } @@ -164,9 +178,9 @@ Root updateRoot() || hasNewKeys( preUpdateTimestampRole, trustedRoot.getSignedMeta().getRoles().get(RootRole.TIMESTAMP))) { - localStore.clearMetaDueToKeyRotation(); + trustedMeta.clearMetaDueToKeyRotation(); } - return trustedRoot; + trustedMeta.setRoot(trustedRoot); } private void throwIfExpired(ZonedDateTime expires) { @@ -265,9 +279,9 @@ void verifyDelegate( } } - Optional updateTimestamp(Root root) - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, - FileNotFoundException, SignatureVerificationException { + void updateTimestamp() + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, FileNotFoundException, + SignatureVerificationException { // 1) download the timestamp.json bytes. var timestamp = metaFetcher @@ -276,12 +290,12 @@ Optional updateTimestamp(Root root) .getMetaResource(); // 2) verify against threshold of keys as specified in trusted root.json - verifyDelegate(root, timestamp); + verifyDelegate(trustedMeta.getRoot(), timestamp); // 3) If the new timestamp file has a lesser version than our current trusted timestamp file - // report a rollback attack. If it is equal abort the update as there should be no changes. If - // it is higher than continue update. - Optional localTimestampMaybe = localStore.loadTimestamp(); + // report a rollback attack. If it is equal, just return the original timestamp there should + // be no changes. If it is higher than continue update. + Optional localTimestampMaybe = trustedMeta.findTimestamp(); if (localTimestampMaybe.isPresent()) { Timestamp localTimestamp = localTimestampMaybe.get(); if (localTimestamp.getSignedMeta().getVersion() > timestamp.getSignedMeta().getVersion()) { @@ -289,28 +303,28 @@ Optional updateTimestamp(Root root) localTimestamp.getSignedMeta().getVersion(), timestamp.getSignedMeta().getVersion()); } if (localTimestamp.getSignedMeta().getVersion() == timestamp.getSignedMeta().getVersion()) { - return Optional.empty(); + trustedMeta.setTimestamp(localTimestamp); + return; } } // 4) check expiration timestamp is after tuf update start time, else fail. throwIfExpired(timestamp.getSignedMeta().getExpiresAsDate()); // 5) persist timestamp.json - localStore.storeMeta(RootRole.TIMESTAMP, timestamp); - return Optional.of(timestamp); + trustedMeta.setTimestamp(timestamp); } - Snapshot updateSnapshot(Root root, Timestamp timestamp) + void updateSnapshot() throws IOException, FileNotFoundException, InvalidHashesException, - SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException, - InvalidKeyException { + SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException { // 1) download the snapshot.json bytes up to timestamp's snapshot length. - int timestampSnapshotVersion = timestamp.getSignedMeta().getSnapshotMeta().getVersion(); + int timestampSnapshotVersion = + trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getVersion(); var snapshotResult = metaFetcher.getMeta( RootRole.SNAPSHOT, timestampSnapshotVersion, Snapshot.class, - timestamp.getSignedMeta().getSnapshotMeta().getLengthOrDefault()); + trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getLengthOrDefault()); if (snapshotResult.isEmpty()) { throw new FileNotFoundException( timestampSnapshotVersion + ".snapshot.json", metaFetcher.getSource()); @@ -318,14 +332,14 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp) // 2) check against timestamp.snapshot.hash, this is optional, the fallback is // that the version must match, which is handled in (4). var snapshot = snapshotResult.get(); - if (timestamp.getSignedMeta().getSnapshotMeta().getHashes().isPresent()) { + if (trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().isPresent()) { verifyHashes( "snapshot", snapshot.getRawBytes(), - timestamp.getSignedMeta().getSnapshotMeta().getHashes().get()); + trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().get()); } // 3) Check against threshold of root signing keys, else fail - verifyDelegate(root, snapshot.getMetaResource()); + verifyDelegate(trustedMeta.getRoot(), snapshot.getMetaResource()); // 4) Check snapshot.version matches timestamp.snapshot.version, else fail. int snapshotVersion = snapshot.getMetaResource().getSignedMeta().getVersion(); if (snapshotVersion != timestampSnapshotVersion) { @@ -334,7 +348,7 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp) // 5) Ensure all targets and delegated targets in the trusted (old) snapshots file have versions // which are less than or equal to the equivalent target in the new file. Check that no targets // are missing in new file. Else fail. - var trustedSnapshotMaybe = localStore.loadSnapshot(); + var trustedSnapshotMaybe = trustedMeta.findSnapshot(); if (trustedSnapshotMaybe.isPresent()) { var trustedSnapshot = trustedSnapshotMaybe.get(); for (Map.Entry trustedTargetEntry : @@ -356,8 +370,7 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp) // 6) Ensure expiration timestamp of snapshot is later than tuf update start time. throwIfExpired(snapshot.getMetaResource().getSignedMeta().getExpiresAsDate()); // 7) persist snapshot. - localStore.storeMeta(RootRole.SNAPSHOT, snapshot.getMetaResource()); - return snapshot.getMetaResource(); + trustedMeta.setSnapshot(snapshot.getMetaResource()); } // this method feels very wrong. I would not show it to a friend. @@ -389,12 +402,13 @@ static void verifyHashes(String name, byte[] data, Hashes hashes) throws Invalid } } - Targets updateTargets(Root root, Snapshot snapshot) + void updateTargets() throws IOException, FileNotFoundException, InvalidHashesException, SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException, - InvalidKeyException, FileExceedsMaxLengthException { + FileExceedsMaxLengthException { // 1) download the targets.json up to targets.json length in bytes. - SnapshotMeta.SnapshotTarget targetMeta = snapshot.getSignedMeta().getTargetMeta("targets.json"); + SnapshotMeta.SnapshotTarget targetMeta = + trustedMeta.getSnapshot().getSignedMeta().getTargetMeta("targets.json"); var targetsResultMaybe = metaFetcher.getMeta( RootRole.TARGETS, @@ -415,7 +429,7 @@ Targets updateTargets(Root root, Snapshot snapshot) targetMeta.getHashes().get()); } // 3) check against threshold of keys as specified by trusted root.json - verifyDelegate(root, targetsResult.getMetaResource()); + verifyDelegate(trustedMeta.getRoot(), targetsResult.getMetaResource()); // 4) check targets.version == snapshot.targets.version, else fail. int targetsVersion = targetsResult.getMetaResource().getSignedMeta().getVersion(); int snapshotTargetsVersion = targetMeta.getVersion(); @@ -426,8 +440,7 @@ Targets updateTargets(Root root, Snapshot snapshot) throwIfExpired(targetsResult.getMetaResource().getSignedMeta().getExpiresAsDate()); // 6) persist targets metadata // why do we persist the - localStore.storeMeta(RootRole.TARGETS, targetsResult.getMetaResource()); - return targetsResult.getMetaResource(); + trustedMeta.setTargets(targetsResult.getMetaResource()); } void downloadTargets(Targets targets) @@ -470,6 +483,11 @@ MutableTufStore getLocalStore() { return localStore; } + @VisibleForTesting + TrustedMeta getTrustedMeta() { + return trustedMeta; + } + public static class Builder { private Clock clock = Clock.systemUTC(); private Verifiers.Supplier verifiers = Verifiers::newVerifier; diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java new file mode 100644 index 00000000..57e47a4e --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java @@ -0,0 +1,182 @@ +/* + * 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 static dev.sigstore.json.GsonSupplier.GSON; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.io.Resources; +import dev.sigstore.tuf.model.Root; +import dev.sigstore.tuf.model.Snapshot; +import dev.sigstore.tuf.model.Targets; +import dev.sigstore.tuf.model.Timestamp; +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class TrustedMetaTest { + + @TempDir Path localStore; + private MutableTufStore tufStore; + private TrustedMeta trustedMeta; + + private static Root root; + private static Timestamp timestamp; + private static Snapshot snapshot; + private static Targets targets; + + @BeforeAll + public static void readAllMeta() throws IOException, URISyntaxException { + Path rootResource = + Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/root.json").getPath()); + root = GSON.get().fromJson(Files.newBufferedReader(rootResource), Root.class); + Path timestampResource = + Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/timestamp.json").getPath()); + timestamp = GSON.get().fromJson(Files.newBufferedReader(timestampResource), Timestamp.class); + Path snapshotResource = + Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/snapshot.json").getPath()); + snapshot = GSON.get().fromJson(Files.newBufferedReader(snapshotResource), Snapshot.class); + Path targetsResource = + Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/targets.json").getPath()); + targets = GSON.get().fromJson(Files.newBufferedReader(targetsResource), Targets.class); + } + + @BeforeEach + public void setup() throws IOException { + tufStore = FileSystemTufStore.newFileSystemStore(localStore); + trustedMeta = TrustedMeta.newTrustedMeta(tufStore); + } + + @Test + public void root_test() throws Exception { + assertTrue(tufStore.loadTrustedRoot().isEmpty()); + assertTrue(trustedMeta.findRoot().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getRoot); + + trustedMeta.setRoot(root); + + assertTrue(tufStore.loadTrustedRoot().isPresent()); + assertTrue(trustedMeta.findRoot().isPresent()); + Assertions.assertEquals(root, trustedMeta.getRoot()); + } + + @Test + public void root_canInitFromDisk() throws Exception { + assertTrue(tufStore.loadTrustedRoot().isEmpty()); + assertTrue(trustedMeta.findRoot().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getRoot); + + try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("root.json"))) { + GSON.get().toJson(root, fileWriter); + } + + assertTrue(tufStore.loadTrustedRoot().isPresent()); + assertTrue(trustedMeta.findRoot().isPresent()); + Assertions.assertEquals(root, trustedMeta.getRoot()); + } + + @Test + public void timestamp_test() throws Exception { + assertTrue(tufStore.loadTimestamp().isEmpty()); + assertTrue(trustedMeta.findTimestamp().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTimestamp); + + trustedMeta.setTimestamp(timestamp); + + assertTrue(tufStore.loadTimestamp().isPresent()); + assertTrue(trustedMeta.findTimestamp().isPresent()); + Assertions.assertEquals(timestamp, trustedMeta.getTimestamp()); + } + + @Test + public void timestamp_canInitFromDisk() throws Exception { + assertTrue(tufStore.loadTimestamp().isEmpty()); + assertTrue(trustedMeta.findTimestamp().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTimestamp); + + try (BufferedWriter fileWriter = + Files.newBufferedWriter(localStore.resolve("timestamp.json"))) { + GSON.get().toJson(timestamp, fileWriter); + } + + assertTrue(tufStore.loadTimestamp().isPresent()); + assertTrue(trustedMeta.findTimestamp().isPresent()); + Assertions.assertEquals(timestamp, trustedMeta.getTimestamp()); + } + + @Test + public void snapshot_test() throws Exception { + assertTrue(tufStore.loadSnapshot().isEmpty()); + assertTrue(trustedMeta.findSnapshot().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getSnapshot); + + trustedMeta.setSnapshot(snapshot); + + assertTrue(tufStore.loadSnapshot().isPresent()); + assertTrue(trustedMeta.findSnapshot().isPresent()); + Assertions.assertEquals(snapshot, trustedMeta.getSnapshot()); + } + + @Test + public void snapshot_canInitFromDisk() throws Exception { + assertTrue(tufStore.loadSnapshot().isEmpty()); + assertTrue(trustedMeta.findSnapshot().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getSnapshot); + + try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("snapshot.json"))) { + GSON.get().toJson(snapshot, fileWriter); + } + + assertTrue(tufStore.loadSnapshot().isPresent()); + assertTrue(trustedMeta.findSnapshot().isPresent()); + Assertions.assertEquals(snapshot, trustedMeta.getSnapshot()); + } + + @Test + public void targets_test() throws Exception { + assertTrue(tufStore.loadTargets().isEmpty()); + assertTrue(trustedMeta.findTargets().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTargets); + + trustedMeta.setTargets(targets); + + assertTrue(tufStore.loadTargets().isPresent()); + assertTrue(trustedMeta.findTargets().isPresent()); + Assertions.assertEquals(targets, trustedMeta.getTargets()); + } + + @Test + public void targets_canInitFromDisk() throws Exception { + assertTrue(tufStore.loadTargets().isEmpty()); + assertTrue(trustedMeta.findTargets().isEmpty()); + Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTargets); + + try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("targets.json"))) { + GSON.get().toJson(targets, fileWriter); + } + + assertTrue(tufStore.loadTargets().isPresent()); + assertTrue(trustedMeta.findTargets().isPresent()); + Assertions.assertEquals(targets, trustedMeta.getTargets()); + } +} 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 8aebf0ea..2fa2903b 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java @@ -15,6 +15,7 @@ */ package dev.sigstore.tuf; +import static dev.sigstore.json.GsonSupplier.GSON; import static dev.sigstore.testkit.tuf.TestResources.UPDATER_REAL_TRUSTED_ROOT; import static dev.sigstore.testkit.tuf.TestResources.UPDATER_SYNTHETIC_TRUSTED_ROOT; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -40,10 +41,8 @@ import dev.sigstore.tuf.model.Role; import dev.sigstore.tuf.model.Root; import dev.sigstore.tuf.model.Signature; -import dev.sigstore.tuf.model.Snapshot; import dev.sigstore.tuf.model.TargetMeta; import dev.sigstore.tuf.model.Targets; -import dev.sigstore.tuf.model.Timestamp; import io.github.netmikey.logunit.api.LogCapturer; import java.io.File; import java.io.IOException; @@ -71,6 +70,8 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.util.resource.Resource; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterAll; @@ -124,8 +125,7 @@ static void startRemoteResourceServer() throws Exception { } @Test - public void testRootUpdate_fromProdData() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testRootUpdate_fromProdData() throws Exception { setupMirror( "real/prod", "1.root.json", "2.root.json", "3.root.json", "4.root.json", "5.root.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_REAL_TRUSTED_ROOT); @@ -193,8 +193,7 @@ public void testRootUpdate_newRootHasInvalidSignatures() throws Exception { } @Test - public void testRootUpdate_expiredRoot() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testRootUpdate_expiredRoot() throws Exception { setupMirror("synthetic/test-template", "2.root.json"); // root expires 2023-03-09T18:02:21Z var updater = @@ -211,9 +210,7 @@ public void testRootUpdate_expiredRoot() } @Test - public void testRootUpdate_wrongVersion() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, - SignatureVerificationException { + public void testRootUpdate_wrongVersion() throws Exception { setupMirror("synthetic/root-wrong-version", "2.root.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); try { @@ -226,8 +223,7 @@ public void testRootUpdate_wrongVersion() } @Test - public void testRootUpdate_metaFileTooBig() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testRootUpdate_metaFileTooBig() throws Exception { setupMirror("synthetic/root-too-big", "2.root.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); try { @@ -239,85 +235,70 @@ public void testRootUpdate_metaFileTooBig() } @Test - public void testTimestampUpdate_throwMetaNotFoundException() throws IOException { + public void testTimestampUpdate_throwMetaNotFoundException() throws Exception { setupMirror("synthetic/test-template", "2.root.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - assertThrows(FileNotFoundException.class, () -> updater.updateTimestamp(updater.updateRoot())); + var ex = assertThrows(FileNotFoundException.class, updater::update); + MatcherAssert.assertThat( + ex.getMessage(), CoreMatchers.startsWith("file (timestamp.json) was not found at source")); } @Test - public void testTimestampUpdate_throwsSignatureVerificationException() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTimestampUpdate_throwsSignatureVerificationException() throws Exception { setupMirror("synthetic/timestamp-unsigned", "2.root.json", "timestamp.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - try { - updater.updateTimestamp(updater.updateRoot()); - fail("The timestamp was not signed so should have thown a SignatureVerificationException."); - } catch (SignatureVerificationException e) { - assertEquals(0, e.getVerifiedSignatures(), "verified signature threshold did not match"); - assertEquals(1, e.getRequiredSignatures(), "required signatures found did not match"); - } + var ex = + assertThrows( + SignatureVerificationException.class, + updater::update, + "The timestamp was not signed so should have thown a SignatureVerificationException."); + assertEquals(0, ex.getVerifiedSignatures(), "verified signature threshold did not match"); + assertEquals(1, ex.getRequiredSignatures(), "required signatures found did not match"); } @Test - public void testTimestampUpdate_throwsRollbackVersionException() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTimestampUpdate_throwsRollbackVersionException() throws Exception { bootstrapLocalStore(localStorePath, "synthetic/test-template", "root.json", "timestamp.json"); setupMirror("synthetic/timestamp-rollback-version", "2.root.json", "timestamp.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - try { - updater.updateTimestamp(updater.updateRoot()); - fail( - "The repo in this test provides an older signed timestamp version that should have caused a RoleVersionException."); - } catch (RollbackVersionException e) { - assertEquals(3, e.getCurrentVersion(), "expected timestamp version did not match"); - assertEquals(1, e.getFoundVersion(), "found timestamp version did not match"); - } + var ex = + assertThrows( + RollbackVersionException.class, + updater::update, + "The repo in this test provides an older signed timestamp version that should have caused a RoleVersionException."); + assertEquals(3, ex.getCurrentVersion(), "expected timestamp version did not match"); + assertEquals(1, ex.getFoundVersion(), "found timestamp version did not match"); } @Test - public void testTimestampUpdate_noUpdate() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { - bootstrapLocalStore(localStorePath, "synthetic/test-template", "2.root.json", "timestamp.json"); - setupMirror("synthetic/test-template", "2.root.json", "timestamp.json"); - var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - assertTrue( - updater.updateTimestamp(updater.updateRoot()).isEmpty(), - "We expect the updater to return an empty timestamp if there are no updates"); - } - - @Test - public void testTimestampUpdate_throwsRoleExpiredException() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTimestampUpdate_throwsRoleExpiredException() throws Exception { setupMirror("synthetic/test-template", "2.root.json", "timestamp.json"); // timestamp expires 2022-12-10T18:07:30Z var updater = createTimeStaticUpdater( localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2023-02-13T15:37:49Z"); - try { - updater.updateTimestamp(updater.updateRoot()); - fail("Expects a RoleExpiredException as the repo timestamp.json should be expired."); - } catch (RoleExpiredException e) { - // expected. - } + + assertThrows( + RoleExpiredException.class, + updater::update, + "Expects a RoleExpiredException as the repo timestamp.json should be expired."); } @Test - public void testTimestampUpdate_noPreviousTimestamp_success() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTimestampUpdate_noPreviousTimestamp_success() throws Exception { setupMirror("synthetic/test-template", "2.root.json", "timestamp.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - updater.updateTimestamp(updater.updateRoot()); + updater.updateRoot(); + updater.updateTimestamp(); assertStoreContains("timestamp.json"); assertEquals( 3, - updater.getLocalStore().loadTimestamp().get().getSignedMeta().getVersion(), + updater.getTrustedMeta().getTimestamp().getSignedMeta().getVersion(), "timestamp version did not match expectations"); } @Test - public void testTimestampUpdate_updateExistingTimestamp_success() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTimestampUpdate_updateExistingTimestamp_success() throws Exception { bootstrapLocalStore( localStorePath, "synthetic/test-template", "1.root.json", "1.timestamp.json"); setupMirror("synthetic/test-template", "1.root.json", "2.root.json", "timestamp.json"); @@ -326,62 +307,51 @@ public void testTimestampUpdate_updateExistingTimestamp_success() 1, updater.getLocalStore().loadTimestamp().get().getSignedMeta().getVersion(), "timestamp version should start at 1 before the update."); - updater.updateTimestamp(updater.updateRoot()); + updater.updateRoot(); + updater.updateTimestamp(); assertStoreContains("timestamp.json"); assertEquals( 3, - updater.getLocalStore().loadTimestamp().get().getSignedMeta().getVersion(), + updater.getTrustedMeta().getTimestamp().getSignedMeta().getVersion(), "timestamp version did not match expectations."); } @Test - public void testSnapshotUpdate_snapshotMetaMissing() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_snapshotMetaMissing() throws Exception { setupMirror("synthetic/test-template", "2.root.json", "timestamp.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); + updater.updateRoot(); + updater.updateTimestamp(); assertThrows( FileNotFoundException.class, - () -> updater.updateSnapshot(root, timestamp.get()), + updater::updateSnapshot, "Expected remote with no snapshot.json to throw FileNotFoundException."); } @Test - public void testSnapshotUpdate_invalidHash() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_invalidHash() throws Exception { setupMirror( "synthetic/snapshot-invalid-hash", "2.root.json", "timestamp.json", "3.snapshot.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); assertThrows( InvalidHashesException.class, - () -> { - updater.updateSnapshot(root, timestamp.get()); - }, + updater::update, "snapshot.json edited and should fail hash test."); } @Test - public void testSnapshotUpdate_timestampSnapshotVersionMismatch() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_timestampSnapshotVersionMismatch() throws Exception { setupMirror( "synthetic/snapshot-version-mismatch", "2.root.json", "timestamp.json", "3.snapshot.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); assertThrows( SnapshotVersionMismatchException.class, - () -> { - updater.updateSnapshot(root, timestamp.get()); - }, + updater::update, "snapshot version should not match the timestamp metadata."); } @Test - public void testSnapshotUpdate_snapshotTargetMissing() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_snapshotTargetMissing() throws Exception { bootstrapLocalStore( localStorePath, "synthetic/test-template", @@ -391,19 +361,14 @@ public void testSnapshotUpdate_snapshotTargetMissing() setupMirror( "synthetic/snapshot-target-missing", "2.root.json", "timestamp.json", "4.snapshot.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); assertThrows( SnapshotTargetMissingException.class, - () -> { - updater.updateSnapshot(root, timestamp.get()); - }, + updater::update, "All targets from previous versions of snapshot should be contained in future versions of snapshot."); } @Test - public void testSnapshotUpdate_snapshotTargetVersionRollback() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_snapshotTargetVersionRollback() throws Exception { bootstrapLocalStore( localStorePath, "synthetic/test-template", @@ -416,30 +381,23 @@ public void testSnapshotUpdate_snapshotTargetVersionRollback() "timestamp.json", "3.snapshot.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); assertThrows( SnapshotTargetVersionException.class, - () -> { - updater.updateSnapshot(root, timestamp.get()); - }, + updater::update, "The new snapshot.json has a targets.json version that is lower than the current target and so we expect a SnapshotTargetVersionException."); } @Test - public void testSnapshotUpdate_success() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_success() throws Exception { setupMirror("synthetic/test-template", "2.root.json", "timestamp.json", "3.snapshot.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - Root root = updater.updateRoot(); - Timestamp timestamp = updater.updateTimestamp(root).get(); - Snapshot result = updater.updateSnapshot(root, timestamp); - assertNotNull(result); + updater.updateRoot(); + updater.updateTimestamp(); + updater.updateSnapshot(); } @Test - public void testSnapshotUpdate_expired() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testSnapshotUpdate_expired() throws Exception { setupMirror("synthetic/snapshot-expired", "2.root.json", "timestamp.json", "3.snapshot.json"); // snapshot expires 2022-11-19T18:07:27Z var updater = @@ -447,33 +405,24 @@ public void testSnapshotUpdate_expired() localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2022-11-20T18:07:27Z"); // one day after - Root root = updater.updateRoot(); - Timestamp timestamp = updater.updateTimestamp(root).get(); - try { - updater.updateSnapshot(root, timestamp); - fail("Expects a RoleExpiredException as the repo snapshot.json should be expired."); - } catch (RoleExpiredException e) { - // pass - } + assertThrows( + RoleExpiredException.class, + updater::update, + "Expects a RoleExpiredException as the repo snapshot.json should be expired."); } @Test - public void testTargetsUpdate_targetMetaMissing() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsUpdate_targetMetaMissing() throws Exception { setupMirror("synthetic/test-template", "2.root.json", "timestamp.json", "3.snapshot.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); assertThrows( FileNotFoundException.class, - () -> updater.updateTargets(root, snapshot), + updater::update, "Expected remote with no target.json to throw FileNotFoundException."); } @Test - public void testTargetsUpdate_invalidHash() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsUpdate_invalidHash() throws Exception { setupMirror( "synthetic/targets-invalid-hash", "2.root.json", @@ -481,18 +430,14 @@ public void testTargetsUpdate_invalidHash() "3.snapshot.json", "3.targets.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); assertThrows( InvalidHashesException.class, - () -> updater.updateTargets(root, snapshot), + updater::update, "targets.json has been modified to have an invalid hash."); } @Test - public void testTargetsUpdate_snapshotVersionMismatch() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsUpdate_snapshotVersionMismatch() throws Exception { setupMirror( "synthetic/targets-snapshot-version-mismatch", "2.root.json", @@ -500,18 +445,14 @@ public void testTargetsUpdate_snapshotVersionMismatch() "3.snapshot.json", "3.targets.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); assertThrows( SnapshotVersionMismatchException.class, - () -> updater.updateTargets(root, snapshot), + updater::update, "targets version should not match the snapshot targets metadata."); } @Test - public void testTargetsUpdate_targetExpired() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsUpdate_targetExpired() throws Exception { // targets expires 2022-11-19T18:07:27Z setupMirror( "synthetic/targets-expired", @@ -524,12 +465,9 @@ public void testTargetsUpdate_targetExpired() localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2022-11-20T18:07:27Z"); // one day after - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); assertThrows( RoleExpiredException.class, - () -> updater.updateTargets(root, snapshot), + updater::update, "targets are out of date and should cause RoleExpiredException."); } @@ -545,19 +483,18 @@ public void testTargetsUpdate_success() var updater = createTimeStaticUpdater( localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2022-11-20T18:07:27Z"); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - var targets = updater.updateTargets(root, snapshot); - assertNotNull(targets); - assertNotNull(updater.getLocalStore().loadTargets()); - assertEquals( - targets.getSignedMeta(), updater.getLocalStore().loadTargets().get().getSignedMeta()); + updater.updateMeta(); + var localTargets = updater.getTrustedMeta().getTargets(); + assertNotNull(localTargets); + var remoteTargets = + GSON.get() + .fromJson( + Files.newBufferedReader(localMirrorPath.resolve("3.targets.json")), Targets.class); + assertEquals(localTargets.getSignedMeta(), remoteTargets.getSignedMeta()); } @Test - public void testTargetsDownload_targetMissingTargetMetadata() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsDownload_targetMissingTargetMetadata() throws Exception { setupMirror( "synthetic/targets-download-missing-target-metadata", "2.root.json", @@ -565,13 +502,15 @@ public void testTargetsDownload_targetMissingTargetMetadata() "3.snapshot.json", "3.targets.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - assertThrows( - JsonSyntaxException.class, - () -> updater.updateTargets(root, snapshot), - "targets.json data should be causing a gson error due to missing TargetData. If at some point we support nullable TargetData this test should be updated to expect TargetMetadataMissingException while calling downloadTargets()."); + var ex = + assertThrows( + JsonSyntaxException.class, + updater::update, + "targets.json data should be causing a gson error due to missing TargetData. If at some point we support nullable TargetData this test should be updated to expect TargetMetadataMissingException while calling downloadTargets()."); + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.endsWith( + "Cannot build TargetData, some of required attributes are not set [hashes, length]")); } @Test @@ -584,13 +523,10 @@ public void testTargetsDownload_targetFileNotFound() "3.snapshot.json", "3.targets.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - var targets = updater.updateTargets(root, snapshot); + updater.updateMeta(); assertThrows( FileNotFoundException.class, - () -> updater.downloadTargets(targets), + () -> updater.downloadTargets(updater.getTrustedMeta().getTargets()), "the target file for download should be missing from the repo and cause an exception."); } @@ -605,13 +541,10 @@ public void testTargetsDownload_targetInvalidLength() "3.targets.json", "targets/860de8f9a858eea7190fcfa1b53fe55914d3c38f17f8f542273012d19cc9509bb423f37b7c13c577a56339ad7f45273b479b1d0df837cb6e20a550c27cce0885.test.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - var targets = updater.updateTargets(root, snapshot); + updater.updateMeta(); assertThrows( FileExceedsMaxLengthException.class, - () -> updater.downloadTargets(targets), + () -> updater.downloadTargets(updater.getTrustedMeta().getTargets()), "The target file is expected to not match the length specified in targets.json target data."); } @@ -626,19 +559,15 @@ public void testTargetsDownload_targetFileInvalidHash() "3.targets.json", "targets/860de8f9a858eea7190fcfa1b53fe55914d3c38f17f8f542273012d19cc9509bb423f37b7c13c577a56339ad7f45273b479b1d0df837cb6e20a550c27cce0885.test.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - var targets = updater.updateTargets(root, snapshot); + updater.updateMeta(); assertThrows( InvalidHashesException.class, - () -> updater.downloadTargets(targets), + () -> updater.downloadTargets(updater.getTrustedMeta().getTargets()), "The target file has been modified and should not match the expected hash"); } @Test - public void testTargetsDownload_success() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsDownload_success() throws Exception { setupMirror( "synthetic/test-template", "2.root.json", @@ -649,20 +578,15 @@ public void testTargetsDownload_success() "targets/32005f02eac21b4cf161a02495330b6c14b548622b5f7e19d59ecfa622de650603ecceea39ed86cc322749a813503a72ad14ce5462c822b511eaf2f2cd2ad8f2.test.txt.v2", "targets/53904bc6216230bf8da0ec42d34004a3f36764de698638641870e37d270e4fd13e1079285f8bca73c2857a279f6f7fbc82038274c3eb48ec5bb2da9b2e30491a.test2.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - var targets = updater.updateTargets(root, snapshot); - updater.downloadTargets(targets); - assertTrue(updater.getLocalStore().getTargetFile("test.txt") != null); - assertTrue(updater.getLocalStore().getTargetFile("test.txt.v2") != null); - assertTrue(updater.getLocalStore().getTargetFile("test2.txt") != null); + updater.update(); + assertNotNull(updater.getLocalStore().getTargetFile("test.txt")); + assertNotNull(updater.getLocalStore().getTargetFile("test.txt.v2")); + assertNotNull(updater.getLocalStore().getTargetFile("test2.txt")); } // Ensure we accept sha256 or sha512 on hashes for targets @Test - public void testTargetsDownload_sha256Only() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testTargetsDownload_sha256Only() throws Exception { setupMirror( "synthetic/targets-sha256-or-sha512", "1.root.json", @@ -677,11 +601,7 @@ public void testTargetsDownload_sha256Only() Resources.getResource("dev/sigstore/tuf/synthetic/targets-sha256-or-sha512/root.json") .getPath()); var updater = createTimeStaticUpdater(localStorePath, UPDATER_ROOT); - var root = updater.updateRoot(); - var timestamp = updater.updateTimestamp(root); - var snapshot = updater.updateSnapshot(root, timestamp.get()); - var targets = updater.updateTargets(root, snapshot); - assertDoesNotThrow(() -> updater.downloadTargets(targets)); + assertDoesNotThrow(updater::update); } // End to end sanity test on the actual prod sigstore repo. @@ -901,8 +821,7 @@ public void testVerifyDelegate_verified() } @Test - public void testVerifyDelegate_verificationFailed() - throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, IOException { + public void testVerifyDelegate_verificationFailed() throws Exception { List sigs = ImmutableList.of(SIG_1, SIG_2); Map publicKeys = ImmutableMap.of(PUB_KEY_1.getLeft(), PUB_KEY_1.getRight()); @@ -920,8 +839,7 @@ public void testVerifyDelegate_verificationFailed() } @Test - public void testVerifyDelegate_belowThreshold() - throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, IOException { + public void testVerifyDelegate_belowThreshold() throws Exception { List sigs = ImmutableList.of(SIG_1, SIG_2); Map publicKeys = ImmutableMap.of(PUB_KEY_1.getLeft(), PUB_KEY_1.getRight()); @@ -997,8 +915,7 @@ public void testVerifyDelegate_goodSigsAndKeysButNotInRole() } @Test - public void testUpdate_snapshotsAndTimestampHaveNoSizeAndNoHashesInMeta() - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + public void testUpdate_snapshotsAndTimestampHaveNoSizeAndNoHashesInMeta() throws Exception { setupMirror( "synthetic/no-size-no-hash-snapshot-timestamp", "2.root.json", @@ -1009,9 +926,12 @@ public void testUpdate_snapshotsAndTimestampHaveNoSizeAndNoHashesInMeta() localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2022-11-20T18:07:27Z"); // one day after - Root root = updater.updateRoot(); - Timestamp timestamp = updater.updateTimestamp(root).get(); - Snapshot snapshot = updater.updateSnapshot(root, timestamp); + updater.updateRoot(); + updater.updateTimestamp(); + updater.updateSnapshot(); + + var timestamp = updater.getTrustedMeta().getTimestamp(); + var snapshot = updater.getTrustedMeta().getSnapshot(); Assertions.assertTrue(timestamp.getSignedMeta().getSnapshotMeta().getHashes().isEmpty()); Assertions.assertTrue(timestamp.getSignedMeta().getSnapshotMeta().getLength().isEmpty()); From be5deffc150e52597f81700a1edaeb0b9b4de0f9 Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Tue, 29 Oct 2024 10:01:57 -0400 Subject: [PATCH 2/3] rework Signed-off-by: Appu Goundan --- .../dev/sigstore/tuf/FileSystemTufStore.java | 50 ++--- .../java/dev/sigstore/tuf/MetaReader.java | 36 ++++ .../{MutableTufStore.java => MetaStore.java} | 27 ++- .../tuf/PassthroughCacheMetaStore.java | 85 ++++++++ .../dev/sigstore/tuf/SigstoreTufClient.java | 12 +- .../java/dev/sigstore/tuf/TargetReader.java | 30 +++ .../java/dev/sigstore/tuf/TargetStore.java | 33 ++++ .../java/dev/sigstore/tuf/TrustedMeta.java | 114 ----------- .../dev/sigstore/tuf/TrustedMetaStore.java | 123 ++++++++++++ .../main/java/dev/sigstore/tuf/TufStore.java | 56 ------ .../main/java/dev/sigstore/tuf/Updater.java | 90 +++++---- .../sigstore/tuf/FileSystemTufStoreTest.java | 22 ++- .../tuf/PassthroughCacheMetaStoreTest.java | 114 +++++++++++ .../sigstore/tuf/SigstoreTufClientTest.java | 6 +- .../dev/sigstore/tuf/TrustedMetaTest.java | 182 ------------------ .../java/dev/sigstore/tuf/UpdaterTest.java | 46 ++--- 16 files changed, 545 insertions(+), 481 deletions(-) create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java rename sigstore-java/src/main/java/dev/sigstore/tuf/{MutableTufStore.java => MetaStore.java} (73%) create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TufStore.java create mode 100644 sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java delete mode 100644 sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java 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 bb86b47e..7f904823 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java @@ -28,13 +28,13 @@ import java.util.Optional; /** Uses a local file system directory to store the trusted TUF metadata. */ -public class FileSystemTufStore implements MutableTufStore { +public class FileSystemTufStore implements MetaStore, TargetStore { private static final String ROOT_FILE_NAME = "root.json"; private static final String SNAPSHOT_FILE_NAME = "snapshot.json"; private static final String TIMESTAMP_FILE_NAME = "timestamp.json"; - private Path repoBaseDir; - private Path targetsCache; + private final Path repoBaseDir; + private final Path targetsCache; @VisibleForTesting FileSystemTufStore(Path repoBaseDir, Path targetsCache) { @@ -42,7 +42,7 @@ public class FileSystemTufStore implements MutableTufStore { this.targetsCache = targetsCache; } - public static MutableTufStore newFileSystemStore(Path repoBaseDir) throws IOException { + public static FileSystemTufStore newFileSystemStore(Path repoBaseDir) throws IOException { if (!Files.isDirectory(repoBaseDir)) { throw new IllegalArgumentException(repoBaseDir + " must be a file system directory."); } @@ -53,7 +53,7 @@ public static MutableTufStore newFileSystemStore(Path repoBaseDir) throws IOExce return newFileSystemStore(repoBaseDir, defaultTargetsCache); } - public static MutableTufStore newFileSystemStore(Path repoBaseDir, Path targetsCache) { + public static FileSystemTufStore newFileSystemStore(Path repoBaseDir, Path targetsCache) { if (!Files.isDirectory(repoBaseDir)) { throw new IllegalArgumentException(repoBaseDir + " must be a file system directory."); } @@ -65,50 +65,26 @@ public static MutableTufStore newFileSystemStore(Path repoBaseDir, Path targetsC @Override public String getIdentifier() { - return repoBaseDir.toAbsolutePath().toString(); + return "Meta: " + repoBaseDir.toAbsolutePath() + ", Targets:" + targetsCache.toAbsolutePath(); } @Override - public Optional loadTrustedRoot() throws IOException { - return loadRole(RootRole.ROOT, Root.class); - } - - @Override - public Optional loadTimestamp() throws IOException { - return loadRole(RootRole.TIMESTAMP, Timestamp.class); - } - - @Override - public Optional loadSnapshot() throws IOException { - return loadRole(RootRole.SNAPSHOT, Snapshot.class); - } - - @Override - public Optional loadTargets() throws IOException { - return loadRole(RootRole.TARGETS, Targets.class); - } - - @Override - public Optional loadDelegatedTargets(String roleName) throws IOException { - return loadRole(roleName, Targets.class); - } - - @Override - public void storeTargetFile(String targetName, byte[] targetContents) throws IOException { + public void writeTarget(String targetName, byte[] targetContents) throws IOException { Files.write(targetsCache.resolve(targetName), targetContents); } @Override - public byte[] getTargetFile(String targetName) throws IOException { + public byte[] readTarget(String targetName) throws IOException { return Files.readAllBytes(targetsCache.resolve(targetName)); } @Override - public void storeMeta(String roleName, SignedTufMeta meta) throws IOException { + public void setMeta(String roleName, SignedTufMeta meta) throws IOException { storeRole(roleName, meta); } - > Optional loadRole(String roleName, Class tClass) + @Override + public > Optional findMeta(String roleName, Class tClass) throws IOException { Path roleFile = repoBaseDir.resolve(roleName + ".json"); if (!roleFile.toFile().exists()) { @@ -125,8 +101,8 @@ > void storeRole(String roleName, T role) throws IOEx } @Override - public void storeTrustedRoot(Root root) throws IOException { - Optional trustedRoot = loadTrustedRoot(); + public void setRoot(Root root) throws IOException { + Optional trustedRoot = findMeta(RootRole.ROOT, Root.class); if (trustedRoot.isPresent()) { try { Files.move( diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java new file mode 100644 index 00000000..3752ecf8 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java @@ -0,0 +1,36 @@ +/* + * 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 dev.sigstore.tuf.model.SignedTufMeta; +import dev.sigstore.tuf.model.TufMeta; +import java.io.IOException; +import java.util.Optional; + +public interface MetaReader { + + /** + * Return a named metadata item if there is any. + * + * @param roleName the name of the role to load (root, timestamp, snapshot, targets, or a + * delegated target role) + * @param tClass the class type + * @return an instance of the signed metadata for the role if it was found + * @throws IOException if an error occurs reading from the backing store + */ + > Optional findMeta( + String roleName, Class tClass) throws IOException; +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MutableTufStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java similarity index 73% rename from sigstore-java/src/main/java/dev/sigstore/tuf/MutableTufStore.java rename to sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java index 319eb55d..ca12208b 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/MutableTufStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.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,28 +15,25 @@ */ package dev.sigstore.tuf; -import dev.sigstore.tuf.model.*; +import dev.sigstore.tuf.model.Root; +import dev.sigstore.tuf.model.SignedTufMeta; +import dev.sigstore.tuf.model.TufMeta; import java.io.IOException; -/** Defines the set of actions needed to support a local repository of TUF metadata. */ -public interface MutableTufStore extends TufStore { - /** - * Writes a TUF target to the local target store. - * - * @param targetName the name of the target file to write (e.g. ctfe.pub) - * @param targetContents the content of the target file as bytes - * @throws IOException if an error occurs - */ - void storeTargetFile(String targetName, byte[] targetContents) throws IOException; +/** Interface that defined a mutable meta store functionality. */ +public interface MetaStore extends MetaReader { + + String getIdentifier(); /** - * Generic method to store one of the {@link SignedTufMeta} resources in the local tuf store. + * Generic method to store one of the {@link SignedTufMeta} resources in the local tuf store. Do + * not use for Root role, use {@link #setRoot(Root)} instead. * * @param roleName the name of the role * @param meta the metadata to store * @throws IOException if writing the resource causes an IO error */ - void storeMeta(String roleName, SignedTufMeta meta) throws IOException; + void setMeta(String roleName, SignedTufMeta meta) throws IOException; /** * Once you have ascertained that your root is trustworthy use this method to persist it to your @@ -49,7 +46,7 @@ public interface MutableTufStore extends TufStore { * @see 5.3.8 */ - void storeTrustedRoot(Root root) throws IOException; + void setRoot(Root root) throws IOException; /** * This clears out the snapshot and timestamp metadata from the store, as required when snapshot diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java new file mode 100644 index 00000000..a24e9d75 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java @@ -0,0 +1,85 @@ +/* + * 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 dev.sigstore.tuf.model.Root; +import dev.sigstore.tuf.model.RootRole; +import dev.sigstore.tuf.model.SignedTufMeta; +import dev.sigstore.tuf.model.TufMeta; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** An in memory cache that will pass through to a provided local tuf store. */ +public class PassthroughCacheMetaStore implements MetaReader, MetaStore { + private final MetaStore localStore; + private final Map> cache; + + private PassthroughCacheMetaStore(MetaStore localStore) { + this.localStore = localStore; + this.cache = new HashMap<>(); + } + + @Override + public String getIdentifier() { + return "In memory cache backed by: " + localStore.getIdentifier(); + } + + public static PassthroughCacheMetaStore newPassthroughMetaCache(MetaStore localStore) { + return new PassthroughCacheMetaStore(localStore); + } + + @Override + public void setRoot(Root root) throws IOException { + // call storeRoot instead of generic storeMeta because it does extra work when storing on disk + localStore.setRoot(root); + cache.put(RootRole.ROOT, root); + } + + @Override + @SuppressWarnings("unchecked") + public > Optional findMeta( + String roleName, Class tClass) throws IOException { + // check memory cache + if (cache.containsKey(roleName)) { + return Optional.of((T) cache.get(roleName)); + } + + // check backing storage and write to memory if found + var value = localStore.findMeta(roleName, tClass); + value.ifPresent(v -> cache.put(roleName, v)); + + return value; + } + + @Override + public void setMeta(String roleName, SignedTufMeta meta) throws IOException { + if (Objects.equals(roleName, RootRole.ROOT)) { + throw new IllegalArgumentException("Calling setMeta on root instead of setRoot"); + } + localStore.setMeta(roleName, meta); + cache.put(roleName, meta); + } + + @Override + public void clearMetaDueToKeyRotation() throws IOException { + localStore.clearMetaDueToKeyRotation(); + cache.remove(RootRole.TIMESTAMP); + cache.remove(RootRole.SNAPSHOT); + } +} 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 b3126b99..c94f7df1 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java @@ -130,14 +130,18 @@ public SigstoreTufClient build() throws IOException { remoteMirror.toString().endsWith("/") ? remoteMirror : new URL(remoteMirror.toExternalForm() + "/"); - var targetsLocation = new URL(normalizedRemoteMirror.toExternalForm() + "targets"); + var remoteTargetsLocation = new URL(normalizedRemoteMirror.toExternalForm() + "targets"); + var filesystemTufStore = FileSystemTufStore.newFileSystemStore(tufCacheLocation); var tufUpdater = Updater.builder() .setTrustedRootPath(trustedRoot) - .setLocalStore(FileSystemTufStore.newFileSystemStore(tufCacheLocation)) + .setTrustedMetaStore( + TrustedMetaStore.newTrustedMetaStore( + PassthroughCacheMetaStore.newPassthroughMetaCache(filesystemTufStore))) + .setTargetStore(filesystemTufStore) .setMetaFetcher( MetaFetcher.newFetcher(HttpFetcher.newFetcher(normalizedRemoteMirror))) - .setTargetFetcher(HttpFetcher.newFetcher(targetsLocation)) + .setTargetFetcher(HttpFetcher.newFetcher(remoteTargetsLocation)) .build(); return new SigstoreTufClient(tufUpdater, cacheValidity); } @@ -166,7 +170,7 @@ public void forceUpdate() JsonFormat.parser() .merge( new String( - updater.getLocalStore().getTargetFile(TRUST_ROOT_FILENAME), StandardCharsets.UTF_8), + updater.getTargetStore().readTarget(TRUST_ROOT_FILENAME), StandardCharsets.UTF_8), trustedRootBuilder); sigstoreTrustedRoot = SigstoreTrustedRoot.from(trustedRootBuilder.build()); } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java new file mode 100644 index 00000000..ba336942 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java @@ -0,0 +1,30 @@ +/* + * 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 TargetReader { + + /** + * Reads a TUF target file from the local TUF store + * + * @param targetName the name of the target file to read (e.g. ctfe.pub) + * @return the content of the file as bytes + * @throws IOException if an error occurs + */ + byte[] readTarget(String targetName) throws IOException; +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java new file mode 100644 index 00000000..9ffc2f55 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** Interface that defined a mutable meta store functionality. */ +public interface TargetStore extends TargetReader { + + String getIdentifier(); + + /** + * Writes a TUF target to the local target store. + * + * @param targetName the name of the target file to write (e.g. ctfe.pub) + * @param targetContents the content of the target file as bytes + * @throws IOException if an error occurs + */ + void writeTarget(String targetName, byte[] targetContents) throws IOException; +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java deleted file mode 100644 index cd737d6e..00000000 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 dev.sigstore.tuf.model.Root; -import dev.sigstore.tuf.model.RootRole; -import dev.sigstore.tuf.model.Snapshot; -import dev.sigstore.tuf.model.Targets; -import dev.sigstore.tuf.model.Timestamp; -import java.io.IOException; -import java.util.Optional; - -// An in memory cache that will pass through to a provided local tuf store -class TrustedMeta { - private final MutableTufStore localStore; - private Root root; - private Snapshot snapshot; - private Timestamp timestamp; - private Targets targets; - - private TrustedMeta(MutableTufStore localStore) { - this.localStore = localStore; - } - - static TrustedMeta newTrustedMeta(MutableTufStore localStore) { - return new TrustedMeta(localStore); - } - - public void setRoot(Root root) throws IOException { - // call storeTrustedRoot instead of generic storeMeta because it does doesn't extra work - localStore.storeTrustedRoot(root); - this.root = root; - } - - public Root getRoot() throws IOException { - return findRoot().orElseThrow(() -> new IllegalStateException("No cached root to load")); - } - - public Optional findRoot() throws IOException { - if (root == null) { - root = localStore.loadTrustedRoot().orElse(null); - } - return Optional.ofNullable(root); - } - - public void setTimestamp(Timestamp timestamp) throws IOException { - localStore.storeMeta(RootRole.TIMESTAMP, timestamp); - this.timestamp = timestamp; - } - - public Timestamp getTimestamp() throws IOException { - return findTimestamp() - .orElseThrow(() -> new IllegalStateException("No cached timestamp to load")); - } - - public Optional findTimestamp() throws IOException { - if (timestamp == null) { - timestamp = localStore.loadTimestamp().orElse(null); - } - return Optional.ofNullable(timestamp); - } - - public void setSnapshot(Snapshot snapshot) throws IOException { - localStore.storeMeta(RootRole.SNAPSHOT, snapshot); - this.snapshot = snapshot; - } - - public Snapshot getSnapshot() throws IOException { - return findSnapshot() - .orElseThrow(() -> new IllegalStateException("No cached snapshot to load")); - } - - public Optional findSnapshot() throws IOException { - if (snapshot == null) { - snapshot = localStore.loadSnapshot().orElse(null); - } - return Optional.ofNullable(snapshot); - } - - public void setTargets(Targets targets) throws IOException { - localStore.storeMeta(RootRole.TARGETS, targets); - this.targets = targets; - } - - public Targets getTargets() throws IOException { - return findTargets().orElseThrow(() -> new IllegalStateException("No cached targets to load")); - } - - public Optional findTargets() throws IOException { - if (targets == null) { - targets = localStore.loadTargets().orElse(null); - } - return Optional.ofNullable(targets); - } - - public void clearMetaDueToKeyRotation() throws IOException { - localStore.clearMetaDueToKeyRotation(); - timestamp = null; - snapshot = null; - } -} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java new file mode 100644 index 00000000..6352734a --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java @@ -0,0 +1,123 @@ +/* + * Copyright 2023 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 dev.sigstore.tuf.model.RootRole; +import dev.sigstore.tuf.model.SignedTufMeta; +import dev.sigstore.tuf.model.Snapshot; +import dev.sigstore.tuf.model.Targets; +import dev.sigstore.tuf.model.Timestamp; +import dev.sigstore.tuf.model.TufMeta; +import java.io.IOException; +import java.util.Optional; + +/** Local storage for local state of TUF metadata. */ +public class TrustedMetaStore { + + private final MetaStore metaStore; + + private TrustedMetaStore(MetaStore metaStore) { + this.metaStore = metaStore; + } + + public static TrustedMetaStore newTrustedMetaStore(MetaStore metaStore) { + return new TrustedMetaStore(metaStore); + } + + /** + * A generic string for identifying the local store in debug messages. A file system based + * implementation might return the path being used for storage, while an in-memory store may just + * return something like 'in-memory'. + */ + public String getIdentifier() { + return metaStore.getIdentifier(); + } + + /** + * Return a named metadata item. Fail if there isn't any + * + * @param roleName the name of the role to load (root, timestamp, snapshot, targets, or a + * delegated target role) + * @param tClass the class type + * @return an instance of the signed metadata for the role if it was found + * @throws IOException if an error occurs reading from the backing store + * @throws IllegalStateException if the data was never persisted and this function was called + */ + > T getMeta(String roleName, Class tClass) + throws IOException { + return metaStore + .findMeta(roleName, tClass) + .orElseThrow( + () -> + new IllegalStateException( + "No cached " + + roleName + + " to load. This error may occur when (1) update hasn't been called or (2) when find should have been used instead of get.")); + } + + public void setRoot(Root root) throws IOException { + metaStore.setRoot(root); + } + + public Root getRoot() throws IOException { + return getMeta(RootRole.ROOT, Root.class); + } + + public Optional findRoot() throws IOException { + return metaStore.findMeta(RootRole.ROOT, Root.class); + } + + public void setTimestamp(Timestamp timestamp) throws IOException { + metaStore.setMeta(RootRole.TIMESTAMP, timestamp); + } + + public Timestamp getTimestamp() throws IOException { + return getMeta(RootRole.TIMESTAMP, Timestamp.class); + } + + public Optional findTimestamp() throws IOException { + return metaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class); + } + + public void setSnapshot(Snapshot snapshot) throws IOException { + metaStore.setMeta(RootRole.SNAPSHOT, snapshot); + } + + public Snapshot getSnapshot() throws IOException { + return getMeta(RootRole.SNAPSHOT, Snapshot.class); + } + + public Optional findSnapshot() throws IOException { + return metaStore.findMeta(RootRole.SNAPSHOT, Snapshot.class); + } + + public void setTargets(Targets targets) throws IOException { + metaStore.setMeta(RootRole.TARGETS, targets); + } + + public Targets getTargets() throws IOException { + return getMeta(RootRole.TARGETS, Targets.class); + } + + public Optional findTargets() throws IOException { + return metaStore.findMeta(RootRole.TARGETS, Targets.class); + } + + public void clearMetaDueToKeyRotation() throws IOException { + metaStore.clearMetaDueToKeyRotation(); + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TufStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TufStore.java deleted file mode 100644 index c990d32b..00000000 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TufStore.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 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 dev.sigstore.tuf.model.Snapshot; -import dev.sigstore.tuf.model.Targets; -import dev.sigstore.tuf.model.Timestamp; -import java.io.IOException; -import java.util.Optional; - -public interface TufStore { - /** - * A generic string for identifying the local store in debug messages. A file system based - * implementation might return the path being used for storage, while an in-memory store may just - * return something like 'in-memory'. - */ - String getIdentifier(); - - /** Local store must have a root that has been blessed safe. */ - Optional loadTrustedRoot() throws IOException; - - /** Return local trusted timestamp metadata if there is any. */ - Optional loadTimestamp() throws IOException; - - /** Return the local trusted snapshot metadata if there is any. */ - Optional loadSnapshot() throws IOException; - - /** Return the local trusted targets metadata if there is any. */ - Optional loadTargets() throws IOException; - - /** Return a named local delegated targets metadata if there is any. */ - Optional loadDelegatedTargets(String roleName) throws IOException; - - /** - * Reads a TUF target file from the local TUF store - * - * @param targetName the name of the target file to read (e.g. ctfe.pub) - * @return the content of the file as bytes - * @throws IOException if an error occurs - */ - byte[] getTargetFile(String targetName) throws IOException; -} 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 c2580b52..f266dbd1 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java @@ -63,12 +63,12 @@ public class Updater { private final MetaFetcher metaFetcher; private final Fetcher targetFetcher; private final RootProvider trustedRootPath; - // TODO: this should be replaced by a dedicated target store - private final MutableTufStore localStore; + + private final TrustedMetaStore trustedMetaStore; + private final TargetStore targetStore; // Mutable State private ZonedDateTime updateStartTime; - private TrustedMeta trustedMeta; Updater( Clock clock, @@ -76,14 +76,15 @@ public class Updater { MetaFetcher metaFetcher, Fetcher targetFetcher, RootProvider trustedRootPath, - MutableTufStore localStore) { + TrustedMetaStore trustedMetaStore, + TargetStore targetStore) { this.clock = clock; this.verifiers = verifiers; this.trustedRootPath = trustedRootPath; - this.localStore = localStore; this.metaFetcher = metaFetcher; this.targetFetcher = targetFetcher; - this.trustedMeta = TrustedMeta.newTrustedMeta(localStore); + this.trustedMetaStore = trustedMetaStore; + this.targetStore = targetStore; } public static Builder builder() { @@ -93,16 +94,16 @@ public static Builder builder() { public void update() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { updateMeta(); - downloadTargets(trustedMeta.getTargets()); + downloadTargets(trustedMetaStore.getTargets()); } void updateMeta() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { updateRoot(); - var oldTimestamp = trustedMeta.findTimestamp(); + var oldTimestamp = trustedMetaStore.findTimestamp(); updateTimestamp(); - if (Objects.equals(oldTimestamp.orElse(null), trustedMeta.getTimestamp()) - && trustedMeta.findSnapshot().isPresent() - && trustedMeta.findTargets().isPresent()) { + if (Objects.equals(oldTimestamp.orElse(null), trustedMetaStore.getTimestamp()) + && trustedMetaStore.findSnapshot().isPresent() + && trustedMetaStore.findTargets().isPresent()) { return; } // if we need to update or we can't find targets/timestamps locally then grab new snapshot and @@ -121,7 +122,7 @@ void updateRoot() // 5.3.2) load the trust metadata file (root.json), get version of root.json and the role // signature threshold value - Optional localRoot = trustedMeta.findRoot(); + Optional localRoot = trustedMetaStore.findRoot(); Root trustedRoot; if (localRoot.isPresent()) { trustedRoot = localRoot.get(); @@ -162,7 +163,7 @@ void updateRoot() // 5.3.7) set the trusted root metadata to the new root trustedRoot = newRoot; // 5.3.8) persist to repo - trustedMeta.setRoot(trustedRoot); + trustedMetaStore.setRoot(trustedRoot); // 5.3.9) see if there are more versions go back 5.3.3 nextVersion++; } @@ -178,9 +179,9 @@ void updateRoot() || hasNewKeys( preUpdateTimestampRole, trustedRoot.getSignedMeta().getRoles().get(RootRole.TIMESTAMP))) { - trustedMeta.clearMetaDueToKeyRotation(); + trustedMetaStore.clearMetaDueToKeyRotation(); } - trustedMeta.setRoot(trustedRoot); + trustedMetaStore.setRoot(trustedRoot); } private void throwIfExpired(ZonedDateTime expires) { @@ -290,12 +291,12 @@ void updateTimestamp() .getMetaResource(); // 2) verify against threshold of keys as specified in trusted root.json - verifyDelegate(trustedMeta.getRoot(), timestamp); + verifyDelegate(trustedMetaStore.getRoot(), timestamp); // 3) If the new timestamp file has a lesser version than our current trusted timestamp file // report a rollback attack. If it is equal, just return the original timestamp there should // be no changes. If it is higher than continue update. - Optional localTimestampMaybe = trustedMeta.findTimestamp(); + Optional localTimestampMaybe = trustedMetaStore.findTimestamp(); if (localTimestampMaybe.isPresent()) { Timestamp localTimestamp = localTimestampMaybe.get(); if (localTimestamp.getSignedMeta().getVersion() > timestamp.getSignedMeta().getVersion()) { @@ -303,14 +304,14 @@ void updateTimestamp() localTimestamp.getSignedMeta().getVersion(), timestamp.getSignedMeta().getVersion()); } if (localTimestamp.getSignedMeta().getVersion() == timestamp.getSignedMeta().getVersion()) { - trustedMeta.setTimestamp(localTimestamp); + trustedMetaStore.setTimestamp(localTimestamp); return; } } // 4) check expiration timestamp is after tuf update start time, else fail. throwIfExpired(timestamp.getSignedMeta().getExpiresAsDate()); // 5) persist timestamp.json - trustedMeta.setTimestamp(timestamp); + trustedMetaStore.setTimestamp(timestamp); } void updateSnapshot() @@ -318,13 +319,13 @@ void updateSnapshot() SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException { // 1) download the snapshot.json bytes up to timestamp's snapshot length. int timestampSnapshotVersion = - trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getVersion(); + trustedMetaStore.getTimestamp().getSignedMeta().getSnapshotMeta().getVersion(); var snapshotResult = metaFetcher.getMeta( RootRole.SNAPSHOT, timestampSnapshotVersion, Snapshot.class, - trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getLengthOrDefault()); + trustedMetaStore.getTimestamp().getSignedMeta().getSnapshotMeta().getLengthOrDefault()); if (snapshotResult.isEmpty()) { throw new FileNotFoundException( timestampSnapshotVersion + ".snapshot.json", metaFetcher.getSource()); @@ -332,14 +333,14 @@ void updateSnapshot() // 2) check against timestamp.snapshot.hash, this is optional, the fallback is // that the version must match, which is handled in (4). var snapshot = snapshotResult.get(); - if (trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().isPresent()) { + if (trustedMetaStore.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().isPresent()) { verifyHashes( "snapshot", snapshot.getRawBytes(), - trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().get()); + trustedMetaStore.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().get()); } // 3) Check against threshold of root signing keys, else fail - verifyDelegate(trustedMeta.getRoot(), snapshot.getMetaResource()); + verifyDelegate(trustedMetaStore.getRoot(), snapshot.getMetaResource()); // 4) Check snapshot.version matches timestamp.snapshot.version, else fail. int snapshotVersion = snapshot.getMetaResource().getSignedMeta().getVersion(); if (snapshotVersion != timestampSnapshotVersion) { @@ -348,7 +349,7 @@ void updateSnapshot() // 5) Ensure all targets and delegated targets in the trusted (old) snapshots file have versions // which are less than or equal to the equivalent target in the new file. Check that no targets // are missing in new file. Else fail. - var trustedSnapshotMaybe = trustedMeta.findSnapshot(); + var trustedSnapshotMaybe = trustedMetaStore.findSnapshot(); if (trustedSnapshotMaybe.isPresent()) { var trustedSnapshot = trustedSnapshotMaybe.get(); for (Map.Entry trustedTargetEntry : @@ -370,7 +371,7 @@ void updateSnapshot() // 6) Ensure expiration timestamp of snapshot is later than tuf update start time. throwIfExpired(snapshot.getMetaResource().getSignedMeta().getExpiresAsDate()); // 7) persist snapshot. - trustedMeta.setSnapshot(snapshot.getMetaResource()); + trustedMetaStore.setSnapshot(snapshot.getMetaResource()); } // this method feels very wrong. I would not show it to a friend. @@ -408,7 +409,7 @@ void updateTargets() FileExceedsMaxLengthException { // 1) download the targets.json up to targets.json length in bytes. SnapshotMeta.SnapshotTarget targetMeta = - trustedMeta.getSnapshot().getSignedMeta().getTargetMeta("targets.json"); + trustedMetaStore.getSnapshot().getSignedMeta().getTargetMeta("targets.json"); var targetsResultMaybe = metaFetcher.getMeta( RootRole.TARGETS, @@ -429,7 +430,7 @@ void updateTargets() targetMeta.getHashes().get()); } // 3) check against threshold of keys as specified by trusted root.json - verifyDelegate(trustedMeta.getRoot(), targetsResult.getMetaResource()); + verifyDelegate(trustedMetaStore.getRoot(), targetsResult.getMetaResource()); // 4) check targets.version == snapshot.targets.version, else fail. int targetsVersion = targetsResult.getMetaResource().getSignedMeta().getVersion(); int snapshotTargetsVersion = targetMeta.getVersion(); @@ -440,7 +441,7 @@ void updateTargets() throwIfExpired(targetsResult.getMetaResource().getSignedMeta().getExpiresAsDate()); // 6) persist targets metadata // why do we persist the - trustedMeta.setTargets(targetsResult.getMetaResource()); + trustedMetaStore.setTargets(targetsResult.getMetaResource()); } void downloadTargets(Targets targets) @@ -474,18 +475,18 @@ void downloadTargets(Targets targets) // when persisting targets use the targetname without sha512 prefix // https://theupdateframework.github.io/specification/latest/index.html#fetch-target - localStore.storeTargetFile(targetName, targetBytes); + targetStore.writeTarget(targetName, targetBytes); } } @VisibleForTesting - MutableTufStore getLocalStore() { - return localStore; + TargetStore getTargetStore() { + return targetStore; } @VisibleForTesting - TrustedMeta getTrustedMeta() { - return trustedMeta; + TrustedMetaStore getMetaStore() { + return trustedMetaStore; } public static class Builder { @@ -495,7 +496,8 @@ public static class Builder { private MetaFetcher metaFetcher; private Fetcher targetFetcher; private RootProvider trustedRootPath; - private MutableTufStore localStore; + private TrustedMetaStore trustedMetaStore; + private TargetStore targetStore; public Builder setClock(Clock clock) { this.clock = clock; @@ -507,8 +509,13 @@ public Builder setVerifiers(Verifiers.Supplier verifiers) { return this; } - public Builder setLocalStore(MutableTufStore store) { - this.localStore = store; + public Builder setTrustedMetaStore(TrustedMetaStore trustedMetaStore) { + this.trustedMetaStore = trustedMetaStore; + return this; + } + + public Builder setTargetStore(TargetStore targetStore) { + this.targetStore = targetStore; return this; } @@ -528,7 +535,14 @@ public Builder setTargetFetcher(Fetcher fetcher) { } public Updater build() { - return new Updater(clock, verifiers, metaFetcher, targetFetcher, trustedRootPath, localStore); + return new Updater( + clock, + verifiers, + metaFetcher, + targetFetcher, + trustedRootPath, + trustedMetaStore, + targetStore); } } } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java index cae36015..16f227e4 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java @@ -18,6 +18,8 @@ import static org.junit.jupiter.api.Assertions.*; import dev.sigstore.testkit.tuf.TestResources; +import dev.sigstore.tuf.model.Root; +import dev.sigstore.tuf.model.RootRole; import java.io.IOException; import java.nio.file.Path; import org.junit.jupiter.api.Test; @@ -29,22 +31,22 @@ class FileSystemTufStoreTest { @Test void newFileSystemStore_empty(@TempDir Path repoBase) throws IOException { - MutableTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); - assertFalse(tufStore.loadTrustedRoot().isPresent()); + FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); + assertFalse(tufStore.findMeta(RootRole.ROOT, Root.class).isPresent()); } @Test void newFileSystemStore_hasRepo(@TempDir Path repoBase) throws IOException { TestResources.setupRepoFiles(PROD_REPO, repoBase, "root.json"); - MutableTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); - assertTrue(tufStore.loadTrustedRoot().isPresent()); + FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); + assertTrue(tufStore.findMeta(RootRole.ROOT, Root.class).isPresent()); } @Test void setTrustedRoot_noPrevious(@TempDir Path repoBase) throws IOException { - MutableTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); + FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); assertFalse(repoBase.resolve("root.json").toFile().exists()); - tufStore.storeTrustedRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); + tufStore.setRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); assertEquals(2, repoBase.toFile().list().length, "Expect 2: root.json plus the /targets dir."); assertTrue(repoBase.resolve("root.json").toFile().exists()); assertTrue(repoBase.resolve("targets").toFile().isDirectory()); @@ -53,17 +55,17 @@ void setTrustedRoot_noPrevious(@TempDir Path repoBase) throws IOException { @Test void setTrustedRoot_backupPerformed(@TempDir Path repoBase) throws IOException { TestResources.setupRepoFiles(PROD_REPO, repoBase, "root.json"); - MutableTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); - int version = tufStore.loadTrustedRoot().get().getSignedMeta().getVersion(); + FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); + int version = tufStore.findMeta(RootRole.ROOT, Root.class).get().getSignedMeta().getVersion(); assertFalse(repoBase.resolve(version + ".root.json").toFile().exists()); - tufStore.storeTrustedRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); + tufStore.setRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); assertTrue(repoBase.resolve(version + ".root.json").toFile().exists()); } @Test void clearMetaDueToKeyRotation(@TempDir Path repoBase) throws IOException { TestResources.setupRepoFiles(PROD_REPO, repoBase, "snapshot.json", "timestamp.json"); - MutableTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); + FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); assertTrue(repoBase.resolve("snapshot.json").toFile().exists()); assertTrue(repoBase.resolve("timestamp.json").toFile().exists()); tufStore.clearMetaDueToKeyRotation(); diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java new file mode 100644 index 00000000..868789e6 --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java @@ -0,0 +1,114 @@ +/* + * 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 static dev.sigstore.json.GsonSupplier.GSON; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.io.Resources; +import dev.sigstore.tuf.model.Root; +import dev.sigstore.tuf.model.RootRole; +import dev.sigstore.tuf.model.Timestamp; +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PassthroughCacheMetaStoreTest { + + @TempDir Path localStore; + private FileSystemTufStore fileSystemTufStore; + private PassthroughCacheMetaStore passthroughCacheMetaStore; + + private static Root root; + private static Timestamp timestamp; + + @BeforeAll + public static void readAllMeta() throws IOException, URISyntaxException { + Path rootResource = + Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/root.json").getPath()); + root = GSON.get().fromJson(Files.newBufferedReader(rootResource), Root.class); + Path timestampResource = + Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/timestamp.json").getPath()); + timestamp = GSON.get().fromJson(Files.newBufferedReader(timestampResource), Timestamp.class); + } + + @BeforeEach + public void setup() throws IOException { + fileSystemTufStore = FileSystemTufStore.newFileSystemStore(localStore); + passthroughCacheMetaStore = + PassthroughCacheMetaStore.newPassthroughMetaCache(fileSystemTufStore); + } + + @Test + public void root_test() throws Exception { + assertTrue(fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); + + passthroughCacheMetaStore.setRoot(root); + + assertEquals(root, fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).get()); + assertEquals(root, passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).get()); + } + + @Test + public void root_canInitFromDisk() throws Exception { + assertTrue(fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); + + try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("root.json"))) { + GSON.get().toJson(root, fileWriter); + } + + assertEquals(root, fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).get()); + assertEquals(root, passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).get()); + } + + @Test + public void meta_test() throws Exception { + // root uses special handling for writing, but the rest of them don't, so we just test + // timestamp here arbitrarily + assertTrue(fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + + passthroughCacheMetaStore.setMeta(RootRole.TIMESTAMP, timestamp); + + assertEquals(timestamp, fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + assertEquals( + timestamp, passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + } + + @Test + public void timestamp_canInitFromDisk() throws Exception { + assertTrue(fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + + try (BufferedWriter fileWriter = + Files.newBufferedWriter(localStore.resolve("timestamp.json"))) { + GSON.get().toJson(timestamp, fileWriter); + } + + assertEquals(timestamp, fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + assertEquals( + timestamp, passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + } +} diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java index 776cd888..9d3062cf 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java @@ -89,10 +89,10 @@ private static Updater mockUpdater() throws IOException { var trustRootBytes = JsonFormat.printer().print(TrustedRoot.newBuilder()).getBytes(StandardCharsets.UTF_8); var mockUpdater = Mockito.mock(Updater.class); - var mockTufStore = Mockito.mock(MutableTufStore.class); - Mockito.when(mockTufStore.getTargetFile(SigstoreTufClient.TRUST_ROOT_FILENAME)) + var mockTargetStore = Mockito.mock(TargetStore.class); + Mockito.when(mockTargetStore.readTarget(SigstoreTufClient.TRUST_ROOT_FILENAME)) .thenReturn(trustRootBytes); - Mockito.when(mockUpdater.getLocalStore()).thenReturn(mockTufStore); + Mockito.when(mockUpdater.getTargetStore()).thenReturn(mockTargetStore); return mockUpdater; } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java deleted file mode 100644 index 57e47a4e..00000000 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/TrustedMetaTest.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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 static dev.sigstore.json.GsonSupplier.GSON; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.common.io.Resources; -import dev.sigstore.tuf.model.Root; -import dev.sigstore.tuf.model.Snapshot; -import dev.sigstore.tuf.model.Targets; -import dev.sigstore.tuf.model.Timestamp; -import java.io.BufferedWriter; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class TrustedMetaTest { - - @TempDir Path localStore; - private MutableTufStore tufStore; - private TrustedMeta trustedMeta; - - private static Root root; - private static Timestamp timestamp; - private static Snapshot snapshot; - private static Targets targets; - - @BeforeAll - public static void readAllMeta() throws IOException, URISyntaxException { - Path rootResource = - Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/root.json").getPath()); - root = GSON.get().fromJson(Files.newBufferedReader(rootResource), Root.class); - Path timestampResource = - Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/timestamp.json").getPath()); - timestamp = GSON.get().fromJson(Files.newBufferedReader(timestampResource), Timestamp.class); - Path snapshotResource = - Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/snapshot.json").getPath()); - snapshot = GSON.get().fromJson(Files.newBufferedReader(snapshotResource), Snapshot.class); - Path targetsResource = - Path.of(Resources.getResource("dev/sigstore/tuf/real/prod/targets.json").getPath()); - targets = GSON.get().fromJson(Files.newBufferedReader(targetsResource), Targets.class); - } - - @BeforeEach - public void setup() throws IOException { - tufStore = FileSystemTufStore.newFileSystemStore(localStore); - trustedMeta = TrustedMeta.newTrustedMeta(tufStore); - } - - @Test - public void root_test() throws Exception { - assertTrue(tufStore.loadTrustedRoot().isEmpty()); - assertTrue(trustedMeta.findRoot().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getRoot); - - trustedMeta.setRoot(root); - - assertTrue(tufStore.loadTrustedRoot().isPresent()); - assertTrue(trustedMeta.findRoot().isPresent()); - Assertions.assertEquals(root, trustedMeta.getRoot()); - } - - @Test - public void root_canInitFromDisk() throws Exception { - assertTrue(tufStore.loadTrustedRoot().isEmpty()); - assertTrue(trustedMeta.findRoot().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getRoot); - - try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("root.json"))) { - GSON.get().toJson(root, fileWriter); - } - - assertTrue(tufStore.loadTrustedRoot().isPresent()); - assertTrue(trustedMeta.findRoot().isPresent()); - Assertions.assertEquals(root, trustedMeta.getRoot()); - } - - @Test - public void timestamp_test() throws Exception { - assertTrue(tufStore.loadTimestamp().isEmpty()); - assertTrue(trustedMeta.findTimestamp().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTimestamp); - - trustedMeta.setTimestamp(timestamp); - - assertTrue(tufStore.loadTimestamp().isPresent()); - assertTrue(trustedMeta.findTimestamp().isPresent()); - Assertions.assertEquals(timestamp, trustedMeta.getTimestamp()); - } - - @Test - public void timestamp_canInitFromDisk() throws Exception { - assertTrue(tufStore.loadTimestamp().isEmpty()); - assertTrue(trustedMeta.findTimestamp().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTimestamp); - - try (BufferedWriter fileWriter = - Files.newBufferedWriter(localStore.resolve("timestamp.json"))) { - GSON.get().toJson(timestamp, fileWriter); - } - - assertTrue(tufStore.loadTimestamp().isPresent()); - assertTrue(trustedMeta.findTimestamp().isPresent()); - Assertions.assertEquals(timestamp, trustedMeta.getTimestamp()); - } - - @Test - public void snapshot_test() throws Exception { - assertTrue(tufStore.loadSnapshot().isEmpty()); - assertTrue(trustedMeta.findSnapshot().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getSnapshot); - - trustedMeta.setSnapshot(snapshot); - - assertTrue(tufStore.loadSnapshot().isPresent()); - assertTrue(trustedMeta.findSnapshot().isPresent()); - Assertions.assertEquals(snapshot, trustedMeta.getSnapshot()); - } - - @Test - public void snapshot_canInitFromDisk() throws Exception { - assertTrue(tufStore.loadSnapshot().isEmpty()); - assertTrue(trustedMeta.findSnapshot().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getSnapshot); - - try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("snapshot.json"))) { - GSON.get().toJson(snapshot, fileWriter); - } - - assertTrue(tufStore.loadSnapshot().isPresent()); - assertTrue(trustedMeta.findSnapshot().isPresent()); - Assertions.assertEquals(snapshot, trustedMeta.getSnapshot()); - } - - @Test - public void targets_test() throws Exception { - assertTrue(tufStore.loadTargets().isEmpty()); - assertTrue(trustedMeta.findTargets().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTargets); - - trustedMeta.setTargets(targets); - - assertTrue(tufStore.loadTargets().isPresent()); - assertTrue(trustedMeta.findTargets().isPresent()); - Assertions.assertEquals(targets, trustedMeta.getTargets()); - } - - @Test - public void targets_canInitFromDisk() throws Exception { - assertTrue(tufStore.loadTargets().isEmpty()); - assertTrue(trustedMeta.findTargets().isEmpty()); - Assertions.assertThrows(IllegalStateException.class, trustedMeta::getTargets); - - try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("targets.json"))) { - GSON.get().toJson(targets, fileWriter); - } - - assertTrue(tufStore.loadTargets().isPresent()); - assertTrue(trustedMeta.findTargets().isPresent()); - Assertions.assertEquals(targets, trustedMeta.getTargets()); - } -} 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 2fa2903b..841e25c1 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java @@ -61,7 +61,6 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; -import java.util.Optional; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Pair; import org.eclipse.jetty.server.Server; @@ -293,7 +292,7 @@ public void testTimestampUpdate_noPreviousTimestamp_success() throws Exception { assertStoreContains("timestamp.json"); assertEquals( 3, - updater.getTrustedMeta().getTimestamp().getSignedMeta().getVersion(), + updater.getMetaStore().getTimestamp().getSignedMeta().getVersion(), "timestamp version did not match expectations"); } @@ -305,14 +304,14 @@ public void testTimestampUpdate_updateExistingTimestamp_success() throws Excepti var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertEquals( 1, - updater.getLocalStore().loadTimestamp().get().getSignedMeta().getVersion(), + updater.getMetaStore().getTimestamp().getSignedMeta().getVersion(), "timestamp version should start at 1 before the update."); updater.updateRoot(); updater.updateTimestamp(); assertStoreContains("timestamp.json"); assertEquals( 3, - updater.getTrustedMeta().getTimestamp().getSignedMeta().getVersion(), + updater.getMetaStore().getTimestamp().getSignedMeta().getVersion(), "timestamp version did not match expectations."); } @@ -484,7 +483,7 @@ public void testTargetsUpdate_success() createTimeStaticUpdater( localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2022-11-20T18:07:27Z"); updater.updateMeta(); - var localTargets = updater.getTrustedMeta().getTargets(); + var localTargets = updater.getMetaStore().getTargets(); assertNotNull(localTargets); var remoteTargets = GSON.get() @@ -526,7 +525,7 @@ public void testTargetsDownload_targetFileNotFound() updater.updateMeta(); assertThrows( FileNotFoundException.class, - () -> updater.downloadTargets(updater.getTrustedMeta().getTargets()), + () -> updater.downloadTargets(updater.getMetaStore().getTargets()), "the target file for download should be missing from the repo and cause an exception."); } @@ -544,7 +543,7 @@ public void testTargetsDownload_targetInvalidLength() updater.updateMeta(); assertThrows( FileExceedsMaxLengthException.class, - () -> updater.downloadTargets(updater.getTrustedMeta().getTargets()), + () -> updater.downloadTargets(updater.getMetaStore().getTargets()), "The target file is expected to not match the length specified in targets.json target data."); } @@ -562,7 +561,7 @@ public void testTargetsDownload_targetFileInvalidHash() updater.updateMeta(); assertThrows( InvalidHashesException.class, - () -> updater.downloadTargets(updater.getTrustedMeta().getTargets()), + () -> updater.downloadTargets(updater.getMetaStore().getTargets()), "The target file has been modified and should not match the expected hash"); } @@ -579,9 +578,9 @@ public void testTargetsDownload_success() throws Exception { "targets/53904bc6216230bf8da0ec42d34004a3f36764de698638641870e37d270e4fd13e1079285f8bca73c2857a279f6f7fbc82038274c3eb48ec5bb2da9b2e30491a.test2.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); updater.update(); - assertNotNull(updater.getLocalStore().getTargetFile("test.txt")); - assertNotNull(updater.getLocalStore().getTargetFile("test.txt.v2")); - assertNotNull(updater.getLocalStore().getTargetFile("test2.txt")); + assertNotNull(updater.getTargetStore().readTarget("test.txt")); + assertNotNull(updater.getTargetStore().readTarget("test.txt.v2")); + assertNotNull(updater.getTargetStore().readTarget("test2.txt")); } // Ensure we accept sha256 or sha512 on hashes for targets @@ -632,16 +631,15 @@ public void testUpdate_fromProdData() updater.update(); Root oldRoot = TestResources.loadRoot(UPDATER_REAL_TRUSTED_ROOT); - MutableTufStore localStore = updater.getLocalStore(); - Optional newRoot = localStore.loadTrustedRoot(); - assertTrue(newRoot.isPresent(), "trusted root should be present in the store"); - assertRootVersionIncreased(oldRoot, newRoot.get()); - Optional targets = localStore.loadTargets(); - assertTrue(targets.isPresent(), "a list of targets should be available in the store"); - Map targetsData = targets.get().getSignedMeta().getTargets(); + TrustedMetaStore metaStore = updater.getMetaStore(); + TargetStore targetStore = updater.getTargetStore(); + Root newRoot = metaStore.getRoot(); // should be present + assertRootVersionIncreased(oldRoot, newRoot); + Targets targets = metaStore.getTargets(); // should be present + Map targetsData = targets.getSignedMeta().getTargets(); for (String file : targetsData.keySet()) { TargetMeta.TargetData fileData = targetsData.get(file); - byte[] fileBytes = localStore.getTargetFile(file); + byte[] fileBytes = targetStore.readTarget(file); assertNotNull(fileBytes, "each file from targets data should be present"); assertEquals(fileData.getLength(), fileBytes.length, "file length should match metadata"); assertEquals( @@ -930,8 +928,8 @@ public void testUpdate_snapshotsAndTimestampHaveNoSizeAndNoHashesInMeta() throws updater.updateTimestamp(); updater.updateSnapshot(); - var timestamp = updater.getTrustedMeta().getTimestamp(); - var snapshot = updater.getTrustedMeta().getSnapshot(); + var timestamp = updater.getMetaStore().getTimestamp(); + var snapshot = updater.getMetaStore().getSnapshot(); Assertions.assertTrue(timestamp.getSignedMeta().getSnapshotMeta().getHashes().isEmpty()); Assertions.assertTrue(timestamp.getSignedMeta().getSnapshotMeta().getLength().isEmpty()); @@ -965,13 +963,17 @@ private static Updater createTimeStaticUpdater(Path localStore, Path trustedRoot @NotNull private static Updater createTimeStaticUpdater(Path localStore, Path trustedRootFile, String time) throws IOException { + var fsTufStore = FileSystemTufStore.newFileSystemStore(localStore); return Updater.builder() .setClock(Clock.fixed(Instant.parse(time), ZoneOffset.UTC)) .setVerifiers(Verifiers::newVerifier) .setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(new URL(remoteUrl)))) .setTargetFetcher(HttpFetcher.newFetcher(new URL(remoteUrl + "targets/"))) .setTrustedRootPath(RootProvider.fromFile(trustedRootFile)) - .setLocalStore(FileSystemTufStore.newFileSystemStore(localStore)) + .setTrustedMetaStore( + TrustedMetaStore.newTrustedMetaStore( + PassthroughCacheMetaStore.newPassthroughMetaCache(fsTufStore))) + .setTargetStore(fsTufStore) .build(); } From 33482eb55ebed0dd904f7450e852caca3f0491a2 Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Wed, 30 Oct 2024 12:56:17 -0400 Subject: [PATCH 3/3] updates Signed-off-by: Appu Goundan --- .../dev/sigstore/tuf/FileSystemTufStore.java | 8 ++--- .../java/dev/sigstore/tuf/MetaReader.java | 3 +- .../main/java/dev/sigstore/tuf/MetaStore.java | 13 ++++--- .../tuf/PassthroughCacheMetaStore.java | 16 ++++----- .../java/dev/sigstore/tuf/TargetReader.java | 1 + .../java/dev/sigstore/tuf/TargetStore.java | 7 +++- .../dev/sigstore/tuf/TrustedMetaStore.java | 18 +++++----- .../sigstore/tuf/FileSystemTufStoreTest.java | 10 +++--- .../tuf/PassthroughCacheMetaStoreTest.java | 36 +++++++++---------- 9 files changed, 62 insertions(+), 50 deletions(-) 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 7f904823..5fbbd06f 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java @@ -79,12 +79,12 @@ public byte[] readTarget(String targetName) throws IOException { } @Override - public void setMeta(String roleName, SignedTufMeta meta) throws IOException { + public void writeMeta(String roleName, SignedTufMeta meta) throws IOException { storeRole(roleName, meta); } @Override - public > Optional findMeta(String roleName, Class tClass) + public > Optional readMeta(String roleName, Class tClass) throws IOException { Path roleFile = repoBaseDir.resolve(roleName + ".json"); if (!roleFile.toFile().exists()) { @@ -101,8 +101,8 @@ > void storeRole(String roleName, T role) throws IOEx } @Override - public void setRoot(Root root) throws IOException { - Optional trustedRoot = findMeta(RootRole.ROOT, Root.class); + public void writeRoot(Root root) throws IOException { + Optional trustedRoot = readMeta(RootRole.ROOT, Root.class); if (trustedRoot.isPresent()) { try { Files.move( diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java index 3752ecf8..3863e485 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaReader.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Optional; +/** Interface that defines reading meta from local storage. */ public interface MetaReader { /** @@ -31,6 +32,6 @@ public interface MetaReader { * @return an instance of the signed metadata for the role if it was found * @throws IOException if an error occurs reading from the backing store */ - > Optional findMeta( + > Optional readMeta( String roleName, Class tClass) throws IOException; } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java index ca12208b..a3a4b8be 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java @@ -20,20 +20,25 @@ import dev.sigstore.tuf.model.TufMeta; import java.io.IOException; -/** Interface that defined a mutable meta store functionality. */ +/** Interface that defines a mutable meta store functionality. */ public interface MetaStore extends MetaReader { + /** + * A generic string for identifying the local store in debug messages. A file system based + * implementation might return the path being used for storage, while an in-memory store may just + * return something like 'in-memory'. + */ String getIdentifier(); /** * Generic method to store one of the {@link SignedTufMeta} resources in the local tuf store. Do - * not use for Root role, use {@link #setRoot(Root)} instead. + * not use for Root role, use {@link #writeRoot(Root)} instead. * * @param roleName the name of the role * @param meta the metadata to store * @throws IOException if writing the resource causes an IO error */ - void setMeta(String roleName, SignedTufMeta meta) throws IOException; + void writeMeta(String roleName, SignedTufMeta meta) throws IOException; /** * Once you have ascertained that your root is trustworthy use this method to persist it to your @@ -46,7 +51,7 @@ public interface MetaStore extends MetaReader { * @see 5.3.8 */ - void setRoot(Root root) throws IOException; + void writeRoot(Root root) throws IOException; /** * This clears out the snapshot and timestamp metadata from the store, as required when snapshot diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java index a24e9d75..ee6f3314 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/PassthroughCacheMetaStore.java @@ -45,15 +45,15 @@ public static PassthroughCacheMetaStore newPassthroughMetaCache(MetaStore localS } @Override - public void setRoot(Root root) throws IOException { - // call storeRoot instead of generic storeMeta because it does extra work when storing on disk - localStore.setRoot(root); + public void writeRoot(Root root) throws IOException { + // call writeRoot instead of generic writeMeta because it may do extra work when storing on disk + localStore.writeRoot(root); cache.put(RootRole.ROOT, root); } @Override @SuppressWarnings("unchecked") - public > Optional findMeta( + public > Optional readMeta( String roleName, Class tClass) throws IOException { // check memory cache if (cache.containsKey(roleName)) { @@ -61,18 +61,18 @@ public > Optional findMeta( } // check backing storage and write to memory if found - var value = localStore.findMeta(roleName, tClass); + var value = localStore.readMeta(roleName, tClass); value.ifPresent(v -> cache.put(roleName, v)); return value; } @Override - public void setMeta(String roleName, SignedTufMeta meta) throws IOException { + public void writeMeta(String roleName, SignedTufMeta meta) throws IOException { if (Objects.equals(roleName, RootRole.ROOT)) { - throw new IllegalArgumentException("Calling setMeta on root instead of setRoot"); + throw new IllegalArgumentException("Calling writeMeta on root instead of writeRoot"); } - localStore.setMeta(roleName, meta); + localStore.writeMeta(roleName, meta); cache.put(roleName, meta); } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java index ba336942..0a06ca8f 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetReader.java @@ -17,6 +17,7 @@ import java.io.IOException; +/** Interface that defines reading targets from local storage. */ public interface TargetReader { /** diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java index 9ffc2f55..2a2bd25b 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TargetStore.java @@ -17,9 +17,14 @@ import java.io.IOException; -/** Interface that defined a mutable meta store functionality. */ +/** Interface that defines a mutable target store functionality. */ public interface TargetStore extends TargetReader { + /** + * A generic string for identifying the local store in debug messages. A file system based + * implementation might return the path being used for storage, while an in-memory store may just + * return something like 'in-memory'. + */ String getIdentifier(); /** diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java index 6352734a..5471a95b 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java @@ -60,7 +60,7 @@ public String getIdentifier() { > T getMeta(String roleName, Class tClass) throws IOException { return metaStore - .findMeta(roleName, tClass) + .readMeta(roleName, tClass) .orElseThrow( () -> new IllegalStateException( @@ -70,7 +70,7 @@ > T getMeta(String roleName, Class } public void setRoot(Root root) throws IOException { - metaStore.setRoot(root); + metaStore.writeRoot(root); } public Root getRoot() throws IOException { @@ -78,11 +78,11 @@ public Root getRoot() throws IOException { } public Optional findRoot() throws IOException { - return metaStore.findMeta(RootRole.ROOT, Root.class); + return metaStore.readMeta(RootRole.ROOT, Root.class); } public void setTimestamp(Timestamp timestamp) throws IOException { - metaStore.setMeta(RootRole.TIMESTAMP, timestamp); + metaStore.writeMeta(RootRole.TIMESTAMP, timestamp); } public Timestamp getTimestamp() throws IOException { @@ -90,11 +90,11 @@ public Timestamp getTimestamp() throws IOException { } public Optional findTimestamp() throws IOException { - return metaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class); + return metaStore.readMeta(RootRole.TIMESTAMP, Timestamp.class); } public void setSnapshot(Snapshot snapshot) throws IOException { - metaStore.setMeta(RootRole.SNAPSHOT, snapshot); + metaStore.writeMeta(RootRole.SNAPSHOT, snapshot); } public Snapshot getSnapshot() throws IOException { @@ -102,11 +102,11 @@ public Snapshot getSnapshot() throws IOException { } public Optional findSnapshot() throws IOException { - return metaStore.findMeta(RootRole.SNAPSHOT, Snapshot.class); + return metaStore.readMeta(RootRole.SNAPSHOT, Snapshot.class); } public void setTargets(Targets targets) throws IOException { - metaStore.setMeta(RootRole.TARGETS, targets); + metaStore.writeMeta(RootRole.TARGETS, targets); } public Targets getTargets() throws IOException { @@ -114,7 +114,7 @@ public Targets getTargets() throws IOException { } public Optional findTargets() throws IOException { - return metaStore.findMeta(RootRole.TARGETS, Targets.class); + return metaStore.readMeta(RootRole.TARGETS, Targets.class); } public void clearMetaDueToKeyRotation() throws IOException { diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java index 16f227e4..0694b20d 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/FileSystemTufStoreTest.java @@ -32,21 +32,21 @@ class FileSystemTufStoreTest { @Test void newFileSystemStore_empty(@TempDir Path repoBase) throws IOException { FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); - assertFalse(tufStore.findMeta(RootRole.ROOT, Root.class).isPresent()); + assertFalse(tufStore.readMeta(RootRole.ROOT, Root.class).isPresent()); } @Test void newFileSystemStore_hasRepo(@TempDir Path repoBase) throws IOException { TestResources.setupRepoFiles(PROD_REPO, repoBase, "root.json"); FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); - assertTrue(tufStore.findMeta(RootRole.ROOT, Root.class).isPresent()); + assertTrue(tufStore.readMeta(RootRole.ROOT, Root.class).isPresent()); } @Test void setTrustedRoot_noPrevious(@TempDir Path repoBase) throws IOException { FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); assertFalse(repoBase.resolve("root.json").toFile().exists()); - tufStore.setRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); + tufStore.writeRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); assertEquals(2, repoBase.toFile().list().length, "Expect 2: root.json plus the /targets dir."); assertTrue(repoBase.resolve("root.json").toFile().exists()); assertTrue(repoBase.resolve("targets").toFile().isDirectory()); @@ -56,9 +56,9 @@ void setTrustedRoot_noPrevious(@TempDir Path repoBase) throws IOException { void setTrustedRoot_backupPerformed(@TempDir Path repoBase) throws IOException { TestResources.setupRepoFiles(PROD_REPO, repoBase, "root.json"); FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase); - int version = tufStore.findMeta(RootRole.ROOT, Root.class).get().getSignedMeta().getVersion(); + int version = tufStore.readMeta(RootRole.ROOT, Root.class).get().getSignedMeta().getVersion(); assertFalse(repoBase.resolve(version + ".root.json").toFile().exists()); - tufStore.setRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); + tufStore.writeRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT)); assertTrue(repoBase.resolve(version + ".root.json").toFile().exists()); } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java index 868789e6..d1618782 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java @@ -61,54 +61,54 @@ public void setup() throws IOException { @Test public void root_test() throws Exception { - assertTrue(fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); - assertTrue(passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); + assertTrue(fileSystemTufStore.readMeta(RootRole.ROOT, Root.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.readMeta(RootRole.ROOT, Root.class).isEmpty()); - passthroughCacheMetaStore.setRoot(root); + passthroughCacheMetaStore.writeRoot(root); - assertEquals(root, fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).get()); - assertEquals(root, passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).get()); + assertEquals(root, fileSystemTufStore.readMeta(RootRole.ROOT, Root.class).get()); + assertEquals(root, passthroughCacheMetaStore.readMeta(RootRole.ROOT, Root.class).get()); } @Test public void root_canInitFromDisk() throws Exception { - assertTrue(fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); - assertTrue(passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).isEmpty()); + assertTrue(fileSystemTufStore.readMeta(RootRole.ROOT, Root.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.readMeta(RootRole.ROOT, Root.class).isEmpty()); try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("root.json"))) { GSON.get().toJson(root, fileWriter); } - assertEquals(root, fileSystemTufStore.findMeta(RootRole.ROOT, Root.class).get()); - assertEquals(root, passthroughCacheMetaStore.findMeta(RootRole.ROOT, Root.class).get()); + assertEquals(root, fileSystemTufStore.readMeta(RootRole.ROOT, Root.class).get()); + assertEquals(root, passthroughCacheMetaStore.readMeta(RootRole.ROOT, Root.class).get()); } @Test public void meta_test() throws Exception { // root uses special handling for writing, but the rest of them don't, so we just test // timestamp here arbitrarily - assertTrue(fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); - assertTrue(passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + assertTrue(fileSystemTufStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); - passthroughCacheMetaStore.setMeta(RootRole.TIMESTAMP, timestamp); + passthroughCacheMetaStore.writeMeta(RootRole.TIMESTAMP, timestamp); - assertEquals(timestamp, fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + assertEquals(timestamp, fileSystemTufStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).get()); assertEquals( - timestamp, passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + timestamp, passthroughCacheMetaStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).get()); } @Test public void timestamp_canInitFromDisk() throws Exception { - assertTrue(fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); - assertTrue(passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + assertTrue(fileSystemTufStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); + assertTrue(passthroughCacheMetaStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).isEmpty()); try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("timestamp.json"))) { GSON.get().toJson(timestamp, fileWriter); } - assertEquals(timestamp, fileSystemTufStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + assertEquals(timestamp, fileSystemTufStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).get()); assertEquals( - timestamp, passthroughCacheMetaStore.findMeta(RootRole.TIMESTAMP, Timestamp.class).get()); + timestamp, passthroughCacheMetaStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).get()); } }