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 <appu@google.com>
  • Loading branch information
loosebazooka committed Nov 21, 2024
1 parent adc8846 commit 8445766
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 60 deletions.
62 changes: 42 additions & 20 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
}

// finally check the supplied signature can be verified by the public key in the certificate
var signature = bundle.getMessageSignature().isPresent()
? bundle.getMessageSignature().get().getSignature()
: bundle.getDsseEnvelope().get().getSignature();
var publicKey = leafCert.getPublicKey();
try {
var verifier = Verifiers.newVerifier(publicKey);
if (!verifier.verifyDigest(artifactDigest, signature)) {
throw new KeylessVerificationException("Artifact signature was not valid");
if (bundle.getMessageSignature().isPresent()) {
if (!verifier.verifyDigest(
artifactDigest, bundle.getMessageSignature().get().getSignature())) {
throw new KeylessVerificationException("Artifact signature was not valid");
}
} else {
if (!verifier.verify(
bundle.getDsseEnvelope().get().getPAE(),
bundle.getDsseEnvelope().get().getSignature())) {
throw new KeylessVerificationException("DSSE signature was not valid");
}
}
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new RuntimeException(ex);
Expand Down Expand Up @@ -231,26 +237,42 @@ void checkMessageSignature(MessageSignature messageSignature, byte[] artifactDig
}
}

// since we don't check dsse signatures over the artifact, we must verify the artifact is in
// the subject list of the envelope
void checkDsseEnvelope(DsseEnvelope dsseEnvelope, byte[] artifactDigest)
throws KeylessVerificationException {
if (!Objects.equals(InTotoPayload.PAYLOAD_TYPE, dsseEnvelope.getPayloadType())) {
throw new KeylessVerificationException("DSSE envelope must have payload type " + InTotoPayload.PAYLOAD_TYPE + ", but found '" + dsseEnvelope.getPayloadType() + "'");
throw new KeylessVerificationException(
"DSSE envelope must have payload type "
+ InTotoPayload.PAYLOAD_TYPE
+ ", but found '"
+ dsseEnvelope.getPayloadType()
+ "'");
}
if (dsseEnvelope.getSignatures().size() != 1) {
throw new KeylessVerificationException(
"DSSE envelope must have exactly 1 signature, but found: "
+ dsseEnvelope.getSignatures().size());
}
// find one sha256 hash in the subject list that matches the artifact hash
InTotoPayload payload = InTotoPayload.from(dsseEnvelope);
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 and return false
}
}
return false;
})) {
var providedHashes = payload.getSubject().stream()
.map(s -> s.getDigest().getOrDefault("sha256", "no-sha256-hash")).collect(
Collectors.joining(",", "[", "]"));
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 and return 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"
Expand Down
38 changes: 34 additions & 4 deletions sigstore-java/src/main/java/dev/sigstore/bundle/Bundle.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertPath;
import java.util.List;
import java.util.Optional;
import org.immutables.gson.Gson;
import org.immutables.value.Value;
import org.immutables.value.Value.Default;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;
import org.immutables.value.Value.Lazy;

Expand Down Expand Up @@ -141,7 +144,36 @@ public interface DsseEnvelope {
String getPayloadType();

/** DSSE specific signature */
byte[] getSignature();
List<Signature> getSignatures();

/**
* The "Pre-Authentication Encoding" of this statement. The signature is generated over this
* content.
*/
@Gson.Ignore
@Derived
default byte[] getPAE() {
return ("DSSEv1 "
+ getPayloadType().length()
+ " "
+ getPayloadType()
+ " "
+ getPayload().length()
+ " "
+ getPayload())
.getBytes(StandardCharsets.UTF_8);
}

@Lazy
@Gson.Ignore
default byte[] getSignature() {
return getSignatures().get(0).getSig();
}

@Immutable
interface Signature {
byte[] getSig();
}
}

@Immutable
Expand All @@ -166,9 +198,7 @@ public String toJson() {
return BundleWriter.writeBundle(this);
}

/**
* Check if this bundle has MessageSignature, use to determine what verification method to use
*/
/** Check if this bundle has MessageSignature, use to determine what verification method to use */
public static boolean hasMessageSignature(Bundle bundle) {
return bundle.getMessageSignature().isPresent();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ static Bundle readBundle(Reader jsonReader) throws BundleParseException {

if (protoBundle.hasDsseEnvelope()) {
var dsseEnvelopeProto = protoBundle.getDsseEnvelope();
var dsseEnvelope =
var dsseEnvelopeBuilder =
ImmutableDsseEnvelope.builder()
.payload(dsseEnvelopeProto.getPayload().toStringUtf8())
.payloadType(dsseEnvelopeProto.getPayloadType())
.signature(dsseEnvelopeProto.getSignatures(0).toByteArray())
.build();
bundleBuilder.dsseEnvelope(dsseEnvelope);
.payloadType(dsseEnvelopeProto.getPayloadType());
for (int sigIndex = 0; sigIndex < dsseEnvelopeProto.getSignaturesCount(); sigIndex++) {
dsseEnvelopeBuilder.addSignatures(
ImmutableSignature.builder()
.sig(dsseEnvelopeProto.getSignatures(sigIndex).getSig().toByteArray())
.build());
}
bundleBuilder.dsseEnvelope(dsseEnvelopeBuilder.build());
} else if (protoBundle.hasMessageSignature()) {
var signature = protoBundle.getMessageSignature().getSignature().toByteArray();
if (protoBundle.getMessageSignature().hasMessageDigest()) {
Expand Down
30 changes: 20 additions & 10 deletions sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*
* Copyright 2024 The Sigstore Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sigstore.dsse;

import static dev.sigstore.json.GsonSupplier.GSON;
Expand All @@ -7,12 +22,10 @@
import java.util.List;
import java.util.Map;
import org.immutables.gson.Gson;
import org.immutables.gson.Gson.Ignore;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;

@Immutable
@Gson.TypeAdapters
@Immutable
public interface InTotoPayload {

String PAYLOAD_TYPE = "application/vnd.in-toto+json";
Expand All @@ -24,16 +37,13 @@ public interface InTotoPayload {

String getPredicateType();

/**
* Predicate is not processed by this library, if you want to inspect the contents of an
* attestation, you want to use an attestation parser.
*/
JsonElement getPredicate();

@Derived
@Ignore
default String getPredicateAsString() {
return getPredicate().toString();
}

@Immutable
@Gson.TypeAdapters
interface Subject {

String getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package dev.sigstore.json;

import com.google.gson.*;
import dev.sigstore.dsse.GsonAdaptersInTotoPayload;
import dev.sigstore.rekor.client.GsonAdaptersRekorEntry;
import dev.sigstore.rekor.client.GsonAdaptersRekorEntryBody;
import dev.sigstore.tuf.model.*;
Expand Down Expand Up @@ -59,6 +60,7 @@ public enum GsonSupplier implements Supplier<Gson> {
.registerTypeAdapterFactory(new GsonAdaptersTargetMeta())
.registerTypeAdapterFactory(new GsonAdaptersTimestamp())
.registerTypeAdapterFactory(new GsonAdaptersTimestampMeta())
.registerTypeAdapterFactory(new GsonAdaptersInTotoPayload())
.disableHtmlEscaping()
.create();

Expand Down
79 changes: 59 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,6 +28,8 @@
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.List;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -65,26 +68,6 @@ public void testVerify_mismatchedSet() 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 @@ -191,4 +174,60 @@ 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());
}

@Test
public void testVerify_dsseBundleBadSignature() throws Exception {
var bundleFile =
Resources.toString(
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.bad.sig.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("DSSE signature was not valid", ex.getMessage());
}

@Test
public void testVerify_dsseBundleArtifactNotInSubjects() throws Exception {
var bundleFile =
Resources.toString(
Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.bad.sig.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 8445766

Please sign in to comment.