Skip to content

Commit 93f902f

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

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.ZoneOffset;
31+
import java.util.Arrays;
32+
import java.util.List;
33+
import java.util.Map;
34+
import org.eclipse.jetty.server.Request;
35+
import org.eclipse.jetty.server.Server;
36+
import org.eclipse.jetty.server.handler.AbstractHandler;
37+
38+
public class TufConformanceServer {
39+
40+
private static final Gson GSON = new Gson();
41+
private static boolean debug = false;
42+
43+
private static class ExecuteRequest {
44+
String[] args;
45+
String faketime;
46+
}
47+
48+
public static void main(String[] args) throws Exception {
49+
if (args.length > 0 && "--debug".equals(args[0])) {
50+
debug = true;
51+
}
52+
int port = 8080;
53+
Server server = new Server(port);
54+
server.setHandler(new TufConformanceHandler());
55+
server.start();
56+
server.join();
57+
}
58+
59+
public static class TufConformanceHandler extends AbstractHandler {
60+
@Override
61+
public void handle(
62+
String target,
63+
Request baseRequest,
64+
HttpServletRequest request,
65+
HttpServletResponse response)
66+
throws IOException, ServletException {
67+
if ("/".equals(target)) {
68+
handleHealthCheck(response);
69+
} else if ("/execute".equals(target) && "POST".equals(request.getMethod())) {
70+
handleExecute(request, response);
71+
}
72+
baseRequest.setHandled(true);
73+
}
74+
}
75+
76+
private static void handleExecute(HttpServletRequest request, HttpServletResponse response)
77+
throws IOException {
78+
ExecuteRequest executeRequest;
79+
try (InputStream is = request.getInputStream()) {
80+
String requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8);
81+
executeRequest = GSON.fromJson(requestBody, ExecuteRequest.class);
82+
}
83+
84+
// Tests should not be run in parallel, to ensure orderly input/output
85+
PrintStream originalOut = System.out;
86+
PrintStream originalErr = System.err;
87+
88+
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
89+
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
90+
91+
try (PrintStream outPs = new PrintStream(outContent, true, StandardCharsets.UTF_8);
92+
PrintStream errPs = new PrintStream(errContent, true, StandardCharsets.UTF_8)) {
93+
if (!debug) {
94+
System.setOut(outPs);
95+
System.setErr(errPs);
96+
}
97+
98+
try {
99+
Instant fakeNow = Instant.ofEpochSecond(Long.parseLong(executeRequest.faketime));
100+
TestClock.set(Clock.fixed(fakeNow, ZoneOffset.UTC));
101+
} catch (NumberFormatException e) {
102+
throw new IllegalArgumentException(
103+
"Invalid 'faketime' format. Must be a long representing epoch seconds, but was: '"
104+
+ executeRequest.faketime
105+
+ "'",
106+
e);
107+
}
108+
109+
List<String> resolvedArgs = new java.util.ArrayList<>();
110+
resolvedArgs.addAll(Arrays.asList(executeRequest.args));
111+
112+
int exitCode =
113+
new picocli.CommandLine(new Tuf()).execute(resolvedArgs.toArray(new String[0]));
114+
115+
Map<String, Object> responseMap =
116+
Map.of(
117+
"stdout", outContent.toString(StandardCharsets.UTF_8),
118+
"stderr", errContent.toString(StandardCharsets.UTF_8),
119+
"exitCode", exitCode);
120+
String jsonResponse = GSON.toJson(responseMap);
121+
122+
response.setStatus(HttpServletResponse.SC_OK);
123+
response.setContentType("application/json");
124+
byte[] responseBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
125+
response.setContentLength(responseBytes.length);
126+
127+
try (OutputStream os = response.getOutputStream()) {
128+
os.write(responseBytes);
129+
}
130+
} finally {
131+
if (!debug) {
132+
System.setOut(originalOut);
133+
System.setErr(originalErr);
134+
}
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+
DATE_VAL="$(date +%s)"
18+
19+
JSON_PAYLOAD=$(jq -nc --arg cwd "$CWD" --argjson args "$ARGS_JSON" --arg faketime "$DATE_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)