Skip to content

Commit d6b598c

Browse files
committed
Add support for verifying dsse-intoto
- Verification should be able to correctly validate a bundle as cryptographically valid (VerificationOptions.empty()) - Verifiers may also include signer identity during verification - Verifiers should extract the embedded attestation to do further analysis on the attestation. Sigstore-java does not process those in any way - There is no signing options for DSSE bundles Signed-off-by: Appu Goundan <[email protected]>
1 parent c36fc5f commit d6b598c

File tree

6 files changed

+336
-28
lines changed

6 files changed

+336
-28
lines changed

.github/workflows/conformance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
with:
4040
entrypoint: ${{ github.workspace }}/bin/sigstore-cli
4141
environment: ${{ matrix.sigstore-env }}
42-
xfail: "test_verify_dsse_bundle_with_trust_root test_verify_in_toto_in_dsse_envelope"
42+
xfail: "test_verify_dsse_bundle_with_trust_root"

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

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import dev.sigstore.VerificationOptions.CertificateMatcher;
2323
import dev.sigstore.VerificationOptions.UncheckedCertificateException;
2424
import dev.sigstore.bundle.Bundle;
25+
import dev.sigstore.bundle.Bundle.DsseEnvelope;
2526
import dev.sigstore.bundle.Bundle.MessageSignature;
27+
import dev.sigstore.dsse.InTotoPayload;
2628
import dev.sigstore.encryption.certificates.Certificates;
2729
import dev.sigstore.encryption.signers.Verifiers;
2830
import dev.sigstore.fulcio.client.FulcioVerificationException;
@@ -33,6 +35,8 @@
3335
import dev.sigstore.rekor.client.RekorTypes;
3436
import dev.sigstore.rekor.client.RekorVerificationException;
3537
import dev.sigstore.rekor.client.RekorVerifier;
38+
import dev.sigstore.rekor.dsse.v0_0_1.Dsse;
39+
import dev.sigstore.rekor.dsse.v0_0_1.PayloadHash;
3640
import dev.sigstore.tuf.SigstoreTufClient;
3741
import java.io.IOException;
3842
import java.nio.charset.StandardCharsets;
@@ -52,6 +56,7 @@
5256
import java.util.List;
5357
import java.util.Objects;
5458
import java.util.stream.Collectors;
59+
import org.bouncycastle.util.encoders.DecoderException;
5560
import org.bouncycastle.util.encoders.Hex;
5661

