Skip to content

Commit af40879

Browse files
authored
Merge pull request #979 from sigstore/timestamp-keyless
Add RFC3161 timestamps to keyless signer and verifier
2 parents a54d7c5 + 3df4c8d commit af40879

File tree

6 files changed

+224
-17
lines changed

6 files changed

+224
-17
lines changed

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import dev.sigstore.bundle.Bundle.HashAlgorithm;
2727
import dev.sigstore.bundle.Bundle.MessageSignature;
2828
import dev.sigstore.bundle.ImmutableBundle;
29+
import dev.sigstore.bundle.ImmutableTimestamp;
2930
import dev.sigstore.encryption.certificates.Certificates;
3031
import dev.sigstore.encryption.signers.Signer;
3132
import dev.sigstore.encryption.signers.Signers;
@@ -46,6 +47,13 @@
4647
import dev.sigstore.rekor.client.RekorResponse;
4748
import dev.sigstore.rekor.client.RekorVerificationException;
4849
import dev.sigstore.rekor.client.RekorVerifier;
50+
import dev.sigstore.timestamp.client.ImmutableTimestampRequest;
51+
import dev.sigstore.timestamp.client.TimestampClient;
52+
import dev.sigstore.timestamp.client.TimestampClientHttp;
53+
import dev.sigstore.timestamp.client.TimestampException;
54+
import dev.sigstore.timestamp.client.TimestampResponse;
55+
import dev.sigstore.timestamp.client.TimestampVerificationException;
56+
import dev.sigstore.timestamp.client.TimestampVerifier;
4957
import dev.sigstore.trustroot.SigstoreConfigurationException;
5058
import dev.sigstore.tuf.SigstoreTufClient;
5159
import java.io.IOException;
@@ -89,6 +97,8 @@ public class KeylessSigner implements AutoCloseable {
8997
private final FulcioVerifier fulcioVerifier;
9098
private final RekorClient rekorClient;
9199
private final RekorVerifier rekorVerifier;
100+
private final TimestampClient timestampClient;
101+
private final TimestampVerifier timestampVerifier;
92102
private final OidcClients oidcClients;
93103
private final List<OidcTokenMatcher> oidcIdentities;
94104
private final Signer signer;
@@ -114,6 +124,8 @@ private KeylessSigner(
114124
FulcioVerifier fulcioVerifier,
115125
RekorClient rekorClient,
116126
RekorVerifier rekorVerifier,
127+
TimestampClient timestampClient,
128+
TimestampVerifier timestampVerifier,
117129
OidcClients oidcClients,
118130
List<OidcTokenMatcher> oidcIdentities,
119131
Signer signer,
@@ -122,6 +134,8 @@ private KeylessSigner(
122134
this.fulcioVerifier = fulcioVerifier;
123135
this.rekorClient = rekorClient;
124136
this.rekorVerifier = rekorVerifier;
137+
this.timestampClient = timestampClient;
138+
this.timestampVerifier = timestampVerifier;
125139
this.oidcClients = oidcClients;
126140
this.oidcIdentities = oidcIdentities;
127141
this.signer = signer;
@@ -152,6 +166,7 @@ public static class Builder {
152166
private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME;
153167
private URI fulcioUri;
154168
private URI rekorUri;
169+
private URI timestampUri;
155170

156171
@CanIgnoreReturnValue
157172
public Builder fulcioUrl(URI uri) {
@@ -165,6 +180,12 @@ public Builder rekorUrl(URI uri) {
165180
return this;
166181
}
167182

183+
@CanIgnoreReturnValue
184+
public Builder timestampUrl(URI uri) {
185+
this.timestampUri = uri;
186+
return this;
187+
}
188+
168189
@CanIgnoreReturnValue
169190
public Builder trustedRootProvider(TrustedRootProvider trustedRootProvider) {
170191
this.trustedRootProvider = trustedRootProvider;
@@ -233,11 +254,19 @@ public KeylessSigner build()
233254
var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot);
234255
var rekorClient = RekorClientHttp.builder().setUri(rekorUri).build();
235256
var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot);
257+
TimestampClient timestampClient = null;
258+
TimestampVerifier timestampVerifier = null;
259+
if (timestampUri != null) { // Building with staging defaults
260+
timestampClient = TimestampClientHttp.builder().setUri(timestampUri).build();
261+
timestampVerifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
262+
}
236263
return new KeylessSigner(
237264
fulcioClient,
238265
fulcioVerifier,
239266
rekorClient,
240267
rekorVerifier,
268+
timestampClient,
269+
timestampVerifier,
241270
oidcClients,
242271
oidcIdentities,
243272
signer,
@@ -270,6 +299,7 @@ public Builder sigstoreStagingDefaults() {
270299
trustedRootProvider = TrustedRootProvider.from(sigstoreTufClientBuilder);
271300
fulcioUri = FulcioClient.STAGING_URI;
272301
rekorUri = RekorClient.STAGING_URI;
302+
timestampUri = TimestampClient.STAGING_URI;
273303
oidcClients(OidcClients.STAGING);
274304
signer(Signers.newEcdsaSigner());
275305
minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME);
@@ -355,13 +385,43 @@ public List<Bundle> sign(List<byte[]> artifactDigests) throws KeylessSignerExcep
355385
throw new KeylessSignerException("Failed to validate rekor response after signing", ex);
356386
}
357387

358-
result.add(
388+
var bundleBuilder =
359389
ImmutableBundle.builder()
360390
.certPath(signingCert)
361391
.addEntries(rekorResponse.getEntry())
362392
.messageSignature(
363-
MessageSignature.of(HashAlgorithm.SHA2_256, artifactDigest, signature))
364-
.build());
393+
MessageSignature.of(HashAlgorithm.SHA2_256, artifactDigest, signature));
394+
395+
// Timestamp functionality only enabled if timestampUri is provided
396+
if (timestampClient != null && timestampVerifier != null) {
397+
var signatureDigest = Hashing.sha256().hashBytes(signature).asBytes();
398+
399+
var tsReq =
400+
ImmutableTimestampRequest.builder()
401+
.hashAlgorithm(dev.sigstore.timestamp.client.HashAlgorithm.SHA256)
402+
.hash(signatureDigest)
403+
.build();
404+
405+
TimestampResponse tsResp;
406+
try {
407+
tsResp = timestampClient.timestamp(tsReq);
408+
} catch (TimestampException ex) {
409+
throw new KeylessSignerException("Failed to generate timestamp", ex);
410+
}
411+
412+
try {
413+
timestampVerifier.verify(tsResp, signature);
414+
} catch (TimestampVerificationException ex) {
415+
throw new KeylessSignerException("Returned timestamp was invalid", ex);
416+
}
417+
418+
Bundle.Timestamp timestamp =
419+
ImmutableTimestamp.builder().rfc3161Timestamp(tsResp.getEncoded()).build();
420+
421+
bundleBuilder.addTimestamps(timestamp);
422+
}
423+
424+
result.add(bundleBuilder.build());
365425
}
366426
return result.build();
367427
}

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
import dev.sigstore.rekor.client.RekorVerifier;
3838
import dev.sigstore.rekor.dsse.v0_0_1.Dsse;
3939
import dev.sigstore.rekor.dsse.v0_0_1.PayloadHash;
40+
import dev.sigstore.timestamp.client.ImmutableTimestampResponse;
41+
import dev.sigstore.timestamp.client.TimestampException;
42+
import dev.sigstore.timestamp.client.TimestampVerificationException;
43+
import dev.sigstore.timestamp.client.TimestampVerifier;
4044
import dev.sigstore.trustroot.SigstoreConfigurationException;
4145
import dev.sigstore.tuf.SigstoreTufClient;
4246
import java.io.IOException;
@@ -51,9 +55,9 @@
5155
import java.security.cert.CertificateNotYetValidException;
5256
import java.security.cert.X509Certificate;
5357
import java.security.spec.InvalidKeySpecException;
54-
import java.sql.Date;
5558
import java.util.Arrays;
5659
import java.util.Base64;
60+
import java.util.Date;
5761
import java.util.List;
5862
import java.util.Objects;
5963
import java.util.stream.Collectors;
@@ -65,10 +69,15 @@ public class KeylessVerifier {
6569

6670
private final FulcioVerifier fulcioVerifier;
6771
private final RekorVerifier rekorVerifier;
72+
private final TimestampVerifier timestampVerifier;
6873

69-
private KeylessVerifier(FulcioVerifier fulcioVerifier, RekorVerifier rekorVerifier) {
74+
private KeylessVerifier(
75+
FulcioVerifier fulcioVerifier,
76+
RekorVerifier rekorVerifier,
77+
TimestampVerifier timestampVerifier) {
7078
this.fulcioVerifier = fulcioVerifier;
7179
this.rekorVerifier = rekorVerifier;
80+
this.timestampVerifier = timestampVerifier;
7281
}
7382

7483
public static KeylessVerifier.Builder builder() {
@@ -89,7 +98,8 @@ public KeylessVerifier build()
8998
var trustedRoot = trustedRootProvider.get();
9099
var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot);
91100
var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot);
92-
return new KeylessVerifier(fulcioVerifier, rekorVerifier);
101+
var timestampVerifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
102+
return new KeylessVerifier(fulcioVerifier, rekorVerifier, timestampVerifier);
93103
}
94104

95105
public Builder sigstorePublicDefaults() {
@@ -148,11 +158,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
148158
"Bundle verification expects 1 entry, but found " + bundle.getEntries().size());
149159
}
150160

