Skip to content

Commit 30ed39b

Browse files
committed
Use HTTP server for TUF conformance testing
Signed-off-by: Aaron Lew <[email protected]>
1 parent cd936f9 commit 30ed39b

File tree

8 files changed

+269
-5
lines changed

8 files changed

+269
-5
lines changed

.github/workflows/tuf-conformance.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
name: TUF Conformance Tests
2+
permissions:
3+
contents: read
24

35
on:
46
push:
@@ -37,11 +39,14 @@ jobs:
3739
- name: Setup Gradle
3840
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
3941

40-
- name: Build tuf cli
41-
run: ./gradlew :tuf-cli:build
42+
- name: Build tuf cli and server jar
43+
run: ./gradlew :tuf-cli:serverShadowJar
4244

43-
- name: Unpack tuf distribution
44-
run: tar -xvf ${{ github.workspace }}/tuf-cli/build/distributions/tuf-cli-*.tar --strip-components 1
45+
- name: Start test server in background
46+
run: java -jar ${{ github.workspace }}/tuf-cli/build/libs/tuf-cli-server-all.jar &
47+
48+
- name: Wait for server to be ready
49+
run: curl --retry-connrefused --retry 10 --retry-delay 1 --fail http://localhost:8080/
4550

4651
- name: Set up JDK ${{ matrix.java-version }}
4752
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -51,5 +56,5 @@ jobs:
5156

5257
- uses: theupdateframework/tuf-conformance@9bfc222a371e30ad5511eb17449f68f855fb9d8f # v2.3.0
5358
with:
54-
entrypoint: ${{ github.workspace }}/bin/tuf-cli
59+
entrypoint: ${{ github.workspace }}/tuf-cli/tuf-cli-server
5560
artifact-name: test repositories for tuf-cli java ${{ matrix.java-version }}

tuf-cli/build.gradle.kts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
13
plugins {
24
id("build-logic.java")
35
id("application")
6+
id("com.gradleup.shadow") version "9.0.0-rc3"
47
}
58

