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 12, 2024
1 parent 4117822 commit 7a7e1b6
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 80 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"
222 changes: 168 additions & 54 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@
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;
import dev.sigstore.fulcio.client.FulcioVerifier;
import dev.sigstore.rekor.client.HashedRekordRequest;
import dev.sigstore.rekor.client.RekorEntry;
import dev.sigstore.rekor.client.RekorTypeException;
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 @@ -45,10 +52,11 @@
import java.security.spec.InvalidKeySpecException;
import java.sql.Date;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.bouncycastle.util.encoders.Base64;
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 @@ -122,16 +130,11 @@ 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.getDsseEnvelope().isEmpty() && bundle.getMessageSignature().isEmpty()) {
throw new IllegalStateException(
"Bundle must contain a message signature or dsse envelope to verify");
}

if (bundle.getMessageSignature().isEmpty()) {
// this should be unreachable
throw new IllegalStateException("Bundle must contain a message signature to verify");
}
var messageSignature = bundle.getMessageSignature().get();

if (bundle.getEntries().isEmpty()) {
throw new KeylessVerificationException("Cannot verify bundle without tlog entry");
}
Expand All @@ -149,20 +152,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
var signingCert = bundle.getCertPath();
var leafCert = Certificates.getLeaf(signingCert);

// this ensures the provided artifact digest matches what may have come from a bundle (in
// keyless signature)
if (messageSignature.getMessageDigest().isPresent()) {
var bundleDigest = messageSignature.getMessageDigest().get().getDigest();
if (!Arrays.equals(artifactDigest, bundleDigest)) {
throw new KeylessVerificationException(
"Provided artifact digest does not match digest used for verification"
+ "\nprovided(hex) : "
+ Hex.toHexString(artifactDigest)
+ "\nverification : "
+ Hex.toHexString(bundleDigest));
}
}

// verify the certificate chains up to a trusted root (fulcio) and contains a valid SCT from
// a trusted CT log
try {
Expand All @@ -175,8 +164,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
// verify the certificate identity if options are present
checkCertificateMatchers(leafCert, options.getCertificateMatchers());

var signature = messageSignature.getSignature();

RekorEntry rekorEntry = bundle.getEntries().get(0);

// verify the rekor entry is signed by the log keys
Expand All @@ -186,23 +173,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
throw new KeylessVerificationException("Rekor entry signature was not valid", ex);
}

// verify the log entry is relevant to the provided verification materials
try {
var calculatedHashedRekord =
Base64.toBase64String(
HashedRekordRequest.newHashedRekordRequest(
artifactDigest, Certificates.toPemBytes(leafCert), signature)
.toJsonPayload()
.getBytes(StandardCharsets.UTF_8));
if (!Objects.equals(calculatedHashedRekord, rekorEntry.getBody())) {
throw new KeylessVerificationException(
"Provided verification materials are inconsistent with log entry");
}
} catch (IOException e) {
// this should be unreachable, we know leafCert is a valid certificate at this point
throw new RuntimeException("Unexpected IOException on valid leafCert", e);
}

// check if the time of entry inclusion in the log (a stand-in for signing time) is within the
// validity period for the certificate
var entryTime = Date.from(rekorEntry.getIntegratedTimeInstant());
Expand All @@ -214,18 +184,11 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
throw new KeylessVerificationException("Signing time was after certificate expiry", e);
}

// finally check the supplied signature can be verified by the public key in the certificate
var publicKey = leafCert.getPublicKey();
try {
var verifier = Verifiers.newVerifier(publicKey);
if (!verifier.verifyDigest(artifactDigest, signature)) {
throw new KeylessVerificationException("Artifact signature was not valid");
}
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new RuntimeException(ex);
} catch (SignatureException ex) {
throw new KeylessVerificationException(
"Signature could not be processed: " + ex.getMessage(), ex);
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
checkMessageSignature(
bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
} else { // dsse
checkDsseEnvelope(rekorEntry, bundle.getDsseEnvelope().get(), artifactDigest, leafCert);
}
}

Expand All @@ -244,4 +207,155 @@ void checkCertificateMatchers(X509Certificate cert, List<CertificateMatcher> mat
"Could not verify certificate identities: " + ce.getMessage());
}
}