151-
if (!bundle.getTimestamps().isEmpty()) {
152-
throw new KeylessVerificationException(
153-
"Cannot verify bundles with timestamp verification material");
154-
}
155-
156161
var signingCert = bundle.getCertPath();
157162
var leafCert = Certificates.getLeaf(signingCert);
158163

@@ -188,11 +193,43 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
188193
throw new KeylessVerificationException("Signing time was after certificate expiry", e);
189194
}
190195

196+
byte[] signature;
191197
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
192-
checkMessageSignature(
193-
bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
198+
var messageSignature = bundle.getMessageSignature().get();
199+
checkMessageSignature(messageSignature, rekorEntry, artifactDigest, leafCert);
200+
signature = messageSignature.getSignature();
194201
} else { // dsse
195-
checkDsseEnvelope(rekorEntry, bundle.getDsseEnvelope().get(), artifactDigest, leafCert);
202+
var dsseEnvelope = bundle.getDsseEnvelope().get();
203+
checkDsseEnvelope(rekorEntry, dsseEnvelope, artifactDigest, leafCert);
204+
signature = dsseEnvelope.getSignature();
205+
}
206+
207+
verifyTimestamps(leafCert, bundle.getTimestamps(), signature);
208+
}
209+
210+
private void verifyTimestamps(
211+
X509Certificate leafCert, List<Bundle.Timestamp> timestamps, byte[] signature)
212+
throws KeylessVerificationException {
213+
if (timestamps == null || timestamps.isEmpty()) {
214+
return;
215+
}
216+
for (Bundle.Timestamp timestamp : timestamps) {
217+
byte[] tsBytes = timestamp.getRfc3161Timestamp();
218+
if (tsBytes == null || tsBytes.length == 0) {
219+
throw new KeylessVerificationException(
220+
"Found an empty or null RFC3161 timestamp in bundle");
221+
}
222+
try {
223+
var tsResp = ImmutableTimestampResponse.builder().encoded(tsBytes).build();
224+
timestampVerifier.verify(tsResp, signature);
225+
leafCert.checkValidity(tsResp.getGenTime());
226+
} catch (TimestampException
227+
| CertificateNotYetValidException
228+
| CertificateExpiredException
229+
| TimestampVerificationException e) {
230+
throw new KeylessVerificationException(
231+
"RFC3161 timestamp verification failed: " + e.getMessage(), e);
232+
}
196233
}
197234
}
198235

