Skip to content

Commit 2eaaa55

Browse files
committed
Add timestamp client and verifier
Signed-off-by: Aaron Lew <64337293+aaronlew02@users.noreply.github.com>
1 parent eec57a5 commit 2eaaa55

23 files changed

+1378
-14
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@
3131
# except this icon
3232
!/.idea/icon.png
3333

34+
# vscode java output directories
35+
/bin/=
36+
/**/bin

sigstore-java/src/main/java/dev/sigstore/TrustedRootProvider.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.nio.file.Path;
2525
import java.security.InvalidKeyException;
2626
import java.security.NoSuchAlgorithmException;
27+
import java.security.cert.CertificateException;
2728
import java.security.spec.InvalidKeySpecException;
2829

2930
@FunctionalInterface
@@ -41,7 +42,8 @@ static TrustedRootProvider from(SigstoreTufClient.Builder tufClientBuilder) {
4142
} catch (IOException
4243
| NoSuchAlgorithmException
4344
| InvalidKeySpecException
44-
| InvalidKeyException ex) {
45+
| InvalidKeyException
46+
| CertificateException ex) {
4547
throw new SigstoreConfigurationException(ex);
4648
}
4749
};
@@ -52,7 +54,7 @@ static TrustedRootProvider from(Path trustedRoot) {
5254
return () -> {
5355
try (var is = Files.newInputStream(trustedRoot)) {
5456
return SigstoreTrustedRoot.from(is);
55-
} catch (IOException ex) {
57+
} catch (IOException | CertificateException ex) {
5658
throw new SigstoreConfigurationException(ex);
5759
}
5860
};

sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
import java.io.StringReader;
2222
import java.io.StringWriter;
2323
import java.nio.charset.StandardCharsets;
24-
import java.security.cert.*;
24+
import java.security.cert.CertPath;
25+
import java.security.cert.Certificate;
26+
import java.security.cert.CertificateException;
27+
import java.security.cert.CertificateFactory;
28+
import java.security.cert.X509Certificate;
2529
import java.time.temporal.ChronoUnit;
2630
import java.util.ArrayList;
2731
import java.util.Collections;
@@ -142,6 +146,12 @@ public static CertPath toCertPath(Certificate certificate) throws CertificateExc
142146
return cf.generateCertPath(Collections.singletonList(certificate));
143147
}
144148