void checkMessageSignature(
MessageSignature messageSignature,
RekorEntry rekorEntry,
byte[] artifactDigest,
X509Certificate leafCert)
throws KeylessVerificationException {
// this ensures the provided artifact digest matches what may have come from a bundle (in
// keyless signature)
if (messageSignature.getMessageDigest().isPresent()) {
var bundleDigest = messageSignature.getMessageDigest().get().getDigest();
if (!Arrays.equals(artifactDigest, bundleDigest)) {
throw new KeylessVerificationException(
"Provided artifact digest does not match digest used for verification"
+ "\nprovided(hex) : "
+ Hex.toHexString(artifactDigest)
+ "\nverification(hex) : "
+ Hex.toHexString(bundleDigest));
}
}

// verify the signature over the artifact
var signature = messageSignature.getSignature();
try {
if (!Verifiers.newVerifier(leafCert.getPublicKey()).verifyDigest(artifactDigest, signature)) {
throw new KeylessVerificationException("Artifact signature was not valid");
}
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new RuntimeException(ex);
} catch (SignatureException ex) {
throw new KeylessVerificationException(
"Signature could not be processed: " + ex.getMessage(), ex);
}

// recreate the log entry and check if it matches what was provided in the rekorEntry
try {
RekorTypes.getHashedRekord(rekorEntry);
var calculatedHashedRekord =
Base64.getEncoder()
.encodeToString(
HashedRekordRequest.newHashedRekordRequest(
artifactDigest, Certificates.toPemBytes(leafCert), signature)
.toJsonPayload()
.getBytes(StandardCharsets.UTF_8));
if (!Objects.equals(calculatedHashedRekord, rekorEntry.getBody())) {
throw new KeylessVerificationException(
"Provided verification materials are inconsistent with log entry");
}
} catch (IOException e) {
// this should be unreachable, we know leafCert is a valid certificate at this point
throw new RuntimeException("Unexpected IOException on valid leafCert", e);
} catch (RekorTypeException re) {
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 dsse;
try {
dsse = RekorTypes.getDsse(rekorEntry);
} catch (RekorTypeException re) {
throw new KeylessVerificationException("Unexpected rekor type", re);
}

var algorithm = dsse.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(dsse.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"
+ "\nbundle(hex) : "
+ Hex.toHexString(calculatedDigest)
+ "\nlog(hex) : "
+ Hex.toHexString(payloadDigest));
}
}
}
12 changes: 9 additions & 3 deletions sigstore-java/src/main/java/dev/sigstore/bundle/Bundle.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public interface MessageDigest {
public interface DsseEnvelope {

/** An arbitrary payload that does not need to be parsed to be validated */
String getPayload();
byte[] getPayload();

/** Information on how to interpret the payload */
String getPayloadType();
Expand All @@ -158,12 +158,18 @@ default byte[] getPAE() {
+ " "
+ getPayloadType()
+ " "
+ getPayload().length()
+ getPayloadAsString().length()
+ " "
+ getPayload())
+ getPayloadAsString())
.getBytes(StandardCharsets.UTF_8);
}

@Lazy
@Gson.Ignore
default String getPayloadAsString() {
return new String(getPayload(), StandardCharsets.UTF_8);
}

@Lazy
@Gson.Ignore
default byte[] getSignature() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ static Bundle readBundle(Reader jsonReader) throws BundleParseException {
var dsseEnvelopeProto = protoBundle.getDsseEnvelope();
var dsseEnvelopeBuilder =
ImmutableDsseEnvelope.builder()
.payload(dsseEnvelopeProto.getPayload().toStringUtf8())
.payload(dsseEnvelopeProto.getPayload().toByteArray())
.payloadType(dsseEnvelopeProto.getPayloadType());
for (int sigIndex = 0; sigIndex < dsseEnvelopeProto.getSignaturesCount(); sigIndex++) {
dsseEnvelopeBuilder.addSignatures(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ interface Subject {
}

static InTotoPayload from(DsseEnvelope dsseEnvelope) {
return GSON.get().fromJson(dsseEnvelope.getPayload(), InTotoPayload.class);
return GSON.get().fromJson(dsseEnvelope.getPayloadAsString(), InTotoPayload.class);
}
}
Loading

0 comments on commit 7a7e1b6

Please sign in to comment.