Skip to content

Commit fd9d69c

Browse files
committed
Add artifact validation and staging tests to timestamp verifier
Signed-off-by: Aaron Lew <[email protected]>
1 parent c58a351 commit fd9d69c

File tree

9 files changed

+381
-37
lines changed

9 files changed

+381
-37
lines changed

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

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

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

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

18+
import com.google.common.hash.Hashing;
1819
import dev.sigstore.encryption.certificates.Certificates;
1920
import dev.sigstore.trustroot.CertificateAuthority;
2021
import dev.sigstore.trustroot.SigstoreTrustedRoot;
@@ -32,6 +33,7 @@
3233
import java.security.cert.X509Certificate;
3334
import java.security.spec.InvalidKeySpecException;
3435
import java.util.ArrayList;
36+
import java.util.Arrays;
3537
import java.util.Collections;
3638
import java.util.Date;
3739
import java.util.LinkedHashMap;
@@ -83,10 +85,12 @@ private TimestampVerifier(List<CertificateAuthority> tsas) {
8385
*
8486
* @param tsResp The timestamp response object containing the raw bytes of the RFC 3161
8587
* TimeStampResponse.
88+
* @param artifact The artifact that was timestamped.
8689
* @throws TimestampVerificationException if any verification step fails (e.g., no token,
8790
* certificate path validation failure, signature validation failure).
8891
*/
89-
public void verify(TimestampResponse tsResp) throws TimestampVerificationException {
92+
public void verify(TimestampResponse tsResp, byte[] artifact)
93+
throws TimestampVerificationException {
9094
// Parse the timestamp response
9195
TimeStampResponse bcTsResp;
9296
try {
@@ -133,16 +137,37 @@ public void verify(TimestampResponse tsResp) throws TimestampVerificationExcepti
133137
tsaVerificationFailure.put(
134138
tsa.getUri().toString(),
135139
"Timestamp generation time is not within TSA's validity period.");
136-
} else {
137-
return;
140+
String errors =
141+
tsaVerificationFailure.entrySet().stream()
142+
.map(entry -> entry.getKey() + " (" + entry.getValue() + ")")
143+
.collect(Collectors.joining("\n"));
144+
throw new TimestampVerificationException(
145+
"Certificate was not verifiable against TSAs\n" + errors);
138146
}
139147

140-
String errors =
141-
tsaVerificationFailure.entrySet().stream()
142-
.map(entry -> entry.getKey() + " (" + entry.getValue() + ")")
143-
.collect(Collectors.joining("\n"));
144-
throw new TimestampVerificationException(
145-
"Certificate was not verifiable against TSAs\n" + errors);
148+
// Validate the message imprint digest in the token
149+
var oid = tsToken.getTimeStampInfo().getMessageImprintAlgOID();
150+
HashAlgorithm hashAlgorithm;
151+
try {
152+
hashAlgorithm = HashAlgorithm.from(oid);
153+
} catch (UnsupportedHashAlgorithmException e) {
154+
throw new TimestampVerificationException(e);
155+
}
156+
byte[] artifactDigest;
157+
switch (hashAlgorithm) {
158+
case SHA256:
159+
artifactDigest = Hashing.sha256().hashBytes(artifact).asBytes();
160+
break;
161+
case SHA384:
162+
artifactDigest = Hashing.sha384().hashBytes(artifact).asBytes();
163+
break;
164+
case SHA512:
165+
artifactDigest = Hashing.sha512().hashBytes(artifact).asBytes();
166+
break;
167+
default:
168+
throw new IllegalStateException(); // We shouldn't be here.
169+
}
170+
validateTokenMessageImprintDigest(tsToken, artifactDigest);
146171
}
147172

148173
/** Validates the signature of the TimeStampToken using the provided signing certificate. */
@@ -159,6 +184,19 @@ private void validateTokenSignature(TimeStampToken token, X509Certificate signin
159184
}
160185
}
161186

187+
/**
188+
* Validates that the message imprint digest in the timestamp token matches the provided artifact
189+
* digest.
190+
*/
191+
private void validateTokenMessageImprintDigest(TimeStampToken token, byte[] artifactDigest)
192+
throws TimestampVerificationException {
193+
var messageImprintDigest = token.getTimeStampInfo().getMessageImprintDigest();
194+
if (!Arrays.equals(messageImprintDigest, artifactDigest)) {
195+
throw new TimestampVerificationException(
196+
"Timestamp message imprint digest does not match artifact hash");
197+
}
198+
}
199+
162200
/** Validates that the provided TSA's certificate chain is self-consistent. */
163201
void validateTsaChain(CertificateAuthority tsa, Date tsDate)
164202
throws TimestampException,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 UnsupportedHashAlgorithmException extends Exception {
19+
public UnsupportedHashAlgorithmException(String algorithm) {
20+
super("Unsupported hash algorithm: " + algorithm);
21+
}
22+
}

sigstore-java/src/test/java/dev/sigstore/timestamp/client/TimestampVerifierTest.java

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertNotNull;
2121
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
2223

2324
import com.google.common.io.Resources;
2425
import dev.sigstore.json.ProtoJson;
@@ -34,31 +35,33 @@
3435

3536
public class TimestampVerifierTest {
3637
private static SigstoreTrustedRoot trustedRoot;
38+
private static SigstoreTrustedRoot trustedRootWithOneTsa;
3739
private static SigstoreTrustedRoot trustedRootWithOutdatedTsa;
38-
private static SigstoreTrustedRoot trustedRootWithMultipleTsas;
40+
private static byte[] artifact;
3941
private static byte[] trustedTsRespBytesWithEmbeddedCerts;
4042
private static byte[] trustedTsRespBytesWithoutEmbeddedCerts;
4143
private static byte[] invalidTsRespBytes;
4244
private static byte[] untrustedTsRespBytes;
4345

4446
@BeforeAll
4547
public static void loadResources() throws Exception {
46-
// Response from Sigstore TSA (in trusted root) with embedded certs
48+
artifact = "test\n".getBytes(StandardCharsets.UTF_8);
49+
4750
try (var is =
4851
Resources.getResource(
49-
"dev/sigstore/samples/timestamp-response/valid/sigstore_tsa_response_with_embedded_certs.tsr")
52+
"dev/sigstore/samples/timestamp-response/valid/sigstage_tsa_response_with_embedded_certs.tsr")
5053
.openStream()) {
5154
trustedTsRespBytesWithEmbeddedCerts = is.readAllBytes();
5255
}
5356

5457
// Response from Sigstore TSA (in trusted root) without embedded certs
5558
try (var is =
5659
Resources.getResource(
57-
"dev/sigstore/samples/timestamp-response/valid/sigstore_tsa_response_without_embedded_certs.tsr")
60+
"dev/sigstore/samples/timestamp-response/valid/sigstage_tsa_response_without_embedded_certs.tsr")
5861
.openStream()) {
5962
if (is == null) {
6063
throw new IOException(
61-
"dev/sigstore/samples/timestamp-response/valid/sigstore_tsa_response_without_embedded_certs.tsr");
64+
"dev/sigstore/samples/timestamp-response/valid/sigstage_tsa_response_without_embedded_certs.tsr");
6265
}
6366
trustedTsRespBytesWithoutEmbeddedCerts = is.readAllBytes();
6467
}
@@ -90,7 +93,7 @@ public static void loadResources() throws Exception {
9093
public static void initTrustRoot() throws Exception {
9194
var json =
9295
Resources.toString(
93-
Resources.getResource("dev/sigstore/trustroot/trusted_root.json"),
96+
Resources.getResource("dev/sigstore/trustroot/staging_trusted_root.json"),
9497
StandardCharsets.UTF_8);
9598
var builder = TrustedRoot.newBuilder();
9699
ProtoJson.parser().merge(json, builder);
@@ -99,69 +102,73 @@ public static void initTrustRoot() throws Exception {
99102

100103
json =
101104
Resources.toString(
102-
Resources.getResource("dev/sigstore/trustroot/trusted_root_with_outdated_tsa.json"),
105+
Resources.getResource("dev/sigstore/trustroot/staging_trusted_root_with_one_tsa.json"),
103106
StandardCharsets.UTF_8);
104107
builder = TrustedRoot.newBuilder();
105108
ProtoJson.parser().merge(json, builder);
106109

107-
trustedRootWithOutdatedTsa = SigstoreTrustedRoot.from(builder.build());
110+
trustedRootWithOneTsa = SigstoreTrustedRoot.from(builder.build());
111+
trustedRootWithOneTsa = SigstoreTrustedRoot.from(builder.build());
108112

109113
json =
110114
Resources.toString(
111-
Resources.getResource("dev/sigstore/trustroot/trusted_root_with_multiple_tsas.json"),
115+
Resources.getResource(
116+
"dev/sigstore/trustroot/staging_trusted_root_with_outdated_tsa.json"),
112117
StandardCharsets.UTF_8);
113118
builder = TrustedRoot.newBuilder();
114119
ProtoJson.parser().merge(json, builder);
115120

116-
trustedRootWithMultipleTsas = SigstoreTrustedRoot.from(builder.build());
121+
trustedRootWithOutdatedTsa = SigstoreTrustedRoot.from(builder.build());
122+
trustedRootWithOutdatedTsa = SigstoreTrustedRoot.from(builder.build());
117123
}
118124

119125
@Test
120-
public void verify_success_validResponseWithEmbeddedCerts() throws Exception {
126+
public void verify_success_validResponseWithEmbeddedCerts_multipleTsas() throws Exception {
121127
var tsResp =
122128
ImmutableTimestampResponse.builder().encoded(trustedTsRespBytesWithEmbeddedCerts).build();
123129
var verifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
124130

125-
assertDoesNotThrow(() -> verifier.verify(tsResp));
131+
assertDoesNotThrow(() -> verifier.verify(tsResp, artifact));
126132
}
127133

128134
@Test
129-
public void verify_success_validResponseWithoutEmbeddedCerts() throws Exception {
135+
public void verify_success_validResponseWithoutEmbeddedCerts_multipleTsas() throws Exception {
130136
var tsResp =
131137
ImmutableTimestampResponse.builder()
132138
.encoded(trustedTsRespBytesWithoutEmbeddedCerts)
133139
.build();
134140
var verifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
135141

136-
assertDoesNotThrow(() -> verifier.verify(tsResp));
142+
assertDoesNotThrow(() -> verifier.verify(tsResp, artifact));
137143
}
138144

139145
@Test
140-
public void verify_success_validResponseWithEmbeddedCerts_multipleTsas() throws Exception {
146+
public void verify_success_validResponseWithEmbeddedCerts_oneTsa() throws Exception {
141147
var tsResp =
142148
ImmutableTimestampResponse.builder().encoded(trustedTsRespBytesWithEmbeddedCerts).build();
143-
var verifier = TimestampVerifier.newTimestampVerifier(trustedRootWithMultipleTsas);
149+
var verifier = TimestampVerifier.newTimestampVerifier(trustedRootWithOneTsa);
144150

145-
assertDoesNotThrow(() -> verifier.verify(tsResp));
151+
assertDoesNotThrow(() -> verifier.verify(tsResp, artifact));
146152
}
147153

148154
@Test
149-
public void verify_success_validResponseWithoutEmbeddedCerts_multipleTsas() throws Exception {
155+
public void verify_success_validResponseWithoutEmbeddedCerts_oneTsa() throws Exception {
150156
var tsResp =
151157
ImmutableTimestampResponse.builder()
152158
.encoded(trustedTsRespBytesWithoutEmbeddedCerts)
153159
.build();
154-
var verifier = TimestampVerifier.newTimestampVerifier(trustedRootWithMultipleTsas);
160+
var verifier = TimestampVerifier.newTimestampVerifier(trustedRootWithOneTsa);
155161

156-
assertDoesNotThrow(() -> verifier.verify(tsResp));
162+
assertDoesNotThrow(() -> verifier.verify(tsResp, artifact));
157163
}
158164

159165
@Test
160166
public void verify_failure_invalidResponse() throws Exception {
161167
var tsResp = ImmutableTimestampResponse.builder().encoded(invalidTsRespBytes).build();
162168
var verifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
163169

164-
var tsve = assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp));
170+
var tsve =
171+
assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp, artifact));
165172
assertEquals("Failed to parse TimeStampResponse", tsve.getMessage());
166173
}
167174

@@ -172,10 +179,13 @@ public void verify_failure_untrustedTsa() throws Exception {
172179

173180
var verifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
174181

175-
var tsve = assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp));
176-
assertEquals(
177-
"Certificates in token were not verifiable against TSAs\nhttps://timestamp.sigstore.dev (Embedded leaf certificate does not match this trusted TSA's leaf.)",
178-
tsve.getMessage());
182+
var tsve =
183+
assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp, artifact));
184+
assertTrue(
185+
tsve.getMessage().startsWith("Certificates in token were not verifiable against TSAs"));
186+
assertTrue(
187+
tsve.getMessage()
188+
.contains("Embedded leaf certificate does not match this trusted TSA's leaf."));
179189
}
180190