5762
/** Verify hashrekords from rekor signed using the keyless signing flow with fulcio certificates. */
@@ -125,12 +130,9 @@ public void verify(Path artifact, Bundle bundle, VerificationOptions options)
125130
public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions options)
126131
throws KeylessVerificationException {
127132

128-
if (bundle.getDsseEnvelope().isPresent()) {
129-
throw new KeylessVerificationException("Cannot verify DSSE signature based bundles");
130-
}
131-
if (bundle.getMessageSignature().isEmpty()) {
132-
// this should be unreachable
133-
throw new IllegalStateException("Bundle must contain a message signature to verify");
133+
if (bundle.getDsseEnvelope().isEmpty() && bundle.getMessageSignature().isEmpty()) {
134+
throw new IllegalStateException(
135+
"Bundle must contain a message signature or DSSE envelope to verify");
134136
}
135137

136138
if (bundle.getEntries().isEmpty()) {
@@ -182,7 +184,12 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
182184
throw new KeylessVerificationException("Signing time was after certificate expiry", e);
183185
}
184186

185-
checkMessageSignature(bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
187+
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
188+
checkMessageSignature(
189+
bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
190+
} else { // dsse
191+
checkDsseEnvelope(rekorEntry, bundle.getDsseEnvelope().get(), artifactDigest, leafCert);
192+
}
186193
}
187194

188195
@VisibleForTesting
@@ -255,4 +262,110 @@ void checkMessageSignature(
255262
throw new KeylessVerificationException("Unexpected rekor type", re);
256263
}
257264
}
265+
266+
// do all dsse specific checks
267+
void checkDsseEnvelope(
268+
RekorEntry rekorEntry,
269+
DsseEnvelope dsseEnvelope,
270+
byte[] artifactDigest,
271+
X509Certificate leafCert)
272+
throws KeylessVerificationException {
273+
274+
// verify the artifact is in the subject list of the envelope
275+
if (!Objects.equals(InTotoPayload.PAYLOAD_TYPE, dsseEnvelope.getPayloadType())) {
276+
throw new KeylessVerificationException(
277+
"DSSE envelope must have payload type "
278+
+ InTotoPayload.PAYLOAD_TYPE
279+
+ ", but found '"
280+
+ dsseEnvelope.getPayloadType()
281+
+ "'");
282+
}
283+
InTotoPayload payload = InTotoPayload.from(dsseEnvelope);
284+
285+
// find one sha256 hash in the subject list that matches the artifact hash
286+
if (payload.getSubject().stream()
287+
.noneMatch(
288+
subject -> {
289+
if (subject.getDigest().containsKey("sha256")) {
290+
try {
291+
var digestBytes = Hex.decode(subject.getDigest().get("sha256"));
292+
return Arrays.equals(artifactDigest, digestBytes);
293+
} catch (DecoderException de) {
294+
// ignore (assume false)
295+
}
296+
}
297+
return false;
298+
})) {
299+
var providedHashes =
300+
payload.getSubject().stream()
301+
.map(s -> s.getDigest().getOrDefault("sha256", "no-sha256-hash"))
302+
.collect(Collectors.joining(",", "[", "]"));
303+
304+
throw new KeylessVerificationException(
305+
"Provided artifact digest does not match any subject sha256 digests in DSSE payload"
306+
+ "\nprovided(hex) : "
307+
+ Hex.toHexString(artifactDigest)
308+
+ "\nverification : "
309+
+ providedHashes);
310+
}
311+
312+
// verify the dsse signature
313+
if (dsseEnvelope.getSignatures().size() != 1) {
314+
throw new KeylessVerificationException(
315+
"DSSE envelope must have exactly 1 signature, but found: "
316+
+ dsseEnvelope.getSignatures().size());
317+
}
318+
try {
319+
if (!Verifiers.newVerifier(leafCert.getPublicKey())
320+
.verify(dsseEnvelope.getPAE(), dsseEnvelope.getSignature())) {
321+
throw new KeylessVerificationException("DSSE signature was not valid");
322+
}
323+
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
324+
throw new RuntimeException(ex);
325+
} catch (SignatureException se) {
326+
throw new KeylessVerificationException("Signature could not be processed", se);
327+
}
328+
329+
// check if the digest over the dsse payload matches the digest in the rekorEntry
330+
Dsse rekorDsse;
331+
try {
332+
rekorDsse = RekorTypes.getDsse(rekorEntry);
333+
} catch (RekorTypeException re) {
334+
throw new KeylessVerificationException("Unexpected rekor type", re);
335+
}
336+
337+
var algorithm = rekorDsse.getPayloadHash().getAlgorithm();
338+
if (algorithm != PayloadHash.Algorithm.SHA_256) {
339+
throw new KeylessVerificationException(
340+
"Cannot process DSSE entry with hashing algorithm " + algorithm.toString());
341+
}
342+
343+
byte[] payloadDigest;
344+
try {
345+
payloadDigest = Hex.decode(rekorDsse.getPayloadHash().getValue());
346+
} catch (DecoderException de) {
347+
throw new KeylessVerificationException(
348+
"Could not decode hex sha256 artifact hash in hashrekord", de);
349+
}
350+
351+
byte[] calculatedDigest = Hashing.sha256().hashBytes(dsseEnvelope.getPayload()).asBytes();
352+
if (!Arrays.equals(calculatedDigest, payloadDigest)) {
353+
throw new KeylessVerificationException(
354+
"Digest of DSSE payload in bundle does not match DSSE payload digest in log entry");
355+
}
356+
357+
// check if the signature over the dsse payload matches the signature in the rekorEntry
358+
if (rekorDsse.getSignatures().size() != 1) {
359+
throw new KeylessVerificationException(
360+
"DSSE log entry must have exactly 1 signature, but found: "
361+
+ rekorDsse.getSignatures().size());
362+
}
363+
364+
if (!Base64.getEncoder()
365+
.encodeToString(dsseEnvelope.getSignature())
366+
.equals(rekorDsse.getSignatures().get(0).getSignature())) {
367+
throw new KeylessVerificationException(
368+
"Provided DSSE signature materials are inconsistent with DSSE log entry");
369+
}
370+
}
258371
}

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

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package dev.sigstore;
1717

