Skip to content

Commit e25c793

Browse files
committed
Add timestamps to keyless signer and verifier
Signed-off-by: Aaron Lew <64337293+aaronlew02@users.noreply.github.com>
1 parent 22b630a commit e25c793

4 files changed

Lines changed: 221 additions & 17 deletions

File tree

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+
if (timestampClient != null
396+
&& timestampVerifier != null) { // Timestamp funcionality enabled only in staging
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("Failed to validate timestamp against signature", 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: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
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.TimestampVerificationException;
42+
import dev.sigstore.timestamp.client.TimestampVerifier;
4043
import dev.sigstore.trustroot.SigstoreConfigurationException;
4144
import dev.sigstore.tuf.SigstoreTufClient;
4245
import java.io.IOException;
@@ -51,12 +54,16 @@
5154
import java.security.cert.CertificateNotYetValidException;
5255
import java.security.cert.X509Certificate;
5356
import java.security.spec.InvalidKeySpecException;
54-
import java.sql.Date;
5557
import java.util.Arrays;
5658
import java.util.Base64;
59+
import java.util.Date;
5760
import java.util.List;
5861
import java.util.Objects;
5962
import java.util.stream.Collectors;
63+
import org.bouncycastle.tsp.TSPException;
64+
import org.bouncycastle.tsp.TimeStampResponse;
65+
import org.bouncycastle.tsp.TimeStampToken;
66+
import org.bouncycastle.tsp.TimeStampTokenInfo;
6067
import org.bouncycastle.util.encoders.DecoderException;
6168
import org.bouncycastle.util.encoders.Hex;
6269

@@ -65,10 +72,15 @@ public class KeylessVerifier {
6572

6673
private final FulcioVerifier fulcioVerifier;
6774
private final RekorVerifier rekorVerifier;
75+
private final TimestampVerifier timestampVerifier;
6876

69-
private KeylessVerifier(FulcioVerifier fulcioVerifier, RekorVerifier rekorVerifier) {
77+
private KeylessVerifier(
78+
FulcioVerifier fulcioVerifier,
79+
RekorVerifier rekorVerifier,
80+
TimestampVerifier timestampVerifier) {
7081
this.fulcioVerifier = fulcioVerifier;
7182
this.rekorVerifier = rekorVerifier;
83+
this.timestampVerifier = timestampVerifier;
7284
}
7385

7486
public static KeylessVerifier.Builder builder() {
@@ -89,7 +101,8 @@ public KeylessVerifier build()
89101
var trustedRoot = trustedRootProvider.get();
90102
var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot);
91103
var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot);
92-
return new KeylessVerifier(fulcioVerifier, rekorVerifier);
104+
var timestampVerifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
105+
return new KeylessVerifier(fulcioVerifier, rekorVerifier, timestampVerifier);
93106
}
94107

95108
public Builder sigstorePublicDefaults() {
@@ -148,11 +161,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
148161
"Bundle verification expects 1 entry, but found " + bundle.getEntries().size());
149162
}
150163

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

@@ -188,11 +196,55 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
188196
throw new KeylessVerificationException("Signing time was after certificate expiry", e);
189197
}
190198

