diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index b8a798bd..6aa2f874 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java @@ -177,7 +177,7 @@ public void verify(byte[] artifactDigest, KeylessVerificationRequest request) try { rekorVerifier.verifyEntry(rekorEntry); } catch (RekorVerificationException ex) { - throw new KeylessVerificationException("Rekor entry signature was not valid"); + throw new KeylessVerificationException("Rekor entry signature was not valid", ex); } // check if the time of entry inclusion in the log (a stand-in for signing time) is within the diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java index 269add7a..cd000980 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java @@ -17,12 +17,15 @@ import com.google.common.hash.Hashing; import dev.sigstore.encryption.signers.Verifiers; +import dev.sigstore.rekor.client.RekorEntry.Checkpoint; import dev.sigstore.trustroot.SigstoreTrustedRoot; +import dev.sigstore.trustroot.TransparencyLog; import dev.sigstore.trustroot.TransparencyLogs; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; import java.util.Base64; import org.bouncycastle.util.encoders.Hex; @@ -84,6 +87,7 @@ public void verifyEntry(RekorEntry entry) throws RekorVerificationException { // verify inclusion proof verifyInclusionProof(entry); + verifyCheckpoint(entry, tlog); } /** Verify that a Rekor Entry is in the log by checking inclusion proof. */ @@ -134,6 +138,43 @@ private void verifyInclusionProof(RekorEntry entry) throws RekorVerificationExce } } + private void verifyCheckpoint(RekorEntry entry, TransparencyLog tlog) + throws RekorVerificationException { + Checkpoint checkpoint; + try { + checkpoint = entry.getVerification().getInclusionProof().parsedCheckpoint(); + } catch (RekorParseException ex) { + throw new RekorVerificationException("Could not parse checkpoint", ex); + } + + byte[] inclusionRootHash = + Hex.decode(entry.getVerification().getInclusionProof().getRootHash()); + byte[] checkpointRootHash = Base64.getDecoder().decode(checkpoint.getBase64Hash()); + + if (!Arrays.equals(inclusionRootHash, checkpointRootHash)) { + throw new RekorVerificationException( + "Checkpoint root hash does not match root hash provided in inclusion proof"); + } + var keyHash = Hashing.sha256().hashBytes(tlog.getPublicKey().getRawBytes()).asBytes(); + // checkpoint 0 is always the log, not any of the cross signing verifiers/monitors + var sig = checkpoint.getSignatures().get(0); + for (int i = 0; i < 4; i++) { + if (sig.getKeyHint()[i] != keyHash[i]) { + throw new RekorVerificationException( + "Checkpoint key hint did not match provided log public key"); + } + } + try { + Verifiers.newVerifier(tlog.getPublicKey().toJavaPublicKey()) + .verifyDigest(inclusionRootHash, sig.getSignature()); + } catch (NoSuchAlgorithmException + | InvalidKeySpecException + | SignatureException + | InvalidKeyException ex) { + throw new RekorVerificationException("Could not verify checkpoint signature", ex); + } + } + private static byte[] combineBytes(byte[] first, byte[] second) { byte[] result = new byte[first.length + second.length]; System.arraycopy(first, 0, result, 0, first.length); diff --git a/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/entry.json b/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/entry.json index b5295dfa..77315e96 100644 --- a/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/entry.json +++ b/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/entry.json @@ -11,17 +11,23 @@ "67e9d9f66f0ad388f7e1a20991e9a2ae3efad5cbf281e8b3d2aaf1ef99a4618c", "16a106400c53465f6e18c2475df6ba889ca30f5667bacf32b1a5661f14a5080c", "b4439e8d71edbc96271723cb7a969dd725e23e73d139361864a62ed76ce8dc11", - "f4926d3efb0abd42b18e18372886854825a2b378e249bd333f81d8d4534485c4", + "49b3e90806c7b63b5a86f5748e3ecb7d264ea0828eb74a45bc1a2cd7962408e8", + "5059ad9b48fa50bd9adcbff0dd81c5a0dcb60f37e0716e723a33805a464f72f8", "6c2ce64219799e61d72996884eee9e19fb906e4d7fa04b71625fde4108f21762", "784f79c817abb78db3ae99b6c1ede640470bf4bb678673a05bf3a6b50aaaddd6", - "0da021f68571b65e49e926e4c69024de3ac248a1319d254bc51a85a657b93c33" + "c6d92ebf4e10cdba500ca410166cd0a8d8b312154d2f45bc4292d63dea6112f6", + "1768732027401f6718b0df7769e2803127cfc099eb130a8ed7d913218f6a65f6", + "0da021f68571b65e49e926e4c69024de3ac248a1319d254bc51a85a657b93c33", + "bc8cf0c8497d5c24841de0c9bef598ec99bbd59d9538d58568340646fe289e9a", + "be328fa737b8fa9461850b8034250f237ff5b0b590b9468e6223968df294872b", + "6f06f4025d0346f04830352b23f65c8cd9e3ce4b8cb899877c35282521ddaf85" ], "logIndex": 1227, - "rootHash": "c2aaeaf36d5899ee2ab6d931bdc39d0c50e6a9dccee0322b6a9483538f4ef079", - "checkpoint": "The checkpoint (signed tree head) that the inclusion proof is based on", - "treeSize": 1237 + "rootHash": "effa4fa4575f72829016a64e584441203de533212f9470d63a56d1992e73465d", + "treeSize": 14358, + "checkpoint": "rekor.sigstage.dev - 108574341321668964\n14358\n7/pPpFdfcoKQFqZOWERBID3lMyEvlHDWOlbRmS5zRl0=\n\n— rekor.sigstage.dev 0y8wozBFAiB8OkuzdwlL6/rDEu2CsIfqmesaH/KLfmIMvlH3YTdIYgIhAPFZeXK6+b0vbWy4GSU/YZxiTpFrrzjsVOShN4LlPdZb\n" }, - "signedEntryTimestamp": "MEUCIAM2WgNNS1xwUBuoX/rYUBxbyKa6PVwNakss5KJzxw4jAiEA1//71yKdbHUi+rZRX7UyWBf4yBRp1vrkvOcWx6bbqfY=" + "signedEntryTimestamp": "MEUCIQCO8dFvolJwFZDHkhkSdsW3Ny+07fG8CF7G32feG8NJMgIgd2qfJ5shezuXX8I1S6DsudvIZ8xN/+y95at/V5xHfEQ=" } } }