Skip to content

Reorganize message signature checks #872

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

Merged
merged 1 commit into from
Dec 19, 2024
Merged
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
109 changes: 60 additions & 49 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@
import dev.sigstore.VerificationOptions.CertificateMatcher;
import dev.sigstore.VerificationOptions.UncheckedCertificateException;
import dev.sigstore.bundle.Bundle;
import dev.sigstore.bundle.Bundle.MessageSignature;
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.tuf.SigstoreTufClient;
Expand All @@ -45,10 +48,10 @@
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.Hex;

/** Verify hashrekords from rekor signed using the keyless signing flow with fulcio certificates. */
Expand Down Expand Up @@ -125,12 +128,10 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
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");
}
var messageSignature = bundle.getMessageSignature().get();

if (bundle.getEntries().isEmpty()) {
throw new KeylessVerificationException("Cannot verify bundle without tlog entry");
Expand All @@ -149,20 +150,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 +162,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 +171,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,19 +182,7 @@ 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);
}
checkMessageSignature(bundle.getMessageSignature().get(), rekorEntry, artifactDigest, leafCert);
}

@VisibleForTesting
Expand All @@ -244,4 +200,59 @@ 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);
}
}
}
Loading