Skip to content

Commit 308ad29

Browse files
author
Patrick Flynn
authored
Delegate TUF resource fetching to MetaFetcher interface (#159)
* Introduce a MetaFetcher interface to aid code reuse across resource types and facilitate testing. (includes some drive by refactoring to support testing) Signed-off-by: Patrick Flynn <[email protected]> * static builder factory method Signed-off-by: Patrick Flynn <[email protected]> * Change TufClient API to make update configuration instance state rather than as arguments to the update method Signed-off-by: Patrick Flynn <[email protected]> Signed-off-by: Patrick Flynn <[email protected]>
1 parent 2fc1507 commit 308ad29

File tree

4 files changed

+243
-109
lines changed

4 files changed

+243
-109
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2022 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 static dev.sigstore.json.GsonSupplier.GSON;
19+
20+
import com.google.api.client.http.GenericUrl;
21+
import com.google.api.client.json.gson.GsonFactory;
22+
import dev.sigstore.http.HttpClients;
23+
import dev.sigstore.http.ImmutableHttpParams;
24+
import dev.sigstore.tuf.model.Root;
25+
import java.io.IOException;
26+
import java.net.URL;
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.Optional;
29+
30+
public class HttpMetaFetcher implements MetaFetcher {
31+
32+
private static final int MAX_META_BYTES = 99 * 1024; // 99 KB
33+
private URL mirror;
34+
35+
HttpMetaFetcher(URL mirror) {
36+
this.mirror = mirror;
37+
}
38+
39+
public static HttpMetaFetcher newFetcher(URL mirror) {
40+
return new HttpMetaFetcher(mirror);
41+
}
42+
43+
@Override
44+
public String getSource() {
45+
return mirror.toString();
46+
}
47+
48+
@Override
49+
public Optional<Root> getRootAtVersion(int version)
50+
throws IOException, MetaFileExceedsMaxException {
51+
String versionFileName = version + ".root.json";
52+
GenericUrl nextVersionUrl = new GenericUrl(mirror + "/" + versionFileName);
53+
var req =
54+
HttpClients.newHttpTransport(ImmutableHttpParams.builder().build())
55+
.createRequestFactory(
56+
request -> {
57+
request.setParser(GsonFactory.getDefaultInstance().createJsonObjectParser());
58+
})
59+
.buildGetRequest(nextVersionUrl);
60+
req.getHeaders().setAccept("application/json; api-version=2.0");
61+
req.getHeaders().setContentType("application/json");
62+
req.setThrowExceptionOnExecuteError(false);
63+
var resp = req.execute();
64+
if (resp.getStatusCode() == 404) {
65+
return Optional.empty();
66+
}
67+
if (resp.getStatusCode() != 200) {
68+
throw new TufException(
69+
String.format(
70+
"Unexpected return from mirror. Status code: %s, status message: %s"
71+
+ resp.getStatusCode()
72+
+ resp.getStatusMessage()));
73+
}
74+
byte[] rootBytes = resp.getContent().readNBytes(MAX_META_BYTES);
75+
if (rootBytes.length == MAX_META_BYTES && resp.getContent().read() != -1) {
76+
throw new MetaFileExceedsMaxException(nextVersionUrl.toString(), MAX_META_BYTES);
77+
}
78+
return Optional.of(
79+
GSON.get().fromJson(new String(rootBytes, StandardCharsets.UTF_8), Root.class));
80+
}
81+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2022 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 java.io.IOException;
20+
import java.util.Optional;
21+
22+
/** Retrieves TUF metadata. */
23+
public interface MetaFetcher {
24+
25+
/**
26+
* Describes the source of the metadata being fetched from. e.g "http://mirror.bla/mirror",
27+
* "mock", "c:/tmp".
28+
*/
29+
String getSource();
30+
31+
/**
32+
* Fetch the {@link Root} at the specified {@code version}.
33+
*
34+
* @throws MetaFileExceedsMaxException when the retrieved file is larger than the maximum allowed
35+
* by the client
36+
*/
37+
Optional<Root> getRootAtVersion(int version) throws IOException, MetaFileExceedsMaxException;
38+
}

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

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,11 @@
1717

1818
import static dev.sigstore.json.GsonSupplier.GSON;
1919

20-
import com.google.api.client.http.GenericUrl;
21-
import com.google.api.client.json.gson.GsonFactory;
2220
import com.google.common.annotations.VisibleForTesting;
2321
import dev.sigstore.encryption.Keys;
2422
import dev.sigstore.encryption.signers.Verifiers;
25-
import dev.sigstore.http.HttpClients;
26-
import dev.sigstore.http.ImmutableHttpParams;
2723
import dev.sigstore.tuf.model.*;
2824
import java.io.IOException;
29-
import java.net.URL;
30-
import java.nio.charset.StandardCharsets;
3125
import java.nio.file.Files;
3226
import java.nio.file.Path;
3327
import java.security.InvalidKeyException;
@@ -52,31 +46,37 @@
5246
*/
5347
public class TufClient {
5448

55-
private static final int MAX_META_BYTES = 99 * 1024; // 99 KB
5649
private static final int MAX_UPDATES =
5750
1024; // Limit the update loop to retrieve a max of 1024 subsequent versions as expressed in
5851
// 5.3.3 of spec.
5952

6053
private Clock clock;
6154
private Verifiers.Supplier verifiers;
62-
63-
TufClient(Clock clock, Verifiers.Supplier verifiers) {
55+
private MetaFetcher fetcher;
56+
private ZonedDateTime updateStartTime;
57+
private Path trustedRootPath;
58+
private TufLocalStore localStore;
59+
60+
TufClient(
61+
Clock clock,
62+
Verifiers.Supplier verifiers,
63+
MetaFetcher fetcher,
64+
Path trustedRootPath,
65+
TufLocalStore localStore) {
6466
this.clock = clock;
6567
this.verifiers = verifiers;
68+
this.fetcher = fetcher;
69+
this.trustedRootPath = trustedRootPath;
70+
this.localStore = localStore;
71+
this.fetcher = fetcher;
6672
}
6773

68-
TufClient(Verifiers.Supplier verifiers) {
69-
this(Clock.systemUTC(), verifiers);
70-
}
71-
72-
TufClient() {
73-
this(Clock.systemUTC(), Verifiers::newVerifier);
74+
public static Builder builder() {
75+
return new Builder();
7476
}
7577

76-
private ZonedDateTime updateStartTime;
77-
7878
// https://theupdateframework.github.io/specification/latest/#detailed-client-workflow
79-
public void updateRoot(Path trustedRootPath, URL mirror, TufLocalStore localStore)
79+
public void updateRoot()
8080
throws IOException, RootExpiredException, NoSuchAlgorithmException, InvalidKeySpecException,
8181
InvalidKeyException, MetaFileExceedsMaxException, RoleVersionException,
8282
SignatureVerificationException {
@@ -98,36 +98,12 @@ public void updateRoot(Path trustedRootPath, URL mirror, TufLocalStore localStor
9898
// 5.3.3) download $version+1.root.json from mirror url (eventually obtained from remote.json
9999
// or map.json) up MAX_META_BYTES. If the file is not available, or we have reached
100100
// MAX_UPDATES number of root metadata files go to step 5.3.10
101-
String nextVersionFileName = nextVersion + ".root.json";
102-
GenericUrl nextVersionUrl = new GenericUrl(mirror + "/" + nextVersionFileName);
103-
var req =
104-
HttpClients.newHttpTransport(ImmutableHttpParams.builder().build())
105-
.createRequestFactory(
106-
request -> {
107-
request.setParser(GsonFactory.getDefaultInstance().createJsonObjectParser());
108-
})
109-
.buildGetRequest(nextVersionUrl);
110-
req.getHeaders().setAccept("application/json; api-version=2.0");
111-
req.getHeaders().setContentType("application/json");
112-
req.setThrowExceptionOnExecuteError(false);
113-
var resp = req.execute();
114-
if (resp.getStatusCode() == 404) {
101+
var newRootMaybe = fetcher.getRootAtVersion(nextVersion);
102+
if (newRootMaybe.isEmpty()) {
115103
// No newer versions, go to 5.3.10.
116104
break;
117105
}
118-
if (resp.getStatusCode() != 200) {
119-
throw new TufException(
120-
String.format(
121-
"Unexpected return from mirror. Status code: %s, status message: %s"
122-
+ resp.getStatusCode()
123-
+ resp.getStatusMessage()));
124-
}
125-
byte[] rootBytes = resp.getContent().readNBytes(MAX_META_BYTES);
126-
if (rootBytes.length == MAX_META_BYTES && resp.getContent().read() != -1) {
127-
throw new MetaFileExceedsMaxException(nextVersionUrl.toString(), MAX_META_BYTES);
128-
}
129-
var newRoot = GSON.get().fromJson(new String(rootBytes, StandardCharsets.UTF_8), Root.class);
130-
106+
var newRoot = newRootMaybe.get();
131107
// 5.3.4) we have a valid next version of the root.json. Check that the file has been signed
132108
// by:
133109
// a) a threshold (from step 2) of keys specified in the trusted metadata
@@ -163,7 +139,7 @@ public void updateRoot(Path trustedRootPath, URL mirror, TufLocalStore localStor
163139
// otherwise throw error.
164140
ZonedDateTime expires = trustedRoot.getSignedMeta().getExpiresAsDate();
165141
if (expires.isBefore(updateStartTime)) {
166-
throw new RootExpiredException(mirror.toString(), updateStartTime, expires);
142+
throw new RootExpiredException(fetcher.getSource(), updateStartTime, expires);
167143
}
168144
// 5.3.11) If the timestamp and / or snapshot keys have been rotated, then delete the trusted
169145
// timestamp and snapshot metadata files.
@@ -230,7 +206,7 @@ void verifyDelegate(
230206
public void updateTimestamp() {
231207
// 1) download the timestamp.json bytes up to few 10s of K max.
232208

233-
// 2) verify against threshold of keys as specified in trusted root,json
209+
// 2) verify against threshold of keys as specified in trusted root.json
234210

235211
// 3) check that version of new timestamp.json is higher or equal than current, else fail.
236212
// 3.2) check that timestamp.snapshot.version <= timestamp.version or fail
@@ -283,4 +259,42 @@ public void updateTargets() {
283259

284260
// Done!!
285261
}
262+
263+
public static class Builder {
264+
private Clock clock = Clock.systemUTC();
265+
private Verifiers.Supplier verifiers = Verifiers::newVerifier;
266+
267+
private MetaFetcher fetcher;
268+
private Path trustedRootPath;
269+
private TufLocalStore localStore;
270+
271+
public Builder setClock(Clock clock) {
272+
this.clock = clock;
273+
return this;
274+
}
275+
276+
public Builder setVerifiers(Verifiers.Supplier verifiers) {
277+
this.verifiers = verifiers;
278+
return this;
279+
}
280+
281+
public Builder setLocalStore(TufLocalStore store) {
282+
this.localStore = store;
283+
return this;
284+
}
285+
286+
public Builder setTrustedRootPath(Path trustedRootPath) {
287+
this.trustedRootPath = trustedRootPath;
288+
return this;
289+
}
290+
291+
public Builder setFetcher(MetaFetcher fetcher) {
292+
this.fetcher = fetcher;
293+
return this;
294+
}
295+
296+
public TufClient build() {
297+
return new TufClient(clock, verifiers, fetcher, trustedRootPath, localStore);
298+
}
299+
}
286300
}

0 commit comments

Comments
 (0)