Skip to content

Commit

Permalink
Store meta state in memory
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
loosebazooka committed Oct 25, 2024
1 parent cc82dd2 commit 70ebdbe
Show file tree
Hide file tree
Showing 4 changed files with 471 additions and 237 deletions.
114 changes: 114 additions & 0 deletions sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMeta.java
Original file line number Diff line number Diff line change
@@ -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<Root> 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<Timestamp> 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<Snapshot> 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<Targets> 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;
}
}
112 changes: 65 additions & 47 deletions sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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() {
Expand All @@ -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<Root> localRoot = localStore.loadTrustedRoot();
Optional<Root> localRoot = trustedMeta.findRoot();
Root trustedRoot;
if (localRoot.isPresent()) {
trustedRoot = localRoot.get();
Expand Down Expand Up @@ -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++;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -265,9 +279,9 @@ void verifyDelegate(
}
}

Optional<Timestamp> 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
Expand All @@ -276,56 +290,56 @@ Optional<Timestamp> 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<Timestamp> 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<Timestamp> localTimestampMaybe = trustedMeta.findTimestamp();
if (localTimestampMaybe.isPresent()) {
Timestamp localTimestamp = localTimestampMaybe.get();
if (localTimestamp.getSignedMeta().getVersion() > timestamp.getSignedMeta().getVersion()) {
throw new RollbackVersionException(
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());
}
// 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) {
Expand All @@ -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<String, SnapshotMeta.SnapshotTarget> trustedTargetEntry :
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 70ebdbe

Please sign in to comment.