1818
import com.google.common.collect.ImmutableList;
19+
import com.google.common.hash.Hashing;
1920
import com.google.common.io.Resources;
2021
import dev.sigstore.VerificationOptions.CertificateMatcher;
2122
import dev.sigstore.bundle.Bundle;
@@ -27,8 +28,14 @@
2728
import java.nio.file.Path;
2829
import java.security.cert.X509Certificate;
2930
import java.util.List;
31+
import java.util.stream.Stream;
32+
import org.hamcrest.CoreMatchers;
33+
import org.hamcrest.MatcherAssert;
3034
import org.junit.jupiter.api.Assertions;
3135
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.params.ParameterizedTest;
37+
import org.junit.jupiter.params.provider.Arguments;
38+
import org.junit.jupiter.params.provider.MethodSource;
3239

3340
public class KeylessVerifierTest {
3441

@@ -105,26 +112,6 @@ public void testVerify_badCheckpointSignature() throws Exception {
105112
VerificationOptions.empty()));
106113
}
107114

108-
@Test
109-
public void testVerify_errorsOnDSSEBundle() throws Exception {
110-
var bundleFile =
111-
Resources.toString(
112-
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
113-
StandardCharsets.UTF_8);
114-
var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
115-
116-
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
117-
var ex =
118-
Assertions.assertThrows(
119-
KeylessVerificationException.class,
120-
() ->
121-
verifier.verify(
122-
Path.of(artifact),
123-
Bundle.from(new StringReader(bundleFile)),
124-
VerificationOptions.empty()));
125-
Assertions.assertEquals("Cannot verify DSSE signature based bundles", ex.getMessage());
126-
}
127-
128115
@Test
129116
public void testVerify_canVerifyV01Bundle() throws Exception {
130117
// note that this v1 bundle contains an inclusion proof
@@ -231,4 +218,74 @@ public void verifyCertificateMatches_noneMatch() throws Exception {
231218
"No provided certificate identities matched values in certificate: [{issuer:'String: not-match',san:'String: not-match'},{issuer:'String: not-match-again',san:'String: not-match-again'}]",
232219
ex.getMessage());
233220
}
221+
222+
@Test
223+
public void testVerify_dsseBundle() throws Exception {
224+
var bundleFile =
225+
Resources.toString(
226+
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
227+
StandardCharsets.UTF_8);
228+
var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
229+
230+
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
231+
verifier.verify(
232+
Path.of(artifact), Bundle.from(new StringReader(bundleFile)), VerificationOptions.empty());
233+
}
234+
235+
static Stream<Arguments> badDsseProvider() {
236+
return Stream.of(
237+
Arguments.arguments("bundle.dsse.bad-signature.sigstore", "DSSE signature was not valid"),
238+
Arguments.arguments(
239+
"bundle.dsse.mismatched-envelope.sigstore",
240+
"Digest of DSSE payload in bundle does not match DSSE payload digest in log entry"),
241+
Arguments.arguments(
242+
"bundle.dsse.mismatched-signature.sigstore",
243+
"Provided DSSE signature materials are inconsistent with DSSE log entry"));
244+
}
245+
246+
@ParameterizedTest
247+
@MethodSource("badDsseProvider")
248+
public void testVerify_dsseBundleBadSignature(String bundleName, String expectedError)
249+
throws Exception {
250+
var bundleFile =
251+
Resources.toString(
252+
Resources.getResource("dev/sigstore/samples/bundles/" + bundleName),
253+
StandardCharsets.UTF_8);
254+
var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
255+
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
256+
257+
var ex =
258+
Assertions.assertThrows(
259+
KeylessVerificationException.class,
260+
() ->
261+
verifier.verify(
262+
Path.of(artifact),
263+
Bundle.from(new StringReader(bundleFile)),
264+
VerificationOptions.empty()));
265+
Assertions.assertEquals(expectedError, ex.getMessage());
266+
}
267+
268+
@Test
269+
public void testVerify_dsseBundleArtifactNotInSubjects() throws Exception {
270+
var bundleFile =
271+
Resources.toString(
272+
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
273+
StandardCharsets.UTF_8);
274+
var badArtifactDigest =
275+
Hashing.sha256().hashString("nonsense", StandardCharsets.UTF_8).asBytes();
276+
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
277+
278+
var ex =
279+
Assertions.assertThrows(
280+
KeylessVerificationException.class,
281+
() ->
282+
verifier.verify(
283+
badArtifactDigest,
284+
Bundle.from(new StringReader(bundleFile)),
285+
VerificationOptions.empty()));
286+
MatcherAssert.assertThat(
287+
ex.getMessage(),
288+
CoreMatchers.startsWith(
289+
"Provided artifact digest does not match any subject sha256 digests in DSSE payload"));
290+
}
234291
}

0 commit comments

Comments
 (0)