Skip to content

Commit

Permalink
Rekor Verifier should match artifact hash
Browse files Browse the repository at this point in the history
Signed-off-by: Appu Goundan <[email protected]>
  • Loading branch information
loosebazooka committed Nov 22, 2024
1 parent b440f06 commit 57e134d
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 18 deletions.
3 changes: 2 additions & 1 deletion fuzzing/src/main/java/fuzzing/RekorVerifierFuzzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ public class RekorVerifierFuzzer {
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
try {
var tLogs = Tuf.transparencyLogsFrom(data);
byte[] digest = data.consumeBytes(32);
byte[] byteArray = data.consumeRemainingAsBytes();
String string = new String(byteArray, StandardCharsets.UTF_8);

URI uri = new URI(URL);
RekorEntry entry = RekorResponse.newRekorResponse(uri, string).getEntry();
RekorVerifier verifier = RekorVerifier.newRekorVerifier(tLogs);

verifier.verifyEntry(entry);
verifier.verifyHashedRekord(entry, digest);
} catch (URISyntaxException | RekorParseException | RekorVerificationException e) {
// Known exception
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ public List<Bundle> sign(List<byte[]> artifactDigests) throws KeylessSignerExcep
}

try {
rekorVerifier.verifyEntry(rekorResponse.getEntry());
rekorVerifier.verifyHashedRekord(rekorResponse.getEntry(), artifactDigest);
} catch (RekorVerificationException ex) {
throw new KeylessSignerException("Failed to validate rekor response after signing", ex);
}
Expand Down
4 changes: 2 additions & 2 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt

RekorEntry rekorEntry = bundle.getEntries().get(0);

// verify the rekor entry is signed by the log keys
// verify the rekor entry is signed by the log keys and is relevant to this artifact
try {
rekorVerifier.verifyEntry(rekorEntry);
rekorVerifier.verifyHashedRekord(rekorEntry, artifactDigest);
} catch (RekorVerificationException ex) {
throw new KeylessVerificationException("Rekor entry signature was not valid", ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
package dev.sigstore.rekor.client;

import com.google.common.hash.Hashing;
import com.google.gson.JsonSyntaxException;
import dev.sigstore.encryption.signers.Verifiers;
import dev.sigstore.json.GsonSupplier;
import dev.sigstore.rekor.client.RekorEntry.Checkpoint;
import dev.sigstore.rekor.hashedRekord.v0_0_1.Hash.Algorithm;
import dev.sigstore.rekor.hashedRekord.v0_0_1.HashedRekord;
import dev.sigstore.trustroot.SigstoreTrustedRoot;
import dev.sigstore.trustroot.TransparencyLog;
import java.security.InvalidKeyException;
Expand All @@ -27,6 +31,8 @@
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import org.bouncycastle.util.encoders.DecoderException;
import org.bouncycastle.util.encoders.Hex;

/** Verifier for rekor entries. */
Expand All @@ -46,12 +52,54 @@ private RekorVerifier(List<TransparencyLog> 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 any public key loaded into this verifier. Also checks
* if the embedded hashrekord has a matching artifactDigest.
*
* @param entry the entry to verify
* @throws RekorVerificationException if the entry cannot be verified
*/
public void verifyEntry(RekorEntry entry) throws RekorVerificationException {
public void verifyHashedRekord(RekorEntry entry, byte[] artifactDigest)
throws RekorVerificationException {
verifyEntry(entry);

try {
RekorEntryBody entryBody = entry.getBodyDecoded();
if (!Objects.equals(entryBody.getKind(), "hashedrekord")
|| !Objects.equals(entryBody.getApiVersion(), "0.0.1")) {
throw new RekorVerificationException(
"Rekor entry was not expected type hashedrekord:0.0.1");
}
HashedRekord hashedRekord =
GsonSupplier.GSON.get().fromJson(entryBody.getSpec(), HashedRekord.class);
var algorithm = hashedRekord.getData().getHash().getAlgorithm();
if (hashedRekord.getData().getHash().getAlgorithm() != Algorithm.SHA_256) {
throw new RekorVerificationException(
"Cannot process hashedRekord with hashing algorithm " + algorithm.toString());
}
var entryDigest = Hex.decode(hashedRekord.getData().getHash().getValue());
if (!Arrays.equals(artifactDigest, entryDigest)) {
throw new RekorVerificationException(
"Provided artifact digest does not match digest in log entry"
+ "\nprovided(hex) : "
+ Hex.toHexString(artifactDigest)
+ "\nlog(hex) : "
+ Hex.toHexString(entryDigest));
}
} catch (JsonSyntaxException jse) {
throw new RekorVerificationException("Could not parse hashrekord during verification", jse);
} catch (DecoderException de) {
throw new RekorVerificationException(
"Could not decode hex sha256 artifact hash in hashrekord", de);
}
}

/**
* 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
*/
private void verifyEntry(RekorEntry entry) throws RekorVerificationException {
if (entry.getVerification() == null) {
throw new RekorVerificationException("No verification information in entry.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"
}
Expand All @@ -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"
}
},
Expand Down
21 changes: 21 additions & 0 deletions sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
public class RekorVerifierTest {
public String rekorResponse;
public byte[] rekorPub;
public byte[] artifactHash;

public static SigstoreTrustedRoot trustRoot;

Expand All @@ -54,6 +55,7 @@ public void loadResources() throws IOException {
rekorPub =
Resources.toByteArray(
Resources.getResource("dev/sigstore/samples/rekor-response/valid/rekor.pub"));
artifactHash = Hex.decode("47c1db9fb5fe7ff66e3d7c15b2ca5d3a046ee8f4aa043dedd317e46b321c4390");
}

@BeforeAll
Expand All @@ -77,17 +79,34 @@ public void verifyEntry_invalid() throws Exception {

var thrown =
Assertions.assertThrows(
RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry()));
RekorVerificationException.class,
() -> verifier.verifyHashedRekord(response.getEntry(), artifactHash));
Assertions.assertEquals("Entry SET was not valid", thrown.getMessage());
}

@Test
public void verifyEntry_withMismatchedArtifactHash() throws Exception {
var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse);
var verifier = RekorVerifier.newRekorVerifier(trustRoot);
// change the provided artifact hash
artifactHash[0] = 10;

var thrown =
Assertions.assertThrows(
RekorVerificationException.class,
() -> verifier.verifyHashedRekord(response.getEntry(), artifactHash));
MatcherAssert.assertThat(
thrown.getMessage(),
CoreMatchers.startsWith("Provided artifact digest does not match digest in log entry"));
}

@Test
public void verifyEntry() throws Exception {
var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse);
var verifier = RekorVerifier.newRekorVerifier(trustRoot);

var entry = response.getEntry();
verifier.verifyEntry(entry);
verifier.verifyHashedRekord(entry, artifactHash);
}

@Test
Expand All @@ -101,7 +120,8 @@ public void verifyEntry_withInvalidInclusionProof() throws Exception {
var entry = response.getEntry();
var thrown =
Assertions.assertThrows(
RekorVerificationException.class, () -> verifier.verifyEntry(entry));
RekorVerificationException.class,
() -> verifier.verifyHashedRekord(entry, artifactHash));

MatcherAssert.assertThat(
thrown.getMessage(),
Expand Down Expand Up @@ -139,7 +159,8 @@ public void verifyEntry_logIdMismatch() throws Exception {

var thrown =
Assertions.assertThrows(
RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry()));
RekorVerificationException.class,
() -> verifier.verifyHashedRekord(response.getEntry(), artifactHash));
Assertions.assertEquals(
"Log entry (logid, timestamp) does not match any provided transparency logs.",
thrown.getMessage());
Expand Down Expand Up @@ -180,7 +201,8 @@ public void verifyEntry_logIdTimeMismatch() throws Exception {

var thrown =
Assertions.assertThrows(
RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry()));
RekorVerificationException.class,
() -> verifier.verifyHashedRekord(response.getEntry(), artifactHash));
Assertions.assertEquals(
"Log entry (logid, timestamp) does not match any provided transparency logs.",
thrown.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}

0 comments on commit 57e134d

Please sign in to comment.