149+
/** Converts a list of X509Certificates to a {@link CertPath}. */
150+
public static CertPath toCertPath(List<Certificate> certificate) throws CertificateException {
151+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
152+
return cf.generateCertPath(certificate);
153+
}
154+
145155
/** Appends an CertPath to another {@link CertPath} as children. */
146156
public static CertPath append(CertPath parent, CertPath child) throws CertificateException {
147157
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.timestamp.client;
17+
18+
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
19+
import org.bouncycastle.tsp.TSPAlgorithms;
20+
21+
/** Supported hash algorithms for timestamp requests. */
22+
public enum HashAlgorithm {
23+
SHA256("SHA256", TSPAlgorithms.SHA256),
24+
SHA384("SHA384", TSPAlgorithms.SHA384),
25+
SHA512("SHA512", TSPAlgorithms.SHA512);
26+
27+
private final String algorithmName;
28+
private final ASN1ObjectIdentifier oid;
29+
30+
HashAlgorithm(String algorithmName, ASN1ObjectIdentifier oid) {
31+
this.algorithmName = algorithmName;
32+
this.oid = oid;
33+
}
34+
35+
public String getAlgorithmName() {
36+
return algorithmName;
37+
}
38+
39+
public ASN1ObjectIdentifier getOid() {
40+
return oid;
41+
}
42+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.timestamp.client;
17+
18+
/** A client to communicate with a timestamp service instance. */
19+
public interface TimestampClient {
20+
/**
21+
* Request a timestanp for a timestamp authority.
22+
*
23+
* @param tsReq a structured request for a timestamp
24+
* @return a {@link TimestampResponse} from the timestamp authority
25+
*/
26+
TimestampResponse timestamp(TimestampRequest tsReq) throws TimestampException;
27+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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.timestamp.client;
17+
18+
import com.google.api.client.http.ByteArrayContent;
19+
import com.google.api.client.http.GenericUrl;
20+
import com.google.api.client.http.HttpRequestFactory;
21+
import com.google.api.client.http.HttpResponse;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import dev.sigstore.http.HttpClients;
24+
import dev.sigstore.http.HttpParams;
25+
import dev.sigstore.http.ImmutableHttpParams;
26+
import java.io.IOException;
27+
import java.net.URI;
28+
import java.util.Locale;
29+
import java.util.Objects;
30+
import org.bouncycastle.tsp.TSPException;
31+
import org.bouncycastle.tsp.TimeStampRequest;
32+
import org.bouncycastle.tsp.TimeStampRequestGenerator;
33+
import org.bouncycastle.tsp.TimeStampResponse;
34+
35+
/** A client to communicate with a timestamp service instance. */
36+
public class TimestampClientHttp implements TimestampClient {
37+
private static final URI SIGSTORE_TSA_URI =
38+
URI.create("https://timestamp.sigstage.dev/api/v1/timestamp");
39+
private static final String CONTENT_TYPE_TIMESTAMP_QUERY = "application/timestamp-query";
40+
private static final String ACCEPT_TYPE_TIMESTAMP_REPLY = "application/timestamp-reply";
41+
42+
private final HttpRequestFactory requestFactory;
43+
private final URI uri;
44+
45+
public static TimestampClientHttp.Builder builder() {
46+
return new TimestampClientHttp.Builder();
47+
}
48+
49+
@VisibleForTesting
50+
TimestampClientHttp(HttpRequestFactory requestFactory, URI uri) {
51+
this.requestFactory = requestFactory;
52+
this.uri = uri;
53+
}
54+
55+
public static class Builder {
56+
private HttpParams httpParams = ImmutableHttpParams.builder().build();
57+
private URI uri = SIGSTORE_TSA_URI;
58+
59+
private Builder() {}
60+
61+
/** Configure the http properties, see {@link HttpParams}, {@link ImmutableHttpParams}. */
62+
public Builder setHttpParams(HttpParams httpParams) {
63+
this.httpParams = httpParams;
64+
return this;
65+
}
66+
67+
/** Base url of the timestamp authority. */
68+
public Builder setUri(URI uri) {
69+
this.uri = uri;
70+
return this;
71+
}
72+
73+
public TimestampClientHttp build() throws IOException {
74+
var requestFactory = HttpClients.newRequestFactory(httpParams);
75+
return new TimestampClientHttp(requestFactory, uri);
76+
}
77+
}
78+
79+
@Override
80+
public TimestampResponse timestamp(TimestampRequest tsReq) throws TimestampException {
81+
TimeStampRequestGenerator bcTsReqGen = new TimeStampRequestGenerator();
82+
83+
// Prepare and send the timestamp request
84+
var bcAlgorithmOid = tsReq.getHashAlgorithm().getOid();
85+
var artifactHashBytes = tsReq.getHash();
86+
var nonce = tsReq.getNonce();
87+
bcTsReqGen.setCertReq(tsReq.requestCertificates());
88+
TimeStampRequest bcTsReq;
89+
HttpResponse httpTsResp;
90+
try {
91+
bcTsReq = bcTsReqGen.generate(bcAlgorithmOid, artifactHashBytes, nonce);
92+
var requestBytes = bcTsReq.getEncoded();
93+
httpTsResp = sendTimestampRequest(uri, requestBytes);
94+
} catch (IOException e) {
95+
throw new TimestampException("Timestamp request failed: " + e.getMessage(), e);
96+
}
97+
98+
// Parse the timestamp response
99+
TimestampResponse tsResp;
100+
try {
101+
var bcTsResp = getBcTimestampResponse(httpTsResp, bcTsReq);
102+
var tsRespBytes = bcTsResp.getEncoded();
103+
tsResp = ImmutableTimestampResponse.builder().encoded(tsRespBytes).build();
104+
} catch (IOException | TSPException e) {
105+
throw new TimestampException(
106+
"Timestamp response validation or parsing failed: " + e.getMessage(), e);
107+
}
108+
109+
return tsResp;
110+
}
111+
112+
HttpResponse sendTimestampRequest(URI tsaUri, byte[] requestBytes) throws IOException {
113+
Objects.requireNonNull(tsaUri, "tsaUri cannot be null");
114+
Objects.requireNonNull(requestBytes, "requestBytes cannot be null");
115+
var httpReq =
116+
requestFactory.buildPostRequest(
117+
new GenericUrl(tsaUri),
118+
new ByteArrayContent(CONTENT_TYPE_TIMESTAMP_QUERY, requestBytes));
119+
httpReq.getHeaders().setAccept(ACCEPT_TYPE_TIMESTAMP_REPLY);
120+
httpReq.setThrowExceptionOnExecuteError(false);
121+
// Skip exception thrown by API to manually handle error code below
122+
httpReq.setNumberOfRetries(5);
123+
var httpResp = httpReq.execute();
124+
if (!(httpResp.getStatusCode() >= 200 && httpResp.getStatusCode() < 300)) {
125+
throw new IOException(
126+
String.format(
127+
Locale.ROOT,
128+
"bad response from timestamp @ '%s' : %s",
129+
tsaUri,
130+
httpResp.parseAsString()));
131+
}
132+
return httpResp;
133+
}
134+
135+
private TimeStampResponse getBcTimestampResponse(
136+
HttpResponse httpTsResp, TimeStampRequest bcTsReq) throws IOException, TSPException {
137+
Objects.requireNonNull(httpTsResp, "HttpResponse cannot be null");
138+
Objects.requireNonNull(bcTsReq, "TimeStampRequest cannot be null");
139+
140+
var bcTsResp = new TimeStampResponse(httpTsResp.getContent());
141+
bcTsResp.validate(bcTsReq);
142+
return bcTsResp;
143+
}
144+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.timestamp.client;
17+
18+
public class TimestampException extends Exception {
19+
public TimestampException(String message) {
20+
super(message);
21+
}
22+
23+
public TimestampException(String message, Throwable cause) {
24+
super(message, cause);
25+
}
26+
27+
public TimestampException(Throwable cause) {
28+
super(cause);
29+
}
30+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.timestamp.client;
17+
18+
import java.math.BigInteger;
19+
import java.security.SecureRandom;
20+
import org.immutables.value.Value;
21+
import org.immutables.value.Value.Immutable;
22+
23+
@Immutable
24+
public interface TimestampRequest {
25+
/** The hash algorithm used to hash the artifact. */
26+
HashAlgorithm getHashAlgorithm();
27+
28+
/**
29+
* The hash of the artifact to be timestamped. For sigstore-java, this typically refers to the
30+
* hash of the signature (not the original artifact's hash) in a signing event.
31+
*/
32+
byte[] getHash();
33+
34+
/** A nonce to prevent replay attacks. Defaults to a 64-bit random number. */
35+
@Value.Default
36+
default BigInteger getNonce() {
37+
return new BigInteger(64, new SecureRandom());
38+
}
39+
40+
/** Whether or not to include certificates in the response. Defaults to {@code false}. */
41+
@Value.Default
42+
default Boolean requestCertificates() {
43+
return false;
44+
}
45+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.timestamp.client;
17+
18+
import org.immutables.value.Value.Immutable;
19+
20+
@Immutable
21+
public interface TimestampResponse {
22+
/** The ASN.1 encoded representation of the timestamp response. */
23+
byte[] getEncoded();
24+
}

0 commit comments

Comments
 (0)