199+
byte[] signature;
191200
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
192-
checkMessageSignature(
193-
bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
201+
var messageSignature = bundle.getMessageSignature().get();
202+
checkMessageSignature(messageSignature, rekorEntry, artifactDigest, leafCert);
203+
signature = messageSignature.getSignature();
194204
} else { // dsse
195-
checkDsseEnvelope(rekorEntry, bundle.getDsseEnvelope().get(), artifactDigest, leafCert);
205+
var dsseEnvelope = bundle.getDsseEnvelope().get();
206+
checkDsseEnvelope(rekorEntry, dsseEnvelope, artifactDigest, leafCert);
207+
signature = dsseEnvelope.getSignature();
208+
}
209+
210+
verifyTimestamps(leafCert, bundle.getTimestamps(), signature);
211+
}
212+
213+
private void verifyTimestamps(
214+
X509Certificate leafCert, List<Bundle.Timestamp> timestamps, byte[] signature)
215+
throws KeylessVerificationException {
216+
if (timestamps == null || timestamps.isEmpty()) {
217+
return;
218+
}
219+
for (Bundle.Timestamp timestamp : timestamps) {
220+
byte[] tsBytes = timestamp.getRfc3161Timestamp();
221+
if (tsBytes == null || tsBytes.length == 0) {
222+
throw new KeylessVerificationException(
223+
"Found an empty or null RFC3161 timestamp in bundle");
224+
}
225+
try {
226+
TimeStampResponse bcTsResp = new TimeStampResponse(tsBytes);
227+
TimeStampToken tsToken = bcTsResp.getTimeStampToken();
228+
if (tsToken == null) {
229+
throw new KeylessVerificationException(
230+
"Timestamp token is missing from timestamp response");
231+
}
232+
TimeStampTokenInfo tsInfo = tsToken.getTimeStampInfo();
233+
if (tsInfo == null) {
234+
throw new KeylessVerificationException("Timestamp info is missing from timestamp token");
235+
}
236+
Date timestampGenerationTime = Date.from(tsInfo.getGenTime().toInstant());
237+
leafCert.checkValidity(timestampGenerationTime);
238+
var tsResp = ImmutableTimestampResponse.builder().encoded(tsBytes).build();
239+
timestampVerifier.verify(tsResp, signature);
240+
} catch (IOException
241+
| TSPException
242+
| CertificateNotYetValidException
243+
| CertificateExpiredException
244+
| TimestampVerificationException e) {
245+
throw new KeylessVerificationException(
246+
"RFC3161 timestamp verification failed: " + e.getMessage(), e);
247+
}
196248
}
197249
}
198250

sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
import static dev.sigstore.json.GsonSupplier.GSON;
1919

2020
import com.google.common.annotations.VisibleForTesting;
21-
import dev.sigstore.tuf.model.*;
21+
import dev.sigstore.tuf.model.SignedTufMeta;
22+
import dev.sigstore.tuf.model.TufMeta;
2223
import java.io.BufferedWriter;
2324
import java.io.IOException;
2425
import java.io.InputStream;
@@ -96,8 +97,8 @@ public void writeMeta(String roleName, SignedTufMeta<?> meta) throws IOException
9697
}
9798

