Skip to content

Commit 70ebdbe

Browse files
committed
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 <[email protected]>
1 parent cc82dd2 commit 70ebdbe

File tree

4 files changed

+471
-237
lines changed

4 files changed

+471
-237
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2024 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.tuf;
17+
18+
import dev.sigstore.tuf.model.Root;
19+
import dev.sigstore.tuf.model.RootRole;
20+
import dev.sigstore.tuf.model.Snapshot;
21+
import dev.sigstore.tuf.model.Targets;
22+
import dev.sigstore.tuf.model.Timestamp;
23+
import java.io.IOException;
24+
import java.util.Optional;
25+
26+
// An in memory cache that will pass through to a provided local tuf store
27+
class TrustedMeta {
28+
private final MutableTufStore localStore;
29+
private Root root;
30+
private Snapshot snapshot;
31+
private Timestamp timestamp;
32+
private Targets targets;
33+
34+
private TrustedMeta(MutableTufStore localStore) {
35+
this.localStore = localStore;
36+
}
37+
38+
static TrustedMeta newTrustedMeta(MutableTufStore localStore) {
39+
return new TrustedMeta(localStore);
40+
}
41+
42+
public void setRoot(Root root) throws IOException {
43+
// call storeTrustedRoot instead of generic storeMeta because it does doesn't extra work
44+
localStore.storeTrustedRoot(root);
45+
this.root = root;
46+
}
47+
48+
public Root getRoot() throws IOException {
49+
return findRoot().orElseThrow(() -> new IllegalStateException("No cached root to load"));
50+
}
51+
52+
public Optional<Root> findRoot() throws IOException {
53+
if (root == null) {
54+
root = localStore.loadTrustedRoot().orElse(null);
55+
}
56+
return Optional.ofNullable(root);
57+
}
58+
59+
public void setTimestamp(Timestamp timestamp) throws IOException {
60+
localStore.storeMeta(RootRole.TIMESTAMP, timestamp);
61+
this.timestamp = timestamp;
62+
}
63+
64+
public Timestamp getTimestamp() throws IOException {
65+
return findTimestamp()
66+
.orElseThrow(() -> new IllegalStateException("No cached timestamp to load"));
67+
}
68+
69+
public Optional<Timestamp> findTimestamp() throws IOException {
70+
if (timestamp == null) {
71+
timestamp = localStore.loadTimestamp().orElse(null);
72+
}
73+
return Optional.ofNullable(timestamp);
74+
}
75+
76+
public void setSnapshot(Snapshot snapshot) throws IOException {
77+
localStore.storeMeta(RootRole.SNAPSHOT, snapshot);
78+
this.snapshot = snapshot;
79+
}
80+
81+
public Snapshot getSnapshot() throws IOException {
82+
return findSnapshot()
83+
.orElseThrow(() -> new IllegalStateException("No cached snapshot to load"));
84+
}
85+
86+
public Optional<Snapshot> findSnapshot() throws IOException {
87+
if (snapshot == null) {
88+
snapshot = localStore.loadSnapshot().orElse(null);
89+
}
90+
return Optional.ofNullable(snapshot);
91+
}
92+
93+
public void setTargets(Targets targets) throws IOException {
94+
localStore.storeMeta(RootRole.TARGETS, targets);
95+
this.targets = targets;
96+
}
97+
98+
public Targets getTargets() throws IOException {
99+
return findTargets().orElseThrow(() -> new IllegalStateException("No cached targets to load"));
100+
}
101+
102+
public Optional<Targets> findTargets() throws IOException {
103+
if (targets == null) {
104+
targets = localStore.loadTargets().orElse(null);
105+
}
106+
return Optional.ofNullable(targets);
107+
}
108+
109+
public void clearMetaDueToKeyRotation() throws IOException {
110+
localStore.clearMetaDueToKeyRotation();
111+
timestamp = null;
112+
snapshot = null;
113+
}
114+
}

sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.List;
3737
import java.util.Locale;
3838
import java.util.Map;
39+
import java.util.Objects;
3940
import java.util.Optional;
4041
import java.util.logging.Level;
4142
import java.util.logging.Logger;
@@ -57,13 +58,17 @@ public class Updater {
5758

5859
private static final Logger log = Logger.getLogger(Updater.class.getName());
5960

60-
private Clock clock;
61-
private Verifiers.Supplier verifiers;
62-
private MetaFetcher metaFetcher;
63-
private Fetcher targetFetcher;
61+
private final Clock clock;
62+
private final Verifiers.Supplier verifiers;
63+
private final MetaFetcher metaFetcher;
64+
private final Fetcher targetFetcher;
65+
private final RootProvider trustedRootPath;
66+
// TODO: this should be replaced by a dedicated target store
67+
private final MutableTufStore localStore;
68+
69+
// Mutable State
6470
private ZonedDateTime updateStartTime;
65-
private RootProvider trustedRootPath;
66-
private MutableTufStore localStore;
71+
private TrustedMeta trustedMeta;
6772

6873
Updater(
6974
Clock clock,
@@ -78,6 +83,7 @@ public class Updater {
7883
this.localStore = localStore;
7984
this.metaFetcher = metaFetcher;
8085
this.targetFetcher = targetFetcher;
86+
this.trustedMeta = TrustedMeta.newTrustedMeta(localStore);
8187
}
8288

8389
public static Builder builder() {
@@ -86,28 +92,36 @@ public static Builder builder() {
8692

8793
public void update()
8894
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
89-
var root = updateRoot();
90-
// only returns a timestamp value if a more recent timestamp file has been found.
91-
var timestampMaybe = updateTimestamp(root);
92-
if (timestampMaybe.isPresent()) {
93-
var snapshot = updateSnapshot(root, timestampMaybe.get());
94-
var targets = updateTargets(root, snapshot);
95-
downloadTargets(targets);
95+
updateMeta();
96+
downloadTargets(trustedMeta.getTargets());
97+
}
98+
99+
void updateMeta() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
100+
updateRoot();
101+
var oldTimestamp = trustedMeta.findTimestamp();
102+
updateTimestamp();
103+
if (Objects.equals(oldTimestamp.orElse(null), trustedMeta.getTimestamp())
104+
&& trustedMeta.findSnapshot().isPresent()
105+
&& trustedMeta.findTargets().isPresent()) {
106+
return;
96107
}
108+
// if we need to update or we can't find targets/timestamps locally then grab new snapshot and
109+
// targets from remote
110+
updateSnapshot();
111+
updateTargets();
97112
}
98113

99114
// https://theupdateframework.github.io/specification/latest/#detailed-client-workflow
100-
Root updateRoot()
115+
void updateRoot()
101116
throws IOException, RoleExpiredException, NoSuchAlgorithmException, InvalidKeySpecException,
102-
InvalidKeyException, FileExceedsMaxLengthException, RollbackVersionException,
103-
SignatureVerificationException {
117+
FileExceedsMaxLengthException, RollbackVersionException, SignatureVerificationException {
104118
// 5.3.1) record the time at start and use for expiration checks consistently throughout the
105119
// update.
106120
updateStartTime = ZonedDateTime.now(clock);
107121

108122
// 5.3.2) load the trust metadata file (root.json), get version of root.json and the role
109123
// signature threshold value
110-
Optional<Root> localRoot = localStore.loadTrustedRoot();
124+
Optional<Root> localRoot = trustedMeta.findRoot();
111125
Root trustedRoot;
112126
if (localRoot.isPresent()) {
113127
trustedRoot = localRoot.get();
@@ -148,7 +162,7 @@ Root updateRoot()
148162
// 5.3.7) set the trusted root metadata to the new root
149163
trustedRoot = newRoot;
150164
// 5.3.8) persist to repo
151-
localStore.storeTrustedRoot(trustedRoot);
165+
trustedMeta.setRoot(trustedRoot);
152166
// 5.3.9) see if there are more versions go back 5.3.3
153167
nextVersion++;
154168
}
@@ -164,9 +178,9 @@ Root updateRoot()
164178
|| hasNewKeys(
165179
preUpdateTimestampRole,
166180
trustedRoot.getSignedMeta().getRoles().get(RootRole.TIMESTAMP))) {
167-
localStore.clearMetaDueToKeyRotation();
181+
trustedMeta.clearMetaDueToKeyRotation();
168182
}
169-
return trustedRoot;
183+
trustedMeta.setRoot(trustedRoot);
170184
}
171185

172186
private void throwIfExpired(ZonedDateTime expires) {
@@ -265,9 +279,9 @@ void verifyDelegate(
265279
}
266280
}
267281

268-
Optional<Timestamp> updateTimestamp(Root root)
269-
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException,
270-
FileNotFoundException, SignatureVerificationException {
282+
void updateTimestamp()
283+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, FileNotFoundException,
284+
SignatureVerificationException {
271285
// 1) download the timestamp.json bytes.
272286
var timestamp =
273287
metaFetcher
@@ -276,56 +290,56 @@ Optional<Timestamp> updateTimestamp(Root root)
276290
.getMetaResource();
277291

278292
// 2) verify against threshold of keys as specified in trusted root.json
279-
verifyDelegate(root, timestamp);
293+
verifyDelegate(trustedMeta.getRoot(), timestamp);
280294

281295
// 3) If the new timestamp file has a lesser version than our current trusted timestamp file
282-
// report a rollback attack. If it is equal abort the update as there should be no changes. If
283-
// it is higher than continue update.
284-
Optional<Timestamp> localTimestampMaybe = localStore.loadTimestamp();
296+
// report a rollback attack. If it is equal, just return the original timestamp there should
297+
// be no changes. If it is higher than continue update.
298+
Optional<Timestamp> localTimestampMaybe = trustedMeta.findTimestamp();
285299
if (localTimestampMaybe.isPresent()) {
286300
Timestamp localTimestamp = localTimestampMaybe.get();
287301
if (localTimestamp.getSignedMeta().getVersion() > timestamp.getSignedMeta().getVersion()) {
288302
throw new RollbackVersionException(
289303
localTimestamp.getSignedMeta().getVersion(), timestamp.getSignedMeta().getVersion());
290304
}
291305
if (localTimestamp.getSignedMeta().getVersion() == timestamp.getSignedMeta().getVersion()) {
292-
return Optional.empty();
306+
trustedMeta.setTimestamp(localTimestamp);
307+
return;
293308
}
294309
}
295310
// 4) check expiration timestamp is after tuf update start time, else fail.
296311
throwIfExpired(timestamp.getSignedMeta().getExpiresAsDate());
297312
// 5) persist timestamp.json
298-
localStore.storeMeta(RootRole.TIMESTAMP, timestamp);
299-
return Optional.of(timestamp);
313+
trustedMeta.setTimestamp(timestamp);
300314
}
301315

302-
Snapshot updateSnapshot(Root root, Timestamp timestamp)
316+
void updateSnapshot()
303317
throws IOException, FileNotFoundException, InvalidHashesException,
304-
SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException,
305-
InvalidKeyException {
318+
SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException {
306319
// 1) download the snapshot.json bytes up to timestamp's snapshot length.
307-
int timestampSnapshotVersion = timestamp.getSignedMeta().getSnapshotMeta().getVersion();
320+
int timestampSnapshotVersion =
321+
trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getVersion();
308322
var snapshotResult =
309323
metaFetcher.getMeta(
310324
RootRole.SNAPSHOT,
311325
timestampSnapshotVersion,
312326
Snapshot.class,
313-
timestamp.getSignedMeta().getSnapshotMeta().getLengthOrDefault());
327+
trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getLengthOrDefault());
314328
if (snapshotResult.isEmpty()) {
315329
throw new FileNotFoundException(
316330
timestampSnapshotVersion + ".snapshot.json", metaFetcher.getSource());
317331
}
318332
// 2) check against timestamp.snapshot.hash, this is optional, the fallback is
319333
// that the version must match, which is handled in (4).
320334
var snapshot = snapshotResult.get();
321-
if (timestamp.getSignedMeta().getSnapshotMeta().getHashes().isPresent()) {
335+
if (trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().isPresent()) {
322336
verifyHashes(
323337
"snapshot",
324338
snapshot.getRawBytes(),
325-
timestamp.getSignedMeta().getSnapshotMeta().getHashes().get());
339+
trustedMeta.getTimestamp().getSignedMeta().getSnapshotMeta().getHashes().get());
326340
}
327341
// 3) Check against threshold of root signing keys, else fail
328-
verifyDelegate(root, snapshot.getMetaResource());
342+
verifyDelegate(trustedMeta.getRoot(), snapshot.getMetaResource());
329343
// 4) Check snapshot.version matches timestamp.snapshot.version, else fail.
330344
int snapshotVersion = snapshot.getMetaResource().getSignedMeta().getVersion();
331345
if (snapshotVersion != timestampSnapshotVersion) {
@@ -334,7 +348,7 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp)
334348
// 5) Ensure all targets and delegated targets in the trusted (old) snapshots file have versions
335349
// which are less than or equal to the equivalent target in the new file. Check that no targets
336350
// are missing in new file. Else fail.
337-
var trustedSnapshotMaybe = localStore.loadSnapshot();
351+
var trustedSnapshotMaybe = trustedMeta.findSnapshot();
338352
if (trustedSnapshotMaybe.isPresent()) {
339353
var trustedSnapshot = trustedSnapshotMaybe.get();
340354
for (Map.Entry<String, SnapshotMeta.SnapshotTarget> trustedTargetEntry :
@@ -356,8 +370,7 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp)
356370
// 6) Ensure expiration timestamp of snapshot is later than tuf update start time.
357371
throwIfExpired(snapshot.getMetaResource().getSignedMeta().getExpiresAsDate());
358372
// 7) persist snapshot.
359-
localStore.storeMeta(RootRole.SNAPSHOT, snapshot.getMetaResource());
360-
return snapshot.getMetaResource();
373+
trustedMeta.setSnapshot(snapshot.getMetaResource());
361374
}
362375

363376
// 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
389402
}
390403
}
391404

392-
Targets updateTargets(Root root, Snapshot snapshot)
405+
void updateTargets()
393406
throws IOException, FileNotFoundException, InvalidHashesException,
394407
SignatureVerificationException, NoSuchAlgorithmException, InvalidKeySpecException,
395-
InvalidKeyException, FileExceedsMaxLengthException {
408+
FileExceedsMaxLengthException {
396409
// 1) download the targets.json up to targets.json length in bytes.
397-
SnapshotMeta.SnapshotTarget targetMeta = snapshot.getSignedMeta().getTargetMeta("targets.json");
410+
SnapshotMeta.SnapshotTarget targetMeta =
411+
trustedMeta.getSnapshot().getSignedMeta().getTargetMeta("targets.json");
398412
var targetsResultMaybe =
399413
metaFetcher.getMeta(
400414
RootRole.TARGETS,
@@ -415,7 +429,7 @@ Targets updateTargets(Root root, Snapshot snapshot)
415429
targetMeta.getHashes().get());
416430
}
417431
// 3) check against threshold of keys as specified by trusted root.json
418-
verifyDelegate(root, targetsResult.getMetaResource());
432+
verifyDelegate(trustedMeta.getRoot(), targetsResult.getMetaResource());
419433
// 4) check targets.version == snapshot.targets.version, else fail.
420434
int targetsVersion = targetsResult.getMetaResource().getSignedMeta().getVersion();
421435
int snapshotTargetsVersion = targetMeta.getVersion();
@@ -426,8 +440,7 @@ Targets updateTargets(Root root, Snapshot snapshot)
426440
throwIfExpired(targetsResult.getMetaResource().getSignedMeta().getExpiresAsDate());
427441
// 6) persist targets metadata
428442
// why do we persist the
429-
localStore.storeMeta(RootRole.TARGETS, targetsResult.getMetaResource());
430-
return targetsResult.getMetaResource();
443+
trustedMeta.setTargets(targetsResult.getMetaResource());
431444
}
432445

433446
void downloadTargets(Targets targets)
@@ -470,6 +483,11 @@ MutableTufStore getLocalStore() {
470483
return localStore;
471484
}
472485

486+
@VisibleForTesting
487+
TrustedMeta getTrustedMeta() {
488+
return trustedMeta;
489+
}
490+
473491
public static class Builder {
474492
private Clock clock = Clock.systemUTC();
475493
private Verifiers.Supplier verifiers = Verifiers::newVerifier;

0 commit comments

Comments
 (0)