Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sigstore-cli/src/main/java/dev/sigstore/cli/Sign.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import dev.sigstore.oidc.client.TokenStringOidcClient;
import dev.sigstore.tuf.RootProvider;
import dev.sigstore.tuf.SigstoreTufClient;
import java.net.URL;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -88,7 +88,7 @@ public Integer call() throws Exception {
SigstoreTufClient.builder()
.usePublicGoodInstance()
.tufMirror(
new URL(target.publicGoodWithTufUrlOverride),
URI.create(target.publicGoodWithTufUrlOverride),
RootProvider.fromResource(SigstoreTufClient.PUBLIC_GOOD_ROOT_RESOURCE));
signerBuilder =
KeylessSigner.builder()
Expand All @@ -99,7 +99,7 @@ public Integer call() throws Exception {
SigstoreTufClient.builder()
.useStagingInstance()
.tufMirror(
new URL(target.stagingWithTufUrlOverride),
URI.create(target.stagingWithTufUrlOverride),
RootProvider.fromResource(SigstoreTufClient.STAGING_ROOT_RESOURCE));
signerBuilder =
KeylessSigner.builder()
Expand Down
6 changes: 3 additions & 3 deletions sigstore-cli/src/main/java/dev/sigstore/cli/Verify.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import dev.sigstore.strings.StringMatcher;
import dev.sigstore.tuf.RootProvider;
import dev.sigstore.tuf.SigstoreTufClient;
import java.net.URL;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.Callable;
Expand Down Expand Up @@ -141,7 +141,7 @@ public Integer call() throws Exception {
SigstoreTufClient.builder()
.usePublicGoodInstance()
.tufMirror(
new URL(target.publicGoodWithTufUrlOverride),
URI.create(target.publicGoodWithTufUrlOverride),
RootProvider.fromResource(SigstoreTufClient.PUBLIC_GOOD_ROOT_RESOURCE));
verifier =
KeylessVerifier.builder()
Expand All @@ -152,7 +152,7 @@ public Integer call() throws Exception {
SigstoreTufClient.builder()
.useStagingInstance()
.tufMirror(
new URL(target.stagingWithTufUrlOverride),
URI.create(target.stagingWithTufUrlOverride),
RootProvider.fromResource(SigstoreTufClient.STAGING_ROOT_RESOURCE));
verifier =
KeylessVerifier.builder()
Expand Down
73 changes: 73 additions & 0 deletions sigstore-java/src/main/java/dev/sigstore/http/URIFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2025 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.http;

import java.net.URI;
import java.net.URISyntaxException;

/**
* A utility class for formatting URIs, providing predictable path appending.
*
* <p>This is preferable to {@link java.net.URI#resolve(String)} for simple path appending, as it
* avoids {@code resolve()}'s specific handling of base paths without trailing slashes and appended
* paths with leading slashes.
*/
public final class URIFormat {

private URIFormat() {}

/**
* Ensures the given URI's path has a trailing slash. This method correctly handles URIs with
* query parameters and fragments.
*
* @param input the URI to check.
* @return a new URI with a trailing slash, or the original URI if it already had one.
*/
public static URI addTrailingSlash(URI input) {
String path = input.getPath();
if (path == null || path.isEmpty()) {
path = "";
} else if (path.endsWith("/")) {
return input;
}
try {
return new URI(
input.getScheme(),
input.getAuthority(),
path + "/",
input.getQuery(),
input.getFragment());
} catch (URISyntaxException e) {
// This should be unreachable with a valid input URI
throw new IllegalStateException("Could not append slash to invalid URI: " + input, e);
}
}

/**
* Appends a path segment to a base URI, ensuring exactly one slash separates them. This method
* will erase any query parameters or fragments
*
* @param base the base URI (e.g., "http://example.com/api?key=1").
* @param path the path segment to append (e.g., "users" or "/users").
* @return a new URI with the path appended (e.g., "http://example.com/api/users").
*/
public static URI appendPath(URI base, String path) {
String relativePath = path.replaceAll("^/+", "");

// resolve has some goofy behavior unless we normalize everything before applying
return addTrailingSlash(base).resolve(relativePath);
}
}
16 changes: 7 additions & 9 deletions sigstore-java/src/main/java/dev/sigstore/tuf/HttpFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,27 @@
import com.google.api.client.json.gson.GsonFactory;
import dev.sigstore.http.HttpClients;
import dev.sigstore.http.HttpParams;
import dev.sigstore.http.URIFormat;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.util.Locale;

public class HttpFetcher implements Fetcher {

private final URL mirror;
private final URI mirror;
private final HttpRequestFactory requestFactory;

private HttpFetcher(URL mirror, HttpRequestFactory requestFactory) {
private HttpFetcher(URI mirror, HttpRequestFactory requestFactory) {
this.mirror = mirror;
this.requestFactory = requestFactory;
}

public static HttpFetcher newFetcher(URL mirror) throws IOException {
public static HttpFetcher newFetcher(URI mirror) throws IOException {
var requestFactory =
HttpClients.newRequestFactory(
HttpParams.builder().build(),
GsonFactory.getDefaultInstance().createJsonObjectParser());
if (mirror.toString().endsWith("/")) {
return new HttpFetcher(mirror, requestFactory);
}
return new HttpFetcher(new URL(mirror.toExternalForm() + "/"), requestFactory);
return new HttpFetcher(URIFormat.addTrailingSlash(mirror), requestFactory);
}

@Override
Expand All @@ -53,7 +51,7 @@ public String getSource() {
@Override
public byte[] fetchResource(String filename, int maxLength)
throws IOException, FileExceedsMaxLengthException {
GenericUrl fileUrl = new GenericUrl(mirror + filename);
GenericUrl fileUrl = new GenericUrl(URIFormat.appendPath(mirror, filename));
var req = requestFactory.buildGetRequest(fileUrl);
req.getHeaders().setAccept("application/json; api-version=2.0");
req.getHeaders().setContentType("application/json");
Expand Down
37 changes: 12 additions & 25 deletions sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import dev.sigstore.http.URIFormat;
import dev.sigstore.trustroot.SigstoreConfigurationException;
import dev.sigstore.trustroot.SigstoreSigningConfig;
import dev.sigstore.trustroot.SigstoreTrustedRoot;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
Expand Down Expand Up @@ -65,21 +65,17 @@ public static class Builder {
Path tufCacheLocation =
Path.of(System.getProperty("user.home")).resolve(".sigstore-java").resolve("root");

private URL remoteMirror;
private URI remoteMirror;
private RootProvider trustedRoot;

public Builder usePublicGoodInstance() {
if (remoteMirror != null || trustedRoot != null) {
throw new IllegalStateException(
"Using public good after configuring remoteMirror and trustedRoot");
}
try {
tufMirror(
new URL("https://tuf-repo-cdn.sigstore.dev/"),
RootProvider.fromResource(PUBLIC_GOOD_ROOT_RESOURCE));
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
tufMirror(
URI.create("https://tuf-repo-cdn.sigstore.dev/"),
RootProvider.fromResource(PUBLIC_GOOD_ROOT_RESOURCE));
return this;
}

Expand All @@ -88,13 +84,9 @@ public Builder useStagingInstance() {
throw new IllegalStateException(
"Using staging after configuring remoteMirror and trustedRoot");
}
try {
tufMirror(
new URL("https://tuf-repo-cdn.sigstage.dev"),
RootProvider.fromResource(STAGING_ROOT_RESOURCE));
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
tufMirror(
URI.create("https://tuf-repo-cdn.sigstage.dev"),
RootProvider.fromResource(STAGING_ROOT_RESOURCE));
tufCacheLocation =
Path.of(System.getProperty("user.home"))
.resolve(".sigstore-java")
Expand All @@ -103,7 +95,7 @@ public Builder useStagingInstance() {
return this;
}

public Builder tufMirror(URL mirror, RootProvider trustedRoot) {
public Builder tufMirror(URI mirror, RootProvider trustedRoot) {
this.remoteMirror = mirror;
this.trustedRoot = trustedRoot;
return this;
Expand All @@ -126,11 +118,7 @@ public SigstoreTufClient build() throws IOException {
if (!Files.isDirectory(tufCacheLocation)) {
Files.createDirectories(tufCacheLocation);
}
var normalizedRemoteMirror =
remoteMirror.toString().endsWith("/")
? remoteMirror
: new URL(remoteMirror.toExternalForm() + "/");
var remoteTargetsLocation = new URL(normalizedRemoteMirror.toExternalForm() + "targets");
var remoteTargetsLocation = URIFormat.appendPath(remoteMirror, "targets");
var filesystemTufStore = FileSystemTufStore.newFileSystemStore(tufCacheLocation);
var tufUpdater =
Updater.builder()
Expand All @@ -139,8 +127,7 @@ public SigstoreTufClient build() throws IOException {
TrustedMetaStore.newTrustedMetaStore(
PassthroughCacheMetaStore.newPassthroughMetaCache(filesystemTufStore)))
.setTargetStore(filesystemTufStore)
.setMetaFetcher(
MetaFetcher.newFetcher(HttpFetcher.newFetcher(normalizedRemoteMirror)))
.setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(remoteMirror)))
.setTargetFetcher(HttpFetcher.newFetcher(remoteTargetsLocation))
.build();
return new SigstoreTufClient(tufUpdater, cacheValidity);
Expand Down
93 changes: 93 additions & 0 deletions sigstore-java/src/test/java/dev/sigstore/http/URIFormatTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2025 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.http;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URI;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class URIFormatTest {

// Data provider for addTrailingSlash tests
static Stream<Arguments> addTrailingSlashCases() {
return Stream.of(
Arguments.of(
URI.create("https://example.com/path/"), URI.create("https://example.com/path/")),
Arguments.of(
URI.create("https://example.com/path"), URI.create("https://example.com/path/")),
Arguments.of(URI.create("https://example.com"), URI.create("https://example.com/")),
Arguments.of(
URI.create("https://example.com/path?query=1"),
URI.create("https://example.com/path/?query=1")),
Arguments.of(
URI.create("https://example.com/path#fragment"),
URI.create("https://example.com/path/#fragment")),
Arguments.of(
URI.create("https://example.com/path?query=1#fragment"),
URI.create("https://example.com/path/?query=1#fragment")));
}

// Data provider for appendPath tests
static Stream<Arguments> appendPathCases() {
return Stream.of(
Arguments.of(
URI.create("https://example.com/api/"),
"users",
URI.create("https://example.com/api/users")),
Arguments.of(
URI.create("https://example.com/api"),
"users",
URI.create("https://example.com/api/users")),
Arguments.of(
URI.create("https://example.com/api/"),
"/users",
URI.create("https://example.com/api/users")),
Arguments.of(
URI.create("https://example.com/api"),
"///users",
URI.create("https://example.com/api/users")),
Arguments.of(
URI.create("https://example.com/api?key=123"),
"users",
URI.create("https://example.com/api/users")),
Arguments.of(
URI.create("https://example.com/api?key=123#section"),
"users",
URI.create("https://example.com/api/users")),
Arguments.of(
URI.create("https://example.com"),
"/users/get",
URI.create("https://example.com/users/get")));
}

@ParameterizedTest
@MethodSource("addTrailingSlashCases")
void addTrailingSlash(URI input, URI expected) {
URI result = URIFormat.addTrailingSlash(input);
assertEquals(expected, result);
}

@ParameterizedTest
@MethodSource("appendPathCases")
void appendPath(URI base, String path, URI expected) {
URI result = URIFormat.appendPath(base, path);
assertEquals(expected, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/
package dev.sigstore.tuf;

import java.net.URL;
import java.io.IOException;
import java.net.URI;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
Expand All @@ -24,8 +25,8 @@ class HttpFetcherTest {

@ParameterizedTest
@CsvSource({"http://example.com", "http://example.com/"})
public void newFetcher_urlNoTrailingSlash(String url) throws Exception {
var fetcher = HttpFetcher.newFetcher(new URL(url));
public void newFetcher_urlNoTrailingSlash(String url) throws IOException {
var fetcher = HttpFetcher.newFetcher(URI.create(url));
Assertions.assertEquals("http://example.com/", fetcher.getSource());
}
}
Loading
Loading