diff --git a/fuzzing/src/main/java/fuzzing/RekorTypesFuzzer.java b/fuzzing/src/main/java/fuzzing/RekorTypesFuzzer.java index 17fcfb9f..ff2c12c3 100644 --- a/fuzzing/src/main/java/fuzzing/RekorTypesFuzzer.java +++ b/fuzzing/src/main/java/fuzzing/RekorTypesFuzzer.java @@ -29,16 +29,30 @@ public class RekorTypesFuzzer { public static void fuzzerTestOneInput(FuzzedDataProvider data) { try { - int type = data.pickValue(new int[] {0, 1}); + int type = data.pickValue(new int[] {0, 1, 2, 3}); String string = data.consumeRemainingAsString(); - URI uri = new URI(URL); - RekorEntry entry = RekorResponse.newRekorResponse(uri, string).getEntry(); - - if (type == 0) { - RekorTypes.getHashedRekord(entry); + RekorEntry entry; + if (type < 2) { + URI uri = new URI(URL); + entry = RekorResponse.newRekorResponse(uri, string).getEntry(); } else { - RekorTypes.getDsse(entry); + entry = RekorEntry.fromTLogEntryJson(string); + } + + switch (type) { + case 0: + RekorTypes.getHashedRekordV001(entry); + break; + case 1: + RekorTypes.getDsseV001(entry); + break; + case 2: + RekorTypes.getHashedRekordV002(entry); + break; + case 3: + RekorTypes.getDsseV002(entry); + break; } } catch (URISyntaxException | RekorTypeException | RekorParseException e) { // Known exception diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index 8e6e7717..ccd767c8 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java @@ -19,8 +19,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.hash.Hashing; import com.google.common.io.Files; -import com.google.gson.Gson; -import com.google.protobuf.InvalidProtocolBufferException; import dev.sigstore.VerificationOptions.CertificateMatcher; import dev.sigstore.VerificationOptions.UncheckedCertificateException; import dev.sigstore.bundle.Bundle; @@ -31,7 +29,6 @@ import dev.sigstore.encryption.signers.Verifiers; import dev.sigstore.fulcio.client.FulcioVerificationException; import dev.sigstore.fulcio.client.FulcioVerifier; -import dev.sigstore.json.ProtoJson; import dev.sigstore.proto.common.v1.HashAlgorithm; import dev.sigstore.proto.rekor.v2.DSSELogEntryV002; import dev.sigstore.proto.rekor.v2.HashedRekordLogEntryV002; @@ -71,7 +68,9 @@ import org.bouncycastle.util.encoders.DecoderException; import org.bouncycastle.util.encoders.Hex; -/** Verify hashrekords from rekor signed using the keyless signing flow with fulcio certificates. */ +/** + * Verify hashedrekords from rekor signed using the keyless signing flow with fulcio certificates. + */ public class KeylessVerifier { private final FulcioVerifier fulcioVerifier; @@ -271,7 +270,7 @@ private void checkMessageSignature( String version = rekorEntry.getBodyDecoded().getApiVersion(); if ("0.0.1".equals(version)) { try { - RekorTypes.getHashedRekord(rekorEntry); + RekorTypes.getHashedRekordV001(rekorEntry); var calculatedHashedRekord = HashedRekordRequest.newHashedRekordRequest( artifactDigest, Certificates.toPemBytes(leafCert), signature) @@ -291,21 +290,10 @@ private void checkMessageSignature( } else if ("0.0.2".equals(version)) { HashedRekordLogEntryV002 logEntrySpec; try { - HashedRekordLogEntryV002.Builder builder = HashedRekordLogEntryV002.newBuilder(); - ProtoJson.parser() - .ignoringUnknownFields() - .merge( - new Gson() - .toJson( - rekorEntry - .getBodyDecoded() - .getSpec() - .getAsJsonObject() - .get("hashedRekordV002")), - builder); - logEntrySpec = builder.build(); - } catch (InvalidProtocolBufferException ipbe) { - throw new KeylessVerificationException("Could not parse hashedrekord from log entry body"); + logEntrySpec = RekorTypes.getHashedRekordV002(rekorEntry); + } catch (RekorTypeException re) { + throw new KeylessVerificationException( + "Could not parse hashedrekord from log entry body", re); } if (!logEntrySpec.getData().getAlgorithm().equals(HashAlgorithm.SHA2_256)) { @@ -408,7 +396,7 @@ private void checkDsseEnvelope( if ("0.0.1".equals(version)) { Dsse rekorDsse; try { - rekorDsse = RekorTypes.getDsse(rekorEntry); + rekorDsse = RekorTypes.getDsseV001(rekorEntry); } catch (RekorTypeException re) { throw new KeylessVerificationException("Unexpected rekor type", re); } @@ -425,7 +413,7 @@ private void checkDsseEnvelope( payloadDigest = Hex.decode(rekorDsse.getPayloadHash().getValue()); } catch (DecoderException de) { throw new KeylessVerificationException( - "Could not decode hex sha256 artifact hash in hashrekord", de); + "Could not decode hex sha256 artifact hash in hashedrekord", de); } byte[] calculatedDigest = Hashing.sha256().hashBytes(dsseEnvelope.getPayload()).asBytes(); @@ -450,16 +438,9 @@ private void checkDsseEnvelope( } else if ("0.0.2".equals(version)) { DSSELogEntryV002 logEntrySpec; try { - DSSELogEntryV002.Builder builder = DSSELogEntryV002.newBuilder(); - ProtoJson.parser() - .merge( - new Gson() - .toJson( - rekorEntry.getBodyDecoded().getSpec().getAsJsonObject().get("dsseV002")), - builder); - logEntrySpec = builder.build(); - } catch (InvalidProtocolBufferException ipbe) { - throw new KeylessVerificationException("Could not parse DSSE from log entry body", ipbe); + logEntrySpec = RekorTypes.getDsseV002(rekorEntry); + } catch (RekorTypeException re) { + throw new KeylessVerificationException("Could not parse DSSE from log entry body", re); } if (!logEntrySpec.getPayloadHash().getAlgorithm().equals(HashAlgorithm.SHA2_256)) { diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorEntryFetcher.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorEntryFetcher.java index 000dcb1c..a839a4d0 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorEntryFetcher.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorEntryFetcher.java @@ -18,7 +18,9 @@ import dev.sigstore.KeylessVerificationException; import dev.sigstore.TrustedRootProvider; import dev.sigstore.encryption.certificates.Certificates; -import dev.sigstore.trustroot.*; +import dev.sigstore.trustroot.Service; +import dev.sigstore.trustroot.SigstoreConfigurationException; +import dev.sigstore.trustroot.TransparencyLog; import dev.sigstore.tuf.SigstoreTufClient; import java.io.IOException; import java.nio.file.Path; @@ -81,7 +83,7 @@ public RekorEntry getEntryFromRekor( artifactDigest, Certificates.toPemBytes(leafCert), signature); } catch (IOException e) { throw new KeylessVerificationException( - "Could not convert certificate to PEM when recreating hashrekord", e); + "Could not convert certificate to PEM when recreating hashedrekord", e); } Optional rekorEntry; 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 0f3d650e..e5735201 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 @@ -18,6 +18,10 @@ import static dev.sigstore.json.GsonSupplier.GSON; import com.google.gson.JsonParseException; +import com.google.protobuf.InvalidProtocolBufferException; +import dev.sigstore.json.ProtoJson; +import dev.sigstore.proto.rekor.v2.DSSELogEntryV002; +import dev.sigstore.proto.rekor.v2.HashedRekordLogEntryV002; import dev.sigstore.rekor.dsse.v0_0_1.Dsse; import dev.sigstore.rekor.hashedRekord.v0_0_1.HashedRekord; @@ -29,15 +33,41 @@ public class RekorTypes { * * @param entry the rekor entry obtained from rekor * @return the parsed pojo - * @throws RekorTypeException if the hashrekord:0.0.1 entry could not be parsed + * @throws RekorTypeException if the hashedrekord:0.0.1 entry could not be parsed */ - public static HashedRekord getHashedRekord(RekorEntry entry) throws RekorTypeException { + public static HashedRekord getHashedRekordV001(RekorEntry entry) throws RekorTypeException { expect(entry, "hashedrekord", "0.0.1"); try { return GSON.get().fromJson(entry.getBodyDecoded().getSpec(), HashedRekord.class); } catch (JsonParseException jpe) { - throw new RekorTypeException("Could not parse hashrekord:0.0.1", jpe); + throw new RekorTypeException("Could not parse hashedrekord:0.0.1", jpe); + } + } + + /** + * Parse a hashedrekord from rekor at api version 0.0.2. + * + * @param entry the rekor entry obtained from rekor + * @return the parsed proto + * @throws RekorTypeException if the hashedrekord:0.0.2 entry could not be parsed + */ + public static HashedRekordLogEntryV002 getHashedRekordV002(RekorEntry entry) + throws RekorTypeException { + expect(entry, "hashedrekord", "0.0.2"); + + try { + HashedRekordLogEntryV002.Builder builder = HashedRekordLogEntryV002.newBuilder(); + ProtoJson.parser() + .ignoringUnknownFields() + .merge( + GSON.get() + .toJson( + entry.getBodyDecoded().getSpec().getAsJsonObject().get("hashedRekordV002")), + builder); + return builder.build(); + } catch (InvalidProtocolBufferException | NullPointerException | IllegalStateException e) { + throw new RekorTypeException("Could not parse hashedrekord:0.0.2", e); } } @@ -48,7 +78,7 @@ public static HashedRekord getHashedRekord(RekorEntry entry) throws RekorTypeExc * @return the parsed pojo * @throws RekorTypeException if the dsse:0.0.1 entry could not be parsed */ - public static Dsse getDsse(RekorEntry entry) throws RekorTypeException { + public static Dsse getDsseV001(RekorEntry entry) throws RekorTypeException { expect(entry, "dsse", "0.0.1"); try { @@ -58,6 +88,29 @@ public static Dsse getDsse(RekorEntry entry) throws RekorTypeException { } } + /** + * Parse a dsse from rekor at api version 0.0.2. + * + * @param entry the rekor entry obtained from rekor + * @return the parsed proto + * @throws RekorTypeException if the dsse:0.0.2 entry could not be parsed + */ + public static DSSELogEntryV002 getDsseV002(RekorEntry entry) throws RekorTypeException { + expect(entry, "dsse", "0.0.2"); + + try { + DSSELogEntryV002.Builder builder = DSSELogEntryV002.newBuilder(); + ProtoJson.parser() + .ignoringUnknownFields() + .merge( + GSON.get().toJson(entry.getBodyDecoded().getSpec().getAsJsonObject().get("dsseV002")), + builder); + return builder.build(); + } catch (InvalidProtocolBufferException | NullPointerException | IllegalStateException e) { + throw new RekorTypeException("Could not parse dsse:0.0.2", e); + } + } + private static void expect(RekorEntry entry, String expectedKind, String expectedApiVersion) throws RekorTypeException { var kind = entry.getBodyDecoded().getKind(); diff --git a/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java index bf7c68c1..5206cba9 100644 --- a/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java @@ -23,6 +23,7 @@ import dev.sigstore.bundle.Bundle; import dev.sigstore.bundle.ImmutableBundle; import dev.sigstore.encryption.signers.Signers; +import dev.sigstore.rekor.client.RekorTypeException; import dev.sigstore.rekor.client.RekorVerificationException; import dev.sigstore.strings.StringMatcher; import dev.sigstore.testing.CertGenerator; @@ -271,6 +272,8 @@ public void testVerify_mismatchedCertificate_rekorV2() throws Exception { VerificationOptions.empty())); Assertions.assertEquals( "Could not parse hashedrekord from log entry body", thrown.getMessage()); + Assertions.assertTrue(thrown.getCause() instanceof RekorTypeException); + Assertions.assertEquals("Could not parse hashedrekord:0.0.2", thrown.getCause().getMessage()); } @Test diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientHttpTest.java b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientHttpTest.java index 0a336527..1479ced6 100644 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientHttpTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientHttpTest.java @@ -110,7 +110,7 @@ public void searchEntries_oneResult_publicKey() throws Exception { null, null, "x509", - RekorTypes.getHashedRekord(resp.getEntry()) + RekorTypes.getHashedRekordV001(resp.getEntry()) .getSignature() .getPublicKey() .getContent()) diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorTypesTest.java b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorTypesTest.java index 7ef22633..6d774fcf 100644 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorTypesTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorTypesTest.java @@ -23,28 +23,54 @@ public class RekorTypesTest { - private RekorEntry fromResource(String path) throws Exception { + private RekorEntry fromResourceV001(String path) throws Exception { var rekorResponse = Resources.toString(Resources.getResource(path), StandardCharsets.UTF_8); return RekorResponse.newRekorResponse(new URI("https://not.used.com"), rekorResponse) .getEntry(); } + private RekorEntry fromResourceV002(String path) throws Exception { + var tle = Resources.toString(Resources.getResource(path), StandardCharsets.UTF_8); + + return RekorEntry.fromTLogEntryJson(tle); + } + + @Test + public void getHashedRekordV001_pass() throws Exception { + var entry = fromResourceV001("dev/sigstore/samples/rekor-response/valid/entry.json"); + + var hashedRekord = RekorTypes.getHashedRekordV001(entry); + Assertions.assertNotNull(hashedRekord); + } + @Test - public void getHashedRekord_pass() throws Exception { - var entry = fromResource("dev/sigstore/samples/rekor-response/valid/entry.json"); + public void getHashedRekordV002_pass() throws Exception { + var entry = fromResourceV002("dev/sigstore/samples/rekor-response/valid/entry-v2.json"); - var hashedRekord = RekorTypes.getHashedRekord(entry); + var hashedRekord = RekorTypes.getHashedRekordV002(entry); Assertions.assertNotNull(hashedRekord); } @Test - public void getHashedRekord_badType() throws Exception { - var entry = fromResource("dev/sigstore/samples/rekor-response/valid/jar-entry.json"); + public void getHashedRekordV001_badType() throws Exception { + var entry = fromResourceV001("dev/sigstore/samples/rekor-response/valid/jar-entry.json"); var exception = - Assertions.assertThrows(RekorTypeException.class, () -> RekorTypes.getHashedRekord(entry)); + Assertions.assertThrows( + RekorTypeException.class, () -> RekorTypes.getHashedRekordV001(entry)); Assertions.assertEquals( "Expecting type hashedrekord:0.0.1, but found jar:0.0.1", exception.getMessage()); } + + @Test + public void getHashedRekordV002_badType() throws Exception { + var entry = fromResourceV002("dev/sigstore/samples/rekor-response/valid/jar-entry-v2.json"); + + var exception = + Assertions.assertThrows( + RekorTypeException.class, () -> RekorTypes.getHashedRekordV002(entry)); + Assertions.assertEquals( + "Expecting type hashedrekord:0.0.2, but found jar:0.0.2", exception.getMessage()); + } } diff --git a/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/jar-entry-v2.json b/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/jar-entry-v2.json new file mode 100644 index 00000000..8e1408ed --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/samples/rekor-response/valid/jar-entry-v2.json @@ -0,0 +1,28 @@ +{ + "logIndex": "743", + "logId": { + "keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck=" + }, + "kindVersion": { + "kind": "jar", + "version": "0.0.2" + }, + "inclusionProof": { + "logIndex": "743", + "rootHash": "esdDSd9WE37oIvN7WDlJVKtt/QajruODJO7PVEwwTXs=", + "treeSize": "744", + "hashes": [ + "yBZXhlQSPuOSafAsvOnchoFkE4MDWvNF6dUSk9D5aRA=", + "PtwLKIOKsSaNPEOf1mwqL5+x2p0eacowpueVXhsChWg=", + "MMTssW4XXsO1QXFJ9gBI8tWD03ySifDU5wkYwpz1rKE=", + "rPkfqzM2orzgdbRk88RBPZMBlUUu1QEqMkY+f1X846g=", + "+gnK+M5cyTZ0UncCImJch9APOM+yjuVvfEuX7z6AamQ=", + "QMesRTEZdIgthOEinYE/9J7wGv+VmArDZTICj9POmhY=", + "UNUMG62rMwoqCqFKknh4R5Ubkf5Z6dj+Pk0m/1xu8uo=" + ], + "checkpoint": { + "envelope": "log2025-alpha1.rekor.sigstage.dev\n744\nesdDSd9WE37oIvN7WDlJVKtt/QajruODJO7PVEwwTXs=\n\n— log2025-alpha1.rekor.sigstage.dev 8w1amdUe0s4o19zD+N8ffKDR3+mDCYIBCOX+O8gqThpWp6Rq/07hW+UpMbOdY2i6skEjvY71RebKMx2jt+Hq9JRpJAs=\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiamFyIiwic3BlYyI6eyJoYXNoZWRSZWtvcmRWMDAyIjp7ImRhdGEiOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiJMazFHb1VnZWduVHBTQ0tsd2doYUpjMDQycEFVcVdhZzRuQldhUG5SUDRNPSJ9LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQVdDTG93SC9senhLZ20xcGNybmg0emFIYmtkMHdLNHN1UnlUdTZYbDBXbEFpQXduSE5VOTJZbHJzbzB0dVdkbmYyZTNSaVY0QW95NFlHNUJxbXhvNGxBWXc9PSIsInZlcmlmaWVyIjp7ImtleURldGFpbHMiOiJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsIng1MDlDZXJ0aWZpY2F0ZSI6eyJyYXdCeXRlcyI6Ik1JSUNkRENDQWhxZ0F3SUJBZ0lHQVpkazdaZXNNQW9HQ0NxR1NNNDlCQU1DTUNveERUQUxCZ05WQkFNTUJIUmxjM1F4R1RBWEJnTlZCQW9NRUhSbGMzUWdZMlZ5ZEdsbWFXTmhkR1V3SGhjTk1qVXdOakV5TVRZeE5qSXhXaGNOTWpVd05qRXlNVFl6TmpJeFdqQXFNUTB3Q3dZRFZRUUREQVIwWlhOME1Sa3dGd1lEVlFRS0RCQjBaWE4wSUdObGNuUnBabWxqWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTg2WHQ1ZzMyRVdMVkdLcFNXV0xlQUE5bHJ2S1FTVm1RNHZIaHhxKytuMWRjMWI1UG5MZTU0TU9KTjNNQ2hYZkFUTEUwejJ5djlTck9HUjQrL01QRmlxT0NBU293Z2dFbU1CMEdBMVVkRGdRV0JCVHI1ZHJjdnNqRTg1ZW9iTFh4aFZnNUtUanRnakFmQmdOVkhTTUVHREFXZ0JRTEZIZmJqL3EzRHdvVjV5a08yeWhPZ290NkVUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3TXdEQVlEVlIwVEFRSC9CQUl3QURBYkJnTlZIUkVCQWY4RUVUQVBnUTEwWlhOMFFIUmxjM1F1WTI5dE1Dc0dDaXNHQVFRQmc3OHdBUUVFSFdoMGRIQnpPaTh2Wm1GclpXRmpZMjkxYm5SekxuUmxjM1F1WTI5dE1DMEdDaXNHQVFRQmc3OHdBUWdFSHd3ZGFIUjBjSE02THk5bVlXdGxZV05qYjNWdWRITXVkR1Z6ZEM1amIyMHdHQVlLS3dZQkJBR0dqUjhxS2dRS2RHVnpkQ0IyWVd4MVpUQWVCZ29yQmdFRUFZYU5IeW9yQkJBTURuUmxjM1FnZG1Gc2RXVWdaR1Z5TUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUV2MUN4aVhHWXJTNTBtMmRSU0x3QW9mUGx3b05KQnBjeWlXZUJrSlhYeDFBaUVBbSsvZGxUaVNXV2hvOHpOT0plcHNEMWJiNTFPUWN6S2RJQjRUeUxHRWJVQT0ifX19fX19" +} \ No newline at end of file