Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for verifying dsse-intoto #855

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
129 changes: 121 additions & 8 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 All @@ -201,7 +208,7 @@ void checkCertificateMatchers(X509Certificate cert, List<CertificateMatcher> mat
}
}

void checkMessageSignature(
private void checkMessageSignature(
MessageSignature messageSignature,
RekorEntry rekorEntry,
byte[] artifactDigest,
Expand Down Expand Up @@ -255,4 +262,110 @@ void checkMessageSignature(
throw new KeylessVerificationException("Unexpected rekor type", re);
}
}

// do all dsse specific checks
private 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log.warn?

}
}
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
Loading