Skip to content

Commit

Permalink
Add support for verifying dsse-intoto
Browse files Browse the repository at this point in the history
- 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]>
  • Loading branch information
loosebazooka committed Dec 18, 2024
1 parent c36fc5f commit d6b598c
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:
with:
entrypoint: ${{ github.workspace }}/bin/sigstore-cli
environment: ${{ matrix.sigstore-env }}
xfail: "test_verify_dsse_bundle_with_trust_root test_verify_in_toto_in_dsse_envelope"
xfail: "test_verify_dsse_bundle_with_trust_root"
127 changes: 120 additions & 7 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import dev.sigstore.VerificationOptions.CertificateMatcher;
import dev.sigstore.VerificationOptions.UncheckedCertificateException;
import dev.sigstore.bundle.Bundle;
import dev.sigstore.bundle.Bundle.DsseEnvelope;
import dev.sigstore.bundle.Bundle.MessageSignature;
import dev.sigstore.dsse.InTotoPayload;
import dev.sigstore.encryption.certificates.Certificates;
import dev.sigstore.encryption.signers.Verifiers;
import dev.sigstore.fulcio.client.FulcioVerificationException;
Expand All @@ -33,6 +35,8 @@
import dev.sigstore.rekor.client.RekorTypes;
import dev.sigstore.rekor.client.RekorVerificationException;
import dev.sigstore.rekor.client.RekorVerifier;
import dev.sigstore.rekor.dsse.v0_0_1.Dsse;
import dev.sigstore.rekor.dsse.v0_0_1.PayloadHash;
import dev.sigstore.tuf.SigstoreTufClient;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand All @@ -52,6 +56,7 @@
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.bouncycastle.util.encoders.DecoderException;
import org.bouncycastle.util.encoders.Hex;

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

if (bundle.getDsseEnvelope().isPresent()) {
throw new KeylessVerificationException("Cannot verify DSSE signature based bundles");
}
if (bundle.getMessageSignature().isEmpty()) {
// this should be unreachable
throw new IllegalStateException("Bundle must contain a message signature to verify");
if (bundle.getDsseEnvelope().isEmpty() && bundle.getMessageSignature().isEmpty()) {
throw new IllegalStateException(
"Bundle must contain a message signature or DSSE envelope to verify");
}

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

checkMessageSignature(bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
checkMessageSignature(
bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
} else { // dsse
checkDsseEnvelope(rekorEntry, bundle.getDsseEnvelope().get(), artifactDigest, leafCert);
}
}

@VisibleForTesting
Expand Down Expand Up @@ -255,4 +262,110 @@ void checkMessageSignature(
throw new KeylessVerificationException("Unexpected rekor type", re);
}
}

// do all dsse specific checks
void checkDsseEnvelope(
RekorEntry rekorEntry,
DsseEnvelope dsseEnvelope,
byte[] artifactDigest,
X509Certificate leafCert)
throws KeylessVerificationException {

// verify the artifact is in the subject list of the envelope
if (!Objects.equals(InTotoPayload.PAYLOAD_TYPE, dsseEnvelope.getPayloadType())) {
throw new KeylessVerificationException(
"DSSE envelope must have payload type "
+ InTotoPayload.PAYLOAD_TYPE
+ ", but found '"
+ dsseEnvelope.getPayloadType()
+ "'");
}
InTotoPayload payload = InTotoPayload.from(dsseEnvelope);

// find one sha256 hash in the subject list that matches the artifact hash
if (payload.getSubject().stream()
.noneMatch(
subject -> {
if (subject.getDigest().containsKey("sha256")) {
try {
var digestBytes = Hex.decode(subject.getDigest().get("sha256"));
return Arrays.equals(artifactDigest, digestBytes);
} catch (DecoderException de) {
// ignore (assume false)
}
}
return false;
})) {
var providedHashes =
payload.getSubject().stream()
.map(s -> s.getDigest().getOrDefault("sha256", "no-sha256-hash"))
.collect(Collectors.joining(",", "[", "]"));

throw new KeylessVerificationException(
"Provided artifact digest does not match any subject sha256 digests in DSSE payload"
+ "\nprovided(hex) : "
+ Hex.toHexString(artifactDigest)
+ "\nverification : "
+ providedHashes);
}

// verify the dsse signature
if (dsseEnvelope.getSignatures().size() != 1) {
throw new KeylessVerificationException(
"DSSE envelope must have exactly 1 signature, but found: "
+ dsseEnvelope.getSignatures().size());
}
try {
if (!Verifiers.newVerifier(leafCert.getPublicKey())
.verify(dsseEnvelope.getPAE(), dsseEnvelope.getSignature())) {
throw new KeylessVerificationException("DSSE signature was not valid");
}
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new RuntimeException(ex);
} catch (SignatureException se) {
throw new KeylessVerificationException("Signature could not be processed", se);
}

// check if the digest over the dsse payload matches the digest in the rekorEntry
Dsse rekorDsse;
try {
rekorDsse = RekorTypes.getDsse(rekorEntry);
} catch (RekorTypeException re) {
throw new KeylessVerificationException("Unexpected rekor type", re);
}

var algorithm = rekorDsse.getPayloadHash().getAlgorithm();
if (algorithm != PayloadHash.Algorithm.SHA_256) {
throw new KeylessVerificationException(
"Cannot process DSSE entry with hashing algorithm " + algorithm.toString());
}

byte[] payloadDigest;
try {
payloadDigest = Hex.decode(rekorDsse.getPayloadHash().getValue());
} catch (DecoderException de) {
throw new KeylessVerificationException(
"Could not decode hex sha256 artifact hash in hashrekord", de);
}

byte[] calculatedDigest = Hashing.sha256().hashBytes(dsseEnvelope.getPayload()).asBytes();
if (!Arrays.equals(calculatedDigest, payloadDigest)) {
throw new KeylessVerificationException(
"Digest of DSSE payload in bundle does not match DSSE payload digest in log entry");
}

// check if the signature over the dsse payload matches the signature in the rekorEntry
if (rekorDsse.getSignatures().size() != 1) {
throw new KeylessVerificationException(
"DSSE log entry must have exactly 1 signature, but found: "
+ rekorDsse.getSignatures().size());
}

if (!Base64.getEncoder()
.encodeToString(dsseEnvelope.getSignature())
.equals(rekorDsse.getSignatures().get(0).getSignature())) {
throw new KeylessVerificationException(
"Provided DSSE signature materials are inconsistent with DSSE log entry");
}
}
}
97 changes: 77 additions & 20 deletions sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package dev.sigstore;