sigstore-java/src/main/java/dev/sigstore/timestamp/client/TimestampClient.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
*/
1616
package dev.sigstore.timestamp.client;
1717

18+
import java.net.URI;
19+
1820
/** A client to communicate with a timestamp service instance. */
1921
public interface TimestampClient {
22+
URI STAGING_URI = URI.create("https://timestamp.sigstage.dev/api/v1/timestamp");
23+
2024
/**
2125
* Request a timestanp for a timestamp authority.
2226
*

sigstore-java/src/main/java/dev/sigstore/timestamp/client/TimestampClientHttp.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434

3535
/** A client to communicate with a timestamp service instance. */
3636
public class TimestampClientHttp implements TimestampClient {
37-
private static final URI SIGSTORE_TSA_URI =
38-
URI.create("https://timestamp.sigstage.dev/api/v1/timestamp");
3937
private static final String CONTENT_TYPE_TIMESTAMP_QUERY = "application/timestamp-query";
4038
private static final String ACCEPT_TYPE_TIMESTAMP_REPLY = "application/timestamp-reply";
4139

@@ -54,7 +52,7 @@ public static TimestampClientHttp.Builder builder() {
5452

5553
public static class Builder {
5654
private HttpParams httpParams = ImmutableHttpParams.builder().build();
57-
private URI uri = SIGSTORE_TSA_URI;
55+
private URI uri = TimestampClient.STAGING_URI;
5856

5957
private Builder() {}
6058

sigstore-java/src/main/java/dev/sigstore/timestamp/client/TimestampResponse.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,25 @@
1515
*/
1616
package dev.sigstore.timestamp.client;
1717

18+
import java.io.IOException;
19+
import java.util.Date;
20+
import org.bouncycastle.tsp.TSPException;
21+
import org.bouncycastle.tsp.TimeStampResponse;
1822
import org.immutables.value.Value.Immutable;
23+
import org.immutables.value.Value.Lazy;
1924

2025
@Immutable
2126
public interface TimestampResponse {
2227
/** The ASN.1 encoded representation of the timestamp response. */
2328
byte[] getEncoded();
29+
30+
@Lazy
31+
default Date getGenTime() throws TimestampException {
32+
try {
33+
var bcTsResp = new TimeStampResponse(getEncoded());
34+
return bcTsResp.getTimeStampToken().getTimeStampInfo().getGenTime();
35+
} catch (TSPException | IOException e) {
36+
throw new TimestampException("Failed to retrieve timestamp generation time", e);
37+
}
38+
}
2439
}

0 commit comments

Comments
 (0)