diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index c68e919a..82c32b27 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,22 @@ 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) { + throw new RuntimeException(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/main/java/dev/sigstore/rekor/client/HashedRekordRequest.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/HashedRekordRequest.java index 3b982513..a00021f2 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/HashedRekordRequest.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/client/HashedRekordRequest.java @@ -19,7 +19,7 @@ import com.google.common.hash.Hashing; import com.google.common.primitives.Bytes; -import dev.sigstore.rekor.*; +import dev.sigstore.rekor.hashedRekord.v0_0_1.*; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorTypes.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorTypes.java index c82a938b..c2d92206 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorTypes.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorTypes.java @@ -17,7 +17,7 @@ import static dev.sigstore.json.GsonSupplier.GSON; -import dev.sigstore.rekor.HashedRekord; +import dev.sigstore.rekor.hashedRekord.v0_0_1.HashedRekord; /** Parser for the body.spec element of {@link RekorEntry}. */ public class RekorTypes { 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 7017467e..10e71208 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 @@ -46,7 +46,7 @@ private RekorVerifier(List tlogs) { } /** - * Verify that a Rekor Entry is signed with the rekor public key loaded into this verifier + * Verify that a Rekor Entry is signed with the rekor public key loaded into this verifier. * * @param entry the entry to verify * @throws RekorVerificationException if the entry cannot be verified diff --git a/sigstore-java/src/main/resources/rekor/model/hashedRekord.json b/sigstore-java/src/main/resources/rekor/model/hashedRekord/v0.0.1/hashedRekord.json similarity index 82% rename from sigstore-java/src/main/resources/rekor/model/hashedRekord.json rename to sigstore-java/src/main/resources/rekor/model/hashedRekord/v0.0.1/hashedRekord.json index b1ef6505..47cbac0e 100644 --- a/sigstore-java/src/main/resources/rekor/model/hashedRekord.json +++ b/sigstore-java/src/main/resources/rekor/model/hashedRekord/v0.0.1/hashedRekord.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://rekor.sigstore.dev/types/rekord/rekord_v0_0_1_schema.json", + "$id": "http://rekor.sigstore.dev/types/rekord/hashedrekord_v0_0_1_schema.json", "title": "Hashed Rekor v0.0.1 Schema", "description": "Schema for Hashed Rekord object", "type": "object", @@ -15,11 +15,11 @@ "format": "byte" }, "publicKey" : { - "description": "The public key that can verify the signature", + "description": "The public key that can verify the signature; this can also be an X509 code signing certificate that contains the raw public key information", "type": "object", "properties": { "content": { - "description": "Specifies the content of the public key inline within the document", + "description": "Specifies the content of the public key or code signing certificate inline within the document", "type": "string", "format": "byte" } @@ -38,10 +38,10 @@ "algorithm": { "description": "The hashing function used to compute the hash value", "type": "string", - "enum": [ "sha256" ] + "enum": [ "sha256", "sha384", "sha512" ] }, "value": { - "description": "The hash value for the content", + "description": "The hash value for the content, as represented by a lower case hexadecimal string", "type": "string" } }, 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" + } +}