diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index f24c83b9..0640711c 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java @@ -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; @@ -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. */ @@ -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"); @@ -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 { @@ -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 @@ -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()); @@ -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 @@ -244,4 +200,59 @@ void checkCertificateMatchers(X509Certificate cert, List 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); + } + } }