import com.google.common.collect.ImmutableList;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import dev.sigstore.VerificationOptions.CertificateMatcher;
import dev.sigstore.bundle.Bundle;
Expand All @@ -27,8 +28,14 @@
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.stream.Stream;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class KeylessVerifierTest {

Expand Down Expand Up @@ -105,26 +112,6 @@ public void testVerify_badCheckpointSignature() throws Exception {
VerificationOptions.empty()));
}

@Test
public void testVerify_errorsOnDSSEBundle() throws Exception {
var bundleFile =
Resources.toString(
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
StandardCharsets.UTF_8);
var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();

var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
var ex =
Assertions.assertThrows(
KeylessVerificationException.class,
() ->
verifier.verify(
Path.of(artifact),
Bundle.from(new StringReader(bundleFile)),
VerificationOptions.empty()));
Assertions.assertEquals("Cannot verify DSSE signature based bundles", ex.getMessage());
}

@Test
public void testVerify_canVerifyV01Bundle() throws Exception {
// note that this v1 bundle contains an inclusion proof
Expand Down Expand Up @@ -231,4 +218,74 @@ public void verifyCertificateMatches_noneMatch() throws Exception {
"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'}]",
ex.getMessage());
}

@Test
public void testVerify_dsseBundle() throws Exception {
var bundleFile =
Resources.toString(
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
StandardCharsets.UTF_8);
var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();

var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
verifier.verify(
Path.of(artifact), Bundle.from(new StringReader(bundleFile)), VerificationOptions.empty());
}

static Stream<Arguments> badDsseProvider() {
return Stream.of(
Arguments.arguments("bundle.dsse.bad-signature.sigstore", "DSSE signature was not valid"),
Arguments.arguments(
"bundle.dsse.mismatched-envelope.sigstore",
"Digest of DSSE payload in bundle does not match DSSE payload digest in log entry"),
Arguments.arguments(
"bundle.dsse.mismatched-signature.sigstore",
"Provided DSSE signature materials are inconsistent with DSSE log entry"));
}

@ParameterizedTest
@MethodSource("badDsseProvider")
public void testVerify_dsseBundleBadSignature(String bundleName, String expectedError)
throws Exception {
var bundleFile =
Resources.toString(
Resources.getResource("dev/sigstore/samples/bundles/" + bundleName),
StandardCharsets.UTF_8);
var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();

var ex =
Assertions.assertThrows(
KeylessVerificationException.class,
() ->
verifier.verify(
Path.of(artifact),
Bundle.from(new StringReader(bundleFile)),
VerificationOptions.empty()));
Assertions.assertEquals(expectedError, ex.getMessage());
}

@Test
public void testVerify_dsseBundleArtifactNotInSubjects() throws Exception {
var bundleFile =
Resources.toString(
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
StandardCharsets.UTF_8);
var badArtifactDigest =
Hashing.sha256().hashString("nonsense", StandardCharsets.UTF_8).asBytes();
var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();

var ex =
Assertions.assertThrows(
KeylessVerificationException.class,
() ->
verifier.verify(
badArtifactDigest,
Bundle.from(new StringReader(bundleFile)),
VerificationOptions.empty()));
MatcherAssert.assertThat(
ex.getMessage(),
CoreMatchers.startsWith(
"Provided artifact digest does not match any subject sha256 digests in DSSE payload"));
}
}
Loading

0 comments on commit d6b598c

Please sign in to comment.