From be5deffc150e52597f81700a1edaeb0b9b4de0f9 Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Tue, 29 Oct 2024 10:01:57 -0400 Subject: [PATCH] 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(); }