Skip to content

Commit

Permalink
More tuf updates for conformance
Browse files Browse the repository at this point in the history
- don't persist N.root.json, could clash with a delegation
- validate at parsetime the type of meta file
- generify metastore a bit more, force updater to clear the right meta
- validate any bootstrapped root at update start time
- perform an update on meta if download target is called directly
  and we haven't updated yet

Signed-off-by: Appu Goundan <[email protected]>
  • Loading branch information
loosebazooka committed Nov 4, 2024
1 parent 90d958b commit 268a104
Show file tree
Hide file tree
Showing 11 changed files with 63 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,14 @@
import com.google.common.annotations.VisibleForTesting;
import dev.sigstore.tuf.model.*;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

/** Uses a local file system directory to store the trusted TUF metadata. */
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 final Path repoBaseDir;
private final Path targetsCache;

Expand Down Expand Up @@ -101,30 +96,10 @@ <T extends SignedTufMeta<?>> void storeRole(String roleName, T role) throws IOEx
}

@Override
public void writeRoot(Root root) throws IOException {
Optional<Root> trustedRoot = readMeta(RootRole.ROOT, Root.class);
if (trustedRoot.isPresent()) {
try {
Files.move(
repoBaseDir.resolve(ROOT_FILE_NAME),
repoBaseDir.resolve(
trustedRoot.get().getSignedMeta().getVersion() + "." + ROOT_FILE_NAME));
} catch (FileAlreadyExistsException e) {
// The file is already backed-up. continue.
}
}
storeRole(RootRole.ROOT, root);
}

