Skip to content

Commit 1ffbbc5

Browse files
committed
Add timestamp to bundle and validate
Signed-off-by: Aaron Lew <64337293+aaronlew02@users.noreply.github.com>
1 parent ddcbb86 commit 1ffbbc5

20 files changed

+669
-78
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+
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/bundle/Bundle.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ interface Signature {
185185
@Immutable
186186
public interface Timestamp {
187187

188-
/** Raw bytes of an rfc31616 timestamp */
188+
/** Raw bytes of an rfc3161 timestamp */
189189
byte[] getRfc3161Timestamp();
190190
}
191191

sigstore-java/src/main/java/dev/sigstore/bundle/BundleReader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ static Bundle readBundle(Reader jsonReader) throws BundleParseException {
177177
.getTimestampVerificationData()
178178
.getRfc3161TimestampsList()) {
179179
bundleBuilder.addTimestamps(
180-
ImmutableTimestamp.builder().rfc3161Timestamp(timestamp.toByteArray()).build());
180+
ImmutableTimestamp.builder()
181+
.rfc3161Timestamp(timestamp.getSignedTimestamp().toByteArray())
182+
.build());
181183
}
182184
}
183185

sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
import com.google.protobuf.InvalidProtocolBufferException;
2121
import com.google.protobuf.util.JsonFormat;
2222
import dev.sigstore.proto.ProtoMutators;
23+
import dev.sigstore.proto.bundle.v1.TimestampVerificationData;
2324
import dev.sigstore.proto.bundle.v1.VerificationMaterial;
2425
import dev.sigstore.proto.common.v1.HashOutput;
2526
import dev.sigstore.proto.common.v1.LogId;
2627
import dev.sigstore.proto.common.v1.MessageSignature;
28+
import dev.sigstore.proto.common.v1.RFC3161SignedTimestamp;
2729
import dev.sigstore.proto.common.v1.X509Certificate;
2830
import dev.sigstore.proto.rekor.v1.Checkpoint;
2931
import dev.sigstore.proto.rekor.v1.InclusionPromise;
@@ -110,6 +112,11 @@ private static VerificationMaterial.Builder buildVerificationMaterial(Bundle bun
110112
"Exactly 1 rekor entry must be present in the signing result");
111113
}
112114
builder.addTlogEntries(buildTlogEntries(bundle.getEntries().get(0)));
115+
TimestampVerificationData timestampData =
116+
buildTimestampVerificationData(bundle.getTimestamps());
117+
if (timestampData != null) {
118+
builder.setTimestampVerificationData(timestampData);
119+
}
113120
return builder;
114121
}
115122

@@ -148,4 +155,20 @@ private static void addInclusionProof(
148155
.collect(Collectors.toList()))
149156
.setCheckpoint(Checkpoint.newBuilder().setEnvelope(inclusionProof.getCheckpoint())));
150157
}
158+
159+
private static TimestampVerificationData buildTimestampVerificationData(
160+
List<Bundle.Timestamp> bundleTimestamps) {
161+
if (bundleTimestamps == null || bundleTimestamps.isEmpty()) {
162+
return null;
163+
}
164+
TimestampVerificationData.Builder tsvBuilder = TimestampVerificationData.newBuilder();
165+
for (Bundle.Timestamp ts : bundleTimestamps) {
166+
byte[] tsBytes = ts.getRfc3161Timestamp();
167+
if (tsBytes != null && tsBytes.length > 0) {
168+
tsvBuilder.addRfc3161Timestamps(
169+
RFC3161SignedTimestamp.newBuilder().setSignedTimestamp(ByteString.copyFrom(tsBytes)));
170+
}
171+
}
172+
return tsvBuilder.getRfc3161TimestampsCount() > 0 ? tsvBuilder.build() : null;
173+
}
151174
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,13 @@ public String getAlgorithmName() {
3939
public ASN1ObjectIdentifier getOid() {
4040
return oid;
4141
}
42+
43+
public static HashAlgorithm from(ASN1ObjectIdentifier oid) throws TimestampException {
44+
for (HashAlgorithm value : values()) {
45+
if (value.getOid().equals(oid)) {
46+
return value;
47+
}
48+
}
49+
throw new TimestampException("Unsupported hash algorithm: " + oid.getId());
50+
}
4251
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
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 PUBLIC_GOOD_URI = URI.create("https://timestamp.sigstore.dev");
23+
URI STAGING_URI = URI.create("https://timestamp.sigstage.dev");
24+
2025
/**
2126
* Request a timestanp for a timestamp authority.
2227
*

0 commit comments

Comments
 (0)