181191
@Test
@@ -186,9 +196,10 @@ public void verify_failure_outdatedTsa() throws Exception {
186196

187197
var verifier = TimestampVerifier.newTimestampVerifier(trustedRootWithOutdatedTsa);
188198

189-
var tsve = assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp));
199+
var tsve =
200+
assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp, artifact));
190201
assertEquals(
191-
"Certificate was not verifiable against TSAs\nhttps://timestamp.sigstore.dev (Timestamp generation time is not within TSA's validity period.)",
202+
"Certificate was not verifiable against TSAs\nhttps://timestamp.sigstage.dev/api/v1/timestamp (Timestamp generation time is not within TSA's validity period.)",
192203
tsve.getMessage());
193204
}
194205

@@ -205,7 +216,8 @@ public void verify_failure_tsLacksToken() throws Exception {
205216
var tsResp = ImmutableTimestampResponse.builder().encoded(failResponseBytes).build();
206217
var verifier = TimestampVerifier.newTimestampVerifier(trustedRoot);
207218

208-
var tsve = assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp));
219+
var tsve =
220+
assertThrows(TimestampVerificationException.class, () -> verifier.verify(tsResp, artifact));
209221
assertEquals("No TimeStampToken found in response", tsve.getMessage());
210222
}
211223
}

sigstore-java/src/test/resources/dev/sigstore/trustroot/staging_trusted_root.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,26 @@
8585
}
8686
],
8787
"timestampAuthorities": [
88+
{
89+
"subject": {
90+
"organization": "sigstore.dev",
91+
"commonName": "sigstore-tsa-selfsigned"
92+
},
93+
"uri": "https://timestamp.sigstage.dev/api/v1/timestamp",
94+
"certChain": {
95+
"certificates": [
96+
{
97+
"rawBytes": "MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6"
98+
},
99+
{
100+
"rawBytes": "MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQEbKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXAd8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7"
101+
}
102+
]
103+
},
104+
"validFor": {
105+
"start": "2025-04-09T00:00:00Z"
106+
}
107+
},
88108
{
89109
"subject": {
90110
"organization": "GitHub, Inc.",

0 commit comments

Comments
 (0)