9899
@Override
99-
public <T extends SignedTufMeta<?>> Optional<T> readMeta(String roleName, Class<T> tClass)
100-
throws IOException {
100+
public <T extends SignedTufMeta<? extends TufMeta>> Optional<T> readMeta(
101+
String roleName, Class<T> tClass) throws IOException {
101102
Path roleFile = repoBaseDir.resolve(roleName + ".json");
102103
if (!roleFile.toFile().exists()) {
103104
return Optional.empty();

sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.common.io.Resources;
2121
import dev.sigstore.VerificationOptions.CertificateMatcher;
2222
import dev.sigstore.bundle.Bundle;
23+
import dev.sigstore.bundle.ImmutableBundle;
2324
import dev.sigstore.encryption.signers.Signers;
2425
import dev.sigstore.strings.StringMatcher;
2526
import dev.sigstore.testing.CertGenerator;
@@ -288,4 +289,94 @@ public void testVerify_dsseBundleArtifactNotInSubjects() throws Exception {
288289
CoreMatchers.startsWith(
289290
"Provided artifact digest does not match any subject sha256 digests in DSSE payload"));
290291
}
292+
293+
@Test
294+
public void testVerify_validRfc3161Timestamp() throws Exception {
295+
var artifactUrl = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt");
296+
var artifactBytes = Resources.toByteArray(artifactUrl);
297+
var artifactDigest = Hashing.sha256().hashBytes(artifactBytes).asBytes();
298+
299+
var bundleFile =
300+
Resources.toString(
301+
Resources.getResource("dev/sigstore/samples/bundles/bundle-with-timestamp.sigstore"),
302+
StandardCharsets.UTF_8);
303+
var verifier = KeylessVerifier.builder().sigstoreStagingDefaults().build();
304+
305+
Assertions.assertDoesNotThrow(
306+
() ->
307+
verifier.verify(
308+
artifactDigest,
309+
Bundle.from(new StringReader(bundleFile)),
310+
VerificationOptions.empty()));
311+
}
312+
313+
@Test
314+
public void testVerify_invalidRfc3161Timestamp() throws Exception {
315+
var tsRespBytesInvalid =
316+
Resources.toByteArray(
317+
Resources.getResource(
318+
"dev/sigstore/samples/timestamp-response/invalid/sigstore_tsa_response_invalid.tsr"));
319+
320+
var artifactUrl = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt");
321+
var artifactBytes = Resources.toByteArray(artifactUrl);
322+
var artifactDigest = Hashing.sha256().hashBytes(artifactBytes).asBytes();
323+
324+
var bundleFile =
325+
Resources.toString(
326+
Resources.getResource("dev/sigstore/samples/bundles/bundle.v3.sigstore"),
327+
StandardCharsets.UTF_8);
328+
329+
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
330+
331+
var baseBundle = Bundle.from(new StringReader(bundleFile));
332+
var testBundle =
333+
ImmutableBundle.builder()
334+
.from(baseBundle)
335+
.timestamps(List.of(createTimestamp(tsRespBytesInvalid)))
336+
.build();
337+
var ex =
338+
Assertions.assertThrows(
339+
KeylessVerificationException.class,
340+
() -> verifier.verify(artifactDigest, testBundle, VerificationOptions.empty()));
341+
MatcherAssert.assertThat(
342+
ex.getMessage(),
343+
CoreMatchers.equalTo("RFC3161 timestamp verification failed: invalid pad bits detected"));
344+
}
345+
346+
@Test
347+
public void testVerify_invalidTimestampGenTime() throws Exception {
348+
var tsRespBytesInvalidGenTime =
349+
Resources.toByteArray(
350+
Resources.getResource(
351+
"dev/sigstore/samples/timestamp-response/valid/sigstore_tsa_response_with_embedded_certs.tsr"));
352+
353+
var artifactResourcePath = "dev/sigstore/samples/bundles/artifact.txt";
354+
var artifactBytes = Resources.toByteArray(Resources.getResource(artifactResourcePath));
355+
var artifactDigest = Hashing.sha256().hashBytes(artifactBytes).asBytes();
356+
357+
var bundleFileContent =
358+
Resources.toString(
359+
Resources.getResource("dev/sigstore/samples/bundles/bundle-with-timestamp.sigstore"),
360+
StandardCharsets.UTF_8);
361+
var verifier = KeylessVerifier.builder().sigstoreStagingDefaults().build();
362+
363+
var baseBundle = Bundle.from(new StringReader(bundleFileContent));
364+
var testBundle =
365+
ImmutableBundle.builder()
366+
.from(baseBundle)
367+
.timestamps(List.of(createTimestamp(tsRespBytesInvalidGenTime)))
368+
.build();
369+
370+
var ex =
371+
Assertions.assertThrows(
372+
KeylessVerificationException.class,
373+
() -> verifier.verify(artifactDigest, testBundle, VerificationOptions.empty()));
374+
MatcherAssert.assertThat(
375+
ex.getMessage(),
376+
CoreMatchers.startsWith("RFC3161 timestamp verification failed: NotBefore:"));
377+
}
378+
379+
private Bundle.Timestamp createTimestamp(byte[] rfc3161Bytes) {
380+
return () -> rfc3161Bytes;
381+
}
291382
}

0 commit comments

Comments
 (0)