Skip to content

Commit 66b30ed

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

File tree

9 files changed

+387
-37
lines changed

9 files changed

+387
-37
lines changed

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 HashAlgorithmException {
44+
for (HashAlgorithm value : values()) {
45+
if (value.getOid().equals(oid)) {
46+
return value;
47+
}
48+
}
49+
throw new HashAlgorithmException("Unsupported hash algorithm: " + oid.getId());
50+
}
4251
}
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 HashAlgorithmException extends Exception {
19+
public HashAlgorithmException(String message) {
20+
super(message);
21+
}
22+
23+
public HashAlgorithmException(String message, Throwable cause) {
24+
super(message, cause);
25+
}
26+
27+
public HashAlgorithmException(Throwable cause) {
28+
super(cause);
29+
}
30+
}

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

Lines changed: 46 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,36 @@ 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+
try {
150+
var oid = tsToken.getTimeStampInfo().getMessageImprintAlgOID();
151+
var hashAlgorithm = HashAlgorithm.from(oid);
152+
byte[] artifactDigest;
153+
switch (hashAlgorithm) {
154+
case SHA256:
155+
artifactDigest = Hashing.sha256().hashBytes(artifact).asBytes();
156+
break;
157+
case SHA384:
158+
artifactDigest = Hashing.sha384().hashBytes(artifact).asBytes();
159+
break;
160+
case SHA512:
161+
artifactDigest = Hashing.sha512().hashBytes(artifact).asBytes();
162+
break;
163+
default:
164+
throw new IllegalStateException(); // We shouldn't be here.
165+
}
166+
validateTokenMessageImprintDigest(tsToken, artifactDigest);
167+
} catch (HashAlgorithmException hae) {
168+
throw new TimestampVerificationException("Failed to validate artifact hash", hae);
169+
}
146170
}
147171

148172
/** Validates the signature of the TimeStampToken using the provided signing certificate. */
@@ -159,6 +183,19 @@ private void validateTokenSignature(TimeStampToken token, X509Certificate signin
159183
}
160184
}
161185

186+
/**
187+
* Validates that the message imprint digest in the timestamp token matches the provided artifact
188+
* digest.
189+
*/
190+
private void validateTokenMessageImprintDigest(TimeStampToken token, byte[] artifactDigest)
191+
throws TimestampVerificationException {
192+
var messageImprintDigest = token.getTimeStampInfo().getMessageImprintDigest();
193+
if (!Arrays.equals(messageImprintDigest, artifactDigest)) {
194+
throw new TimestampVerificationException(
195+
"Timestamp message imprint digest does not match artifact hash");
196+
}
197+
}
198+
162199
/** Validates that the provided TSA's certificate chain is self-consistent. */
163200
void validateTsaChain(CertificateAuthority tsa, Date tsDate)
164201
throws TimestampException,

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
}
1.22 KB
Binary file not shown.
714 Bytes
Binary file not shown.

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)