@Override
public void clearMetaDueToKeyRotation() throws IOException {
File snapshotMetaFile = repoBaseDir.resolve(SNAPSHOT_FILE_NAME).toFile();
if (snapshotMetaFile.exists()) {
Files.delete(snapshotMetaFile.toPath());
}
File timestampMetaFile = repoBaseDir.resolve(TIMESTAMP_FILE_NAME).toFile();
if (timestampMetaFile.exists()) {
Files.delete(timestampMetaFile.toPath());
public void clearMeta(String role) throws IOException {
Path metaFile = repoBaseDir.resolve(role + ".json");
if (Files.isRegularFile(metaFile)) {
Files.delete(metaFile);
}
}
}
23 changes: 4 additions & 19 deletions sigstore-java/src/main/java/dev/sigstore/tuf/MetaStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package dev.sigstore.tuf;

import dev.sigstore.tuf.model.Root;
import dev.sigstore.tuf.model.SignedTufMeta;
import dev.sigstore.tuf.model.TufMeta;
import java.io.IOException;
Expand All @@ -31,8 +30,7 @@ public interface MetaStore extends MetaReader {
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 #writeRoot(Root)} instead.
* Generic method to store one of the {@link SignedTufMeta} resources in the local tuf store.
*
* @param roleName the name of the role
* @param meta the metadata to store
Expand All @@ -41,26 +39,13 @@ public interface MetaStore extends MetaReader {
void writeMeta(String roleName, SignedTufMeta<? extends TufMeta> meta) throws IOException;

/**
* Once you have ascertained that your root is trustworthy use this method to persist it to your
* local store. This will usually only be called with a root loaded statically from a bundled
* trusted root, or after the successful verification of an updated root from a mirror.
*
* @param root a root that has been proven trustworthy by the client
* @throws IOException since some implementations may persist the root to disk or over the network
* we throw {@code IOException} in case of IO error.
* @see <a
* href="https://theupdateframework.github.io/specification/latest/#detailed-client-workflow">5.3.8</a>
*/
void writeRoot(Root root) throws IOException;

/**
* This clears out the snapshot and timestamp metadata from the store, as required when snapshot
* or timestamp verification keys have changed as a result of a root update.
* Generic method to remove meta, useful when keys rotated in root. Deletion is not optional,
* implementers must ensure meta is removed from the storage medium.
*
* @throws IOException implementations that read/write IO to clear the data may throw {@code
* IOException}
* @see <a
* href="https://theupdateframework.github.io/specification/latest/#detailed-client-workflow">5.3.11</a>
*/
void clearMetaDueToKeyRotation() throws IOException;
void clearMeta(String role) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@
*/
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. */
Expand All @@ -44,13 +41,6 @@ public static PassthroughCacheMetaStore newPassthroughMetaCache(MetaStore localS
return new PassthroughCacheMetaStore(localStore);
}

@Override
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 <T extends SignedTufMeta<? extends TufMeta>> Optional<T> readMeta(
Expand All @@ -69,17 +59,13 @@ public <T extends SignedTufMeta<? extends TufMeta>> Optional<T> readMeta(

@Override
public void writeMeta(String roleName, SignedTufMeta<? extends TufMeta> meta) throws IOException {
if (Objects.equals(roleName, RootRole.ROOT)) {
throw new IllegalArgumentException("Calling writeMeta on root instead of writeRoot");
}
localStore.writeMeta(roleName, meta);
cache.put(roleName, meta);
}

@Override
public void clearMetaDueToKeyRotation() throws IOException {
localStore.clearMetaDueToKeyRotation();
cache.remove(RootRole.TIMESTAMP);
cache.remove(RootRole.SNAPSHOT);
public void clearMeta(String role) throws IOException {
localStore.clearMeta(role);
cache.remove(role);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ <T extends SignedTufMeta<? extends TufMeta>> T getMeta(String roleName, Class<T>
}

public void setRoot(Root root) throws IOException {
metaStore.writeRoot(root);
metaStore.writeMeta(RootRole.ROOT, root);
}

public Root getRoot() throws IOException {
Expand Down Expand Up @@ -118,6 +118,7 @@ public Optional<Targets> findTargets() throws IOException {
}

public void clearMetaDueToKeyRotation() throws IOException {
metaStore.clearMetaDueToKeyRotation();
metaStore.clearMeta(RootRole.TIMESTAMP);
metaStore.clearMeta(RootRole.SNAPSHOT);
}
}
28 changes: 18 additions & 10 deletions sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public class Updater {

// Mutable State
private ZonedDateTime updateStartTime;
private boolean metaUpdated;

Updater(
Clock clock,
Expand Down Expand Up @@ -104,20 +105,24 @@ public void updateMeta() throws IOException, NoSuchAlgorithmException, InvalidKe
updateRoot();
var oldTimestamp = trustedMetaStore.findTimestamp();
updateTimestamp();
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
// targets from remote
updateSnapshot();
updateTargets();
if (!Objects.equals(oldTimestamp.orElse(null), trustedMetaStore.getTimestamp())
|| trustedMetaStore.findSnapshot().isEmpty()
|| trustedMetaStore.findTargets().isEmpty()) {
// if we need to update or we can't find targets/snapshots locally then grab new snapshot and
// targets from remote
updateSnapshot();
updateTargets();
}
metaUpdated = true;
}

/** Download a single target defined in targets. Does not handle delegated targets. */
public void downloadTarget(String targetName)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
if (!metaUpdated) {
updateMeta();
}

var targetData = trustedMetaStore.getTargets().getSignedMeta().getTargets().get(targetName);
if (targetData == null) {
throw new TargetMetadataMissingException(targetName);
Expand All @@ -141,7 +146,11 @@ void updateRoot()
trustedRoot = localRoot.get();
} else {
trustedRoot = GSON.get().fromJson(trustedRootPath.get(), Root.class);
trustedMetaStore.setRoot(trustedRoot);
}
// verify root that we're bootstrapping this update with is good to go
verifyDelegate(trustedRoot, trustedRoot);

int baseVersion = trustedRoot.getSignedMeta().getVersion();
int nextVersion = baseVersion + 1;
// keep these for verifying the last step. 5.3.11
Expand Down Expand Up @@ -194,7 +203,6 @@ void updateRoot()
trustedRoot.getSignedMeta().getRoles().get(RootRole.TIMESTAMP))) {
trustedMetaStore.clearMetaDueToKeyRotation();
}
trustedMetaStore.setRoot(trustedRoot);
}

private void throwIfExpired(ZonedDateTime expires) {
Expand Down
6 changes: 6 additions & 0 deletions sigstore-java/src/main/java/dev/sigstore/tuf/model/Root.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package dev.sigstore.tuf.model;

import com.google.common.base.Preconditions;
import org.immutables.gson.Gson;
import org.immutables.value.Value;
import org.immutables.value.Value.Derived;
Expand All @@ -29,4 +30,9 @@ public interface Root extends SignedTufMeta<RootMeta> {
default RootMeta getSignedMeta() {
return getSignedMeta(RootMeta.class);
}

@Value.Check
default void checkType() {
Preconditions.checkState(getSignedMeta().getType().equals("root"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package dev.sigstore.tuf.model;

import com.google.common.base.Preconditions;
import org.immutables.gson.Gson;
import org.immutables.value.Value;
import org.immutables.value.Value.Derived;
Expand All @@ -29,4 +30,9 @@ public interface Snapshot extends SignedTufMeta<SnapshotMeta> {
default SnapshotMeta getSignedMeta() {
return getSignedMeta(SnapshotMeta.class);
}

@Value.Check
default void checkType() {
Preconditions.checkState(getSignedMeta().getType().equals("snapshot"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package dev.sigstore.tuf.model;

import com.google.common.base.Preconditions;
import org.immutables.gson.Gson;
import org.immutables.value.Value;
import org.immutables.value.Value.Derived;
Expand All @@ -29,4 +30,9 @@ public interface Targets extends SignedTufMeta<TargetMeta> {
default TargetMeta getSignedMeta() {
return getSignedMeta(TargetMeta.class);
}

@Value.Check
default void checkType() {
Preconditions.checkState(getSignedMeta().getType().equals("targets"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package dev.sigstore.tuf.model;

import com.google.common.base.Preconditions;
import org.immutables.gson.Gson;
import org.immutables.value.Value;
import org.immutables.value.Value.Derived;
Expand All @@ -30,4 +31,9 @@ public interface Timestamp extends SignedTufMeta<TimestampMeta> {
default TimestampMeta getSignedMeta() {
return getSignedMeta(TimestampMeta.class);
}

@Value.Check
default void checkType() {
Preconditions.checkState(getSignedMeta().getType().equals("timestamp"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,24 @@ void newFileSystemStore_hasRepo(@TempDir Path repoBase) throws IOException {
}

@Test
void setTrustedRoot_noPrevious(@TempDir Path repoBase) throws IOException {
void writeMeta(@TempDir Path repoBase) throws IOException {
FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase);
assertFalse(repoBase.resolve("root.json").toFile().exists());
tufStore.writeRoot(TestResources.loadRoot(TestResources.UPDATER_REAL_TRUSTED_ROOT));
tufStore.writeMeta(
RootRole.ROOT, 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());
}

@Test
void setTrustedRoot_backupPerformed(@TempDir Path repoBase) throws IOException {
TestResources.setupRepoFiles(PROD_REPO, repoBase, "root.json");
FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase);
int version = tufStore.readMeta(RootRole.ROOT, Root.class).get().getSignedMeta().getVersion();
assertFalse(repoBase.resolve(version + ".root.json").toFile().exists());
tufStore.writeRoot(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");
FileSystemTufStore tufStore = FileSystemTufStore.newFileSystemStore(repoBase);
assertTrue(repoBase.resolve("snapshot.json").toFile().exists());
assertTrue(repoBase.resolve("timestamp.json").toFile().exists());
tufStore.clearMetaDueToKeyRotation();
tufStore.clearMeta(RootRole.TIMESTAMP);
tufStore.clearMeta(RootRole.SNAPSHOT);
assertFalse(repoBase.resolve("snapshot.json").toFile().exists());
assertFalse(repoBase.resolve("timestamp.json").toFile().exists());
}
Expand Down
Loading

0 comments on commit 268a104

Please sign in to comment.