diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java index 0227bc3d..90b2c3ae 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java @@ -63,8 +63,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nullable; +import org.bouncycastle.util.encoders.Base64; /** * A full sigstore keyless signing flow. @@ -335,6 +337,12 @@ public List sign(List artifactDigests) throws KeylessSignerExcep throw new KeylessSignerException("Failed to put entry in rekor", ex); } + var calculatedHashedRekord = + Base64.toBase64String(rekorRequest.toJsonPayload().getBytes(StandardCharsets.UTF_8)); + if (!Objects.equals(calculatedHashedRekord, rekorResponse.getEntry().getBody())) { + throw new KeylessSignerException("Returned log entry was inconsistent with request"); + } + try { rekorVerifier.verifyEntry(rekorResponse.getEntry()); } catch (RekorVerificationException ex) { diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index c68e919a..f08213c1 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java @@ -26,11 +26,13 @@ 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.RekorVerificationException; import dev.sigstore.rekor.client.RekorVerifier; import dev.sigstore.tuf.SigstoreTufClient; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -44,7 +46,9 @@ import java.sql.Date; import java.util.Arrays; 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. */ @@ -182,6 +186,23 @@ 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()); diff --git a/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java index c9549193..733ada9f 100644 --- a/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java @@ -65,6 +65,27 @@ public void testVerify_mismatchedSet() throws Exception { VerificationOptions.empty())); } + @Test + public void testVerify_mismatchedArtifactHash() throws Exception { + // a bundle file that uses the tlog entry from another artifact signed with the same + // certificate. The Bundle is fully valid except that the artifact hash doesn't match + var bundleFile = + Resources.toString( + Resources.getResource( + "dev/sigstore/samples/bundles/bundle-with-wrong-tlog-entry.sigstore"), + StandardCharsets.UTF_8); + var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath(); + + var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build(); + Assertions.assertThrows( + KeylessVerificationException.class, + () -> + verifier.verify( + Path.of(artifact), + Bundle.from(new StringReader(bundleFile)), + VerificationOptions.empty())); + } + @Test public void testVerify_errorsOnDSSEBundle() throws Exception { var bundleFile = diff --git a/sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle-with-wrong-tlog-entry.sigstore b/sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle-with-wrong-tlog-entry.sigstore new file mode 100644 index 00000000..74abc0b6 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle-with-wrong-tlog-entry.sigstore @@ -0,0 +1,39 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "tlogEntries": [{ + "logIndex": "150603746", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "hashedrekord", + "version": "0.0.1" + }, + "integratedTime": "1732221080", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIQDY5kN3WEDvMndKO5JImUGrtV5jW1cVGnEDylEKRk7ZsAIgZN1MIv+eILssoHtsqqk+b9Tda+2ZN3Lkz1Mw8GwTD9k=" + }, + "inclusionProof": { + "logIndex": "28699484", + "rootHash": "p+AbctDd928yM1oCzeJPou8nHME2Qyka0DMB1tOlqsI=", + "treeSize": "28699488", + "hashes": ["fcyRKy95RMsZh/HeRQaZK8jFhhrdqqA5iFs+wInZsaI=", "zl59JwaJVeTvAF5pCTPbaXXdXgsYidumj8TgjRvcZIA=", "4mck2Czn6tiBqOQYPlh3ATO9DaTvouT7biFAkCz4bWM=", "BipkUG/DzduS/XjrbEd3uC5odEYaC9OTJTYElA10POo=", "9yDO9YZftL6yDveO6bWY32s8D4y3+uWUKVLEPmRwbHk=", "GMMIMtm+cQ0ajPC1YWzZCJIZ517pDeYJ5+FnZinynQg=", "9henJICU+Lo3pa+Rrtnt8+5esX8hhksuDcqmaYvxLHA=", "aL2yyr/c5w5R72E6L+AagxQB/oMUftqB7fHIs625QR0=", "Fvp+S31pMTO9ts92nEBj6sYay8OFauXxYrevM73sg3U=", "tk7EpIXIblK0ktOJNY9T+WCMpW0loWy+GouFKk7me8k=", "Ky28cDNBQqCVLHhOLegW99yo/fkUTkjmWsr56fXTt+E=", "PTN8SB4uaeclDiwUlPK/FSmiK7voPpkD8GBfNeRCFnw=", "0C1qnoT2c+8KErA01/VosqRATcsPFXeswb8bk/eb/3g=", "5EVC7yaMdjhwDR6OLXLyveRuRrF8xvXTzlm+8+vzfxE=", "bulsENariUUsC4xiR1yFtqKzD8evI9p/s+YCpl8t9tE=", "E2rLOYPJFKiizYiyu07QLqkMVTVL7i2ZgXiQywdI9KQ=", "4lUF0YOu9XkIDXKXA0wMSzd6VeDY3TZAgmoOeWmS2+Y=", "gf+9m552B3PnkWnO0o4KdVvjcT3WVHLrCbf1DoVYKFw="], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n28699488\np+AbctDd928yM1oCzeJPou8nHME2Qyka0DMB1tOlqsI\u003d\n\n— rekor.sigstore.dev wNI9ajBFAiEA+50Xtrawp0K2slioi3f8lpCrZGu8k919PDoZ+Z80/WUCIDaUTds/GljSB8/Mu7DLj879oi/odHrPZ0OUeubKlBGd\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJkNDBlYzNlZWJjYjI5OTE0OTRmMjI3MzZhM2Y1ZmE3YjJlNGI2OTcyZDdjMDQ2ZDVjNWQ3ZTUwNmFiZTlmNzUyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRWhQanlEZXFCdDJyNnBkNTVTczVYdi9rTG54NXozb0hDOE42TCtWOW1MSUFpQktpQ2xLcTNPV2ttcHZUcXpyRDZ1a2RmdTQvRXhGVVVmdHoxcnMvL04ySnc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjRla05EUVdzeVowRjNTVUpCWjBsVlJteERWeTlHUWtabFVrRmtkVU12Y1c0eGVrNTBOVWt2YlN0SmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFVU1hoTmFrRjZUVlJGTlZkb1kwNU5hbEY0VFZSSmVFMXFRVEJOVkVVMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZzWlhwUWFXeEtWVlZqTkV0cGJtWkVURTl0UTNobVRVNXpLMlJ4Ums5c2JYWkNZMnNLUW1aaWRHeHRXRWRpWTNKRGVtTkxNWGRuTm5SdVYzVklObmhHZUhKclJFUmxaMU5ITDBoYU1HcGpVekJMVUU0M1EyRlBRMEZYZDNkblowWnZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZ4TVZsQ0NteHZUbXg1VDFkU2FuUkZOREZtTVdkeU5GbEZiR2haZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVdWaENkMlJWUW01aU1qbHVZa2RWZFZreU9YUk5RMnRIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVGeVFtZHZja0puUlVWQldVOHZUVUZGU1VKQ01FMUhNbWd3Q21SSVFucFBhVGgyV1ZkT2FtSXpWblZrU0UxMVdqSTVkbG95ZUd4TWJVNTJZbFJEUW1sUldVdExkMWxDUWtGSVYyVlJTVVZCWjFJM1FraHJRV1IzUWpFS1FVNHdPVTFIY2tkNGVFVjVXWGhyWlVoS2JHNU9kMHRwVTJ3Mk5ETnFlWFF2TkdWTFkyOUJka3RsTms5QlFVRkNhekZDYzBWTU5FRkJRVkZFUVVWWmR3cFNRVWxuUkdkNE5tUkNNMHR2YlhoUGVVSXdaakY1SzNZMGJUYzNNWE4yY1dWMUszZHVjazFUVG14WWNHNVNXVU5KUkRkTlZVUjVRa1ZhVlhobmVUSmFDbFJWZDNnd1NUWnllR2hOTm5weU5qRXJNamc0YXpkQmVYQktMMlZOUVc5SFEwTnhSMU5OTkRsQ1FVMUVRVEpuUVUxSFZVTk5VVU16VGpaR05sUlZWVk1LYTA5bVNrSnRZMWhPV2psSFRGaEhWbFYzV2xaQ1IwWmtSSEF2VkZkT1ZrNVZTeTlyT1ZZNE1VcFFXbFF6TkRWcVVUVm5SMUEzVFVOTlJXWjNPVEZUV2dwV00zSllZVWd6VkdGRFZYZGFSM0pQY205RE5XSmFNMGRzYW1oQ05raEdSMk5OUjFSM1JXUlFObE0ySzNsNVNHOTBkbkpYVW5kSFEwOVJQVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In19fX0=" + }], + "certificate": { + "rawBytes": "MIICxzCCAk2gAwIBAgIUFlCW/FBFeRAduC/qn1zNt5I/m+IwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMTIxMjAzMTE5WhcNMjQxMTIxMjA0MTE5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElezPilJUUc4KinfDLOmCxfMNs+dqFOlmvBckBfbtlmXGbcrCzcK1wg6tnWuH6xFxrkDDegSG/HZ0jcS0KPN7CaOCAWwwggFoMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUq1YBloNlyOWRjtE41f1gr4YElhYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHQYDVR0RAQH/BBMwEYEPYXBwdUBnb29nbGUuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABk1BsEL4AAAQDAEYwRAIgDgx6dB3KomxOyB0f1y+v4m771svqeu+wnrMSNlXpnRYCID7MUDyBEZUxgy2ZTUwx0I6rxhM6zr61+288k7AypJ/eMAoGCCqGSM49BAMDA2gAMGUCMQC3N6F6TUUSkOfJBmcXNZ9GLXGVUwZVBGFdDp/TWNVNUK/k9V81JPZT345jQ5gGP7MCMEfw91SZV3rXaH3TaCUwZGrOroC5bZ3GljhB6HFGcMGTwEdP6S6+yyHotvrWRwGCOQ==" + } + }, + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_256", + "digest": "oM/HEnHW4njlfNMy/5V8P3BD/do1TEy7GQow1W76Ab8=" + }, + "signature": "MEYCIQCGZcJ+4mTIMPAyIKV7XZjZ5KyvX1tz/glbin9vVF+9fwIhANUb5z/2Rb88vtvtABHAmQyBl4Yp8ipMLCnv36Ln8/Eb" + } +}