69
repositories {
@@ -15,6 +18,11 @@ dependencies {
1518
implementation(platform("com.google.oauth-client:google-oauth-client-bom:1.39.0"))
1619
implementation("com.google.oauth-client:google-oauth-client")
1720

21+
implementation("org.eclipse.jetty:jetty-server:11.0.24")
22+
implementation("org.eclipse.jetty:jetty-servlet:11.0.24")
23+
24+
implementation("org.slf4j:slf4j-simple:2.0.17")
25+
1826
annotationProcessor("info.picocli:picocli-codegen:4.7.6")
1927
}
2028

@@ -37,3 +45,20 @@ distributions.main {
3745
tasks.run.configure {
3846
workingDir = rootProject.projectDir
3947
}
48+
49+
tasks.register<ShadowJar>("serverShadowJar") {
50+
archiveBaseName.set("tuf-cli-server")
51+
archiveClassifier.set("all")
52+
archiveVersion.set("")
53+
54+
mergeServiceFiles()
55+
56+
from(sourceSets.main.get().output)
57+
configurations = listOf(project.configurations.runtimeClasspath.get())
58+
59+
exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA")
60+
61+
manifest {
62+
attributes("Main-Class" to "dev.sigstore.tuf.cli.TufConformanceServer")
63+
}
64+
}

tuf-cli/src/main/java/dev/sigstore/tuf/cli/Download.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public Integer call() throws Exception {
4949
.setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(metadataUrl)))
5050
.setTargetFetcher(HttpFetcher.newFetcher(targetBaseUrl))
5151
.setTargetStore(fsStore)
52+
.setClock(TestClock.get())
5253
.build();
5354
// the java client isn't one shot like other clients, so downloadTarget doesn't call update
5455
// for the sake of conformance updateMeta here

tuf-cli/src/main/java/dev/sigstore/tuf/cli/Refresh.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public Integer call() throws Exception {
4444
PassthroughCacheMetaStore.newPassthroughMetaCache(fsStore)))
4545
.setTrustedRootPath(RootProvider.fromFile(metadataDir.resolve("root.json")))
4646
.setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(metadataUrl)))
47+
.setClock(TestClock.get())
4748
.build();
4849
tuf.updateMeta();
4950
return 0;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 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.cli;
17+
18+
import java.time.Clock;
19+
20+
public class TestClock {
21+
private static Clock clock = null;
22+
23+
public static void set(Clock clock) {
24+
TestClock.clock = clock;
25+
}
26+
27+
public static Clock get() {
28+
return clock == null ? Clock.systemUTC() : clock;
29+
}
30+
31+
public static void reset() {
32+
TestClock.clock = null;
33+
}
34+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2025 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.cli;
17+
18+
import com.google.gson.Gson;
19+
import jakarta.servlet.ServletException;
20+
import jakarta.servlet.http.HttpServletRequest;
21+
import jakarta.servlet.http.HttpServletResponse;
22+
import java.io.ByteArrayOutputStream;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.io.OutputStream;
26+
import java.io.PrintStream;
27+
import java.nio.charset.StandardCharsets;
28+
import java.time.Clock;
29+
import java.time.Instant;
30+
import java.time.OffsetDateTime;
31+
import java.time.ZoneOffset;
32+
import java.time.format.DateTimeFormatter;
33+
import java.util.Arrays;
34+
import java.util.List;
35+
import java.util.Map;
36+
import org.eclipse.jetty.server.Request;
37+
import org.eclipse.jetty.server.Server;
38+
import org.eclipse.jetty.server.handler.AbstractHandler;
39+
40+
public class TufConformanceServer {
41+
42+
private static final Gson GSON = new Gson();
43+
44+
private static class ExecuteRequest {
45+
String[] args;
46+
String faketime;
47+
}
48+
49+
public static void main(String[] args) throws Exception {
50+
int port = 8080;
51+
Server server = new Server(port);
52+
server.setHandler(new TufConformanceHandler());
53+
server.start();
54+
server.join();
55+
}
56+
57+
public static class TufConformanceHandler extends AbstractHandler {
58+
@Override
59+
public void handle(
60+
String target,
61+
Request baseRequest,
62+
HttpServletRequest request,
63+
HttpServletResponse response)
64+
throws IOException, ServletException {
65+
if ("/".equals(target)) {
66+
handleHealthCheck(response);
67+
} else if ("/execute".equals(target) && "POST".equals(request.getMethod())) {
68+
handleExecute(request, response);
69+
}
70+
baseRequest.setHandled(true);
71+
}
72+
}
73+
74+
private static void handleExecute(HttpServletRequest request, HttpServletResponse response)
75+
throws IOException {
76+
ExecuteRequest executeRequest;
77+
try (InputStream is = request.getInputStream()) {
78+
String requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8);
79+
executeRequest = GSON.fromJson(requestBody, ExecuteRequest.class);
80+
}
81+
82+
// Tests should not be run in parallel, to ensure orderly input/output
83+
PrintStream originalOut = System.out;
84+
PrintStream originalErr = System.err;
85+
86+
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
87+
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
88+
89+
try (PrintStream outPs = new PrintStream(outContent, true, StandardCharsets.UTF_8);
90+
PrintStream errPs = new PrintStream(errContent, true, StandardCharsets.UTF_8)) {
91+
System.setOut(outPs);
92+
System.setErr(errPs);
93+
94+
if (executeRequest.faketime != null && !executeRequest.faketime.isEmpty()) {
95+
String faketime = executeRequest.faketime;
96+
Instant fakeNow;
97+
if (faketime.startsWith("@")) {
98+
long epochSecond = Long.parseLong(faketime.substring(1));
99+
fakeNow = Instant.ofEpochSecond(epochSecond);
100+
} else {
101+
try {
102+
long epochSecond = Long.parseLong(faketime);
103+
fakeNow = Instant.ofEpochSecond(epochSecond);
104+
} catch (NumberFormatException e) {
105+
fakeNow = OffsetDateTime.parse(faketime, DateTimeFormatter.ISO_DATE_TIME).toInstant();
106+
}
107+
}
108+
TestClock.set(Clock.fixed(fakeNow, ZoneOffset.UTC));
109+
}
110+
111+
List<String> resolvedArgs = new java.util.ArrayList<>();
112+
resolvedArgs.addAll(Arrays.asList(executeRequest.args));
113+
114+
int exitCode =
115+
new picocli.CommandLine(new Tuf()).execute(resolvedArgs.toArray(new String[0]));
116+
117+
Map<String, Object> responseMap =
118+
Map.of(
119+
"stdout", outContent.toString(StandardCharsets.UTF_8),
120+
"stderr", errContent.toString(StandardCharsets.UTF_8),
121+
"exitCode", exitCode);
122+
String jsonResponse = GSON.toJson(responseMap);
123+
124+
response.setStatus(HttpServletResponse.SC_OK);
125+
response.setContentType("application/json");
126+
byte[] responseBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
127+
response.setContentLength(responseBytes.length);
128+
129+
try (OutputStream os = response.getOutputStream()) {
130+
os.write(responseBytes);
131+
}
132+
} finally {
133+
System.setOut(originalOut);
134+
System.setErr(originalErr);
135+
TestClock.reset();
136+
}
137+
}
138+
139+
private static void handleHealthCheck(HttpServletResponse response) throws IOException {
140+
response.setStatus(HttpServletResponse.SC_OK);
141+
response.getWriter().println("OK");
142+
}
143+
}

tuf-cli/tuf-cli-server

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
set -o pipefail -o errexit -o nounset
4+
5+
CWD=$PWD
6+
7+
ARGS_JSON="["
8+
for arg in "$@"; do
9+
escaped_arg=$(echo -n "$arg" | jq -R -s '.')
10+
ARGS_JSON="$ARGS_JSON$escaped_arg,"
11+
done
12+
if [[ $ARGS_JSON == *, ]]; then
13+
ARGS_JSON="${ARGS_JSON%,}"
14+
fi
15+
ARGS_JSON="$ARGS_JSON]"
16+
17+
FAKETIME_VAL="${FAKETIME:-}"
18+
19+
JSON_PAYLOAD=$(jq -nc --arg cwd "$CWD" --argjson args "$ARGS_JSON" --arg faketime "$FAKETIME_VAL" '{"cwd": $cwd, "args": $args, "faketime": $faketime}')
20+
21+
RESPONSE=$(curl -s -X POST --header "Content-Type: application/json" --data-binary "$JSON_PAYLOAD" http://localhost:8080/execute)
22+
23+
STDOUT=$(echo "$RESPONSE" | jq -r .stdout)
24+
STDERR=$(echo "$RESPONSE" | jq -r .stderr)
25+
EXIT_CODE=$(echo "$RESPONSE" | jq .exitCode)
26+
27+
echo -n "$STDOUT"
28+
echo -n "$STDERR" >&2
29+
30+
exit "$EXIT_CODE"

tuf-cli/tuf-cli-server.xfails

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
test_metadata_bytes_match
2+
test_unusual_role_name[?]
3+
test_unusual_role_name[#]
4+
test_unusual_role_name[/delegatedrole]
5+
test_unusual_role_name[../delegatedrole]
6+
test_snapshot_rollback[basic]
7+
test_snapshot_rollback[with
8+
test_static_repository[tuf-on-ci-0.11]
9+
test_graph_traversal[basic-delegation]
10+
test_graph_traversal[single-level-delegations]
11+
test_graph_traversal[two-level-delegations]
12+
test_graph_traversal[two-level-test-DFS-order-of-traversal]
13+
test_graph_traversal[three-level-delegation-test-DFS-order-of-traversal]
14+
test_graph_traversal[two-level-terminating-ignores-all-but-roles-descendants]
15+
test_graph_traversal[three-level-terminating-ignores-all-but-roles-descendants]
16+
test_graph_traversal[two-level-ignores-all-branches-not-matching-paths]
17+
test_graph_traversal[three-level-ignores-all-branches-not-matching-paths]
18+
test_graph_traversal[cyclic-graph]
19+
test_graph_traversal[two-roles-delegating-to-a-third]
20+
test_graph_traversal[two-roles-delegating-to-a-third-different-paths]
21+
test_targetfile_search[targetpath matches wildcard]
22+
test_targetfile_search[targetpath with separators x]
23+
test_targetfile_search[targetpath with separators y]
24+
test_targetfile_search[targetpath is not delegated by all roles in the chain]
25+
test_snapshot_rollback[with hashes]

0 commit comments

Comments
 (0)