From a1b3b20ecc23d2296ac73faaab147b0216f27918 Mon Sep 17 00:00:00 2001
From: Appu Goundan <appu@google.com>
Date: Tue, 19 Nov 2024 21:56:45 -0500
Subject: [PATCH] Add support for verifying dsse-intoto

- Verification should be able to correctly validate a bundle as
  cryptographically valid (VerificationOptions.empty())
- Verifiers may also include signer identity during verification
- Verifiers should extract the embedded attestation to do further
  analysis on the attestation. Sigstore-java does not process
  those in any way
- There is no signing options for DSSE bundles

Signed-off-by: Appu Goundan <appu@google.com>
---
 .github/workflows/conformance.yml             |   2 +-
 .../java/dev/sigstore/KeylessVerifier.java    | 202 ++++++++++++++----
 .../main/java/dev/sigstore/bundle/Bundle.java |  17 +-
 .../dev/sigstore/bundle/BundleReader.java     |   2 +-
 .../java/dev/sigstore/dsse/InTotoPayload.java |   2 +-
 .../dev/sigstore/KeylessVerifierTest.java     |  80 +++++--
 .../bundles/bundle.dsse.bad.sig.sigstore      |  59 +++++
 7 files changed, 295 insertions(+), 69 deletions(-)
 create mode 100644 sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle.dsse.bad.sig.sigstore

diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml
index c2a8c724..a2be0d3c 100644
--- a/.github/workflows/conformance.yml
+++ b/.github/workflows/conformance.yml
@@ -39,4 +39,4 @@ jobs:
         with:
           entrypoint: ${{ github.workspace }}/bin/sigstore-cli
           environment: ${{ matrix.sigstore-env }}
-          xfail: "test_verify_dsse_bundle_with_trust_root test_verify_in_toto_in_dsse_envelope"
+          xfail: "test_verify_dsse_bundle_with_trust_root"
diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
index f24c83b9..a07849cf 100644
--- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
+++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
@@ -22,14 +22,21 @@
 import dev.sigstore.VerificationOptions.CertificateMatcher;
 import dev.sigstore.VerificationOptions.UncheckedCertificateException;
 import dev.sigstore.bundle.Bundle;
+import dev.sigstore.bundle.Bundle.DsseEnvelope;
+import dev.sigstore.bundle.Bundle.MessageSignature;
+import dev.sigstore.dsse.InTotoPayload;
 import dev.sigstore.encryption.certificates.Certificates;
 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.RekorTypeException;
+import dev.sigstore.rekor.client.RekorTypes;
 import dev.sigstore.rekor.client.RekorVerificationException;
 import dev.sigstore.rekor.client.RekorVerifier;
+import dev.sigstore.rekor.dsse.v0_0_1.Dsse;
+import dev.sigstore.rekor.dsse.v0_0_1.PayloadHash;
 import dev.sigstore.tuf.SigstoreTufClient;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -49,6 +56,7 @@
 import java.util.Objects;
 import java.util.stream.Collectors;
 import org.bouncycastle.util.encoders.Base64;
+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. */
@@ -123,15 +131,14 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
       throws KeylessVerificationException {
 
     if (bundle.getDsseEnvelope().isPresent()) {
-      throw new KeylessVerificationException("Cannot verify DSSE signature based bundles");
+      checkDsseEnvelope(bundle.getDsseEnvelope().get(), artifactDigest);
+    } else if (bundle.getMessageSignature().isPresent()) {
+      checkMessageSignature(bundle.getMessageSignature().get(), artifactDigest);
+    } else {
+      throw new IllegalStateException(
+          "Bundle must contain a message signature or dsse envelope to verify");
     }
 
-    if (bundle.getMessageSignature().isEmpty()) {
-      // this should be unreachable
-      throw new IllegalStateException("Bundle must contain a message signature to verify");
-    }
-    var messageSignature = bundle.getMessageSignature().get();
-
     if (bundle.getEntries().isEmpty()) {
       throw new KeylessVerificationException("Cannot verify bundle without tlog entry");
     }
@@ -149,20 +156,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
     var signingCert = bundle.getCertPath();
     var leafCert = Certificates.getLeaf(signingCert);
 
-    // this ensures the provided artifact digest matches what may have come from a bundle (in
-    // keyless signature)
-    if (messageSignature.getMessageDigest().isPresent()) {
-      var bundleDigest = messageSignature.getMessageDigest().get().getDigest();
-      if (!Arrays.equals(artifactDigest, bundleDigest)) {
-        throw new KeylessVerificationException(
-            "Provided artifact digest does not match digest used for verification"
-                + "\nprovided(hex) : "
-                + Hex.toHexString(artifactDigest)
-                + "\nverification  : "
-                + Hex.toHexString(bundleDigest));
-      }
-    }
-
     // verify the certificate chains up to a trusted root (fulcio) and contains a valid SCT from
     // a trusted CT log
     try {
@@ -175,8 +168,6 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
     // verify the certificate identity if options are present
     checkCertificateMatchers(leafCert, options.getCertificateMatchers());
 
-    var signature = messageSignature.getSignature();
-
     RekorEntry rekorEntry = bundle.getEntries().get(0);
 
     // verify the rekor entry is signed by the log keys
@@ -186,23 +177,6 @@ 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());
@@ -214,12 +188,31 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
       throw new KeylessVerificationException("Signing time was after certificate expiry", e);
     }
 
-    // finally check the supplied signature can be verified by the public key in the certificate
     var publicKey = leafCert.getPublicKey();
     try {
       var verifier = Verifiers.newVerifier(publicKey);
-      if (!verifier.verifyDigest(artifactDigest, signature)) {
-        throw new KeylessVerificationException("Artifact signature was not valid");
+      if (bundle.getMessageSignature().isPresent()) {
+        // verify the signature over the artifact
+        var signature = bundle.getMessageSignature().get().getSignature();
+        if (!verifier.verifyDigest(artifactDigest, signature)) {
+          throw new KeylessVerificationException("Artifact signature was not valid");
+        }
+        // verify the entry is relevant to the provided verification materials
+        checkHashedRekord(rekorEntry, artifactDigest, leafCert, signature);
+      } else {
+        // verify the rekor entry is signed by the log keys and is relevant to this envelope
+        try {
+          rekorVerifier.verifyEntry(rekorEntry);
+        } catch (RekorVerificationException ex) {
+          throw new KeylessVerificationException("Rekor entry was not valid", ex);
+        }
+        if (!verifier.verify(
+            bundle.getDsseEnvelope().get().getPAE(),
+            bundle.getDsseEnvelope().get().getSignature())) {
+          throw new KeylessVerificationException("DSSE signature was not valid");
+        }
+        // verify the entry is relevant to the provided verification materials
+        checkRekorDsse(rekorEntry, bundle.getDsseEnvelope().get());
       }
     } catch (NoSuchAlgorithmException | InvalidKeyException ex) {
       throw new RuntimeException(ex);
@@ -244,4 +237,127 @@ void checkCertificateMatchers(X509Certificate cert, List<CertificateMatcher> mat
           "Could not verify certificate identities: " + ce.getMessage());
     }
   }
+
+  // this ensures the provided artifact digest matches what may have come from a bundle (in
+  // keyless signature)
+  void checkMessageSignature(MessageSignature messageSignature, byte[] artifactDigest)
+      throws KeylessVerificationException {
+    if (messageSignature.getMessageDigest().isPresent()) {
+      var bundleDigest = messageSignature.getMessageDigest().get().getDigest();
+      if (!Arrays.equals(artifactDigest, bundleDigest)) {
+        throw new KeylessVerificationException(
+            "Provided artifact digest does not match digest used for verification"
+                + "\nprovided(hex) : "
+                + Hex.toHexString(artifactDigest)
+                + "\nverification  : "
+                + Hex.toHexString(bundleDigest));
+      }
+    }
+  }
+
+  // recreate the log entry and check if it matches what was provided in the rekorEntry
+  void checkHashedRekord(
+      RekorEntry rekorEntry, byte[] artifactDigest, X509Certificate leafCert, byte[] signature)
+      throws KeylessVerificationException {
+    try {
+      RekorTypes.getHashedRekord(rekorEntry);
+      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);
+    } catch (RekorTypeException re) {
+      throw new KeylessVerificationException("Unexpected rekor type", re);
+    }
+  }
+
+  // since we don't check dsse signatures over the artifact, we must verify the artifact is in
+  // the subject list of the envelope
+  void checkDsseEnvelope(DsseEnvelope dsseEnvelope, byte[] artifactDigest)
+      throws KeylessVerificationException {
+    if (!Objects.equals(InTotoPayload.PAYLOAD_TYPE, dsseEnvelope.getPayloadType())) {
+      throw new KeylessVerificationException(
+          "DSSE envelope must have payload type "
+              + InTotoPayload.PAYLOAD_TYPE
+              + ", but found '"
+              + dsseEnvelope.getPayloadType()
+              + "'");
+    }
+    if (dsseEnvelope.getSignatures().size() != 1) {
+      throw new KeylessVerificationException(
+          "DSSE envelope must have exactly 1 signature, but found: "
+              + dsseEnvelope.getSignatures().size());
+    }
+    // find one sha256 hash in the subject list that matches the artifact hash
+    InTotoPayload payload = InTotoPayload.from(dsseEnvelope);
+    if (payload.getSubject().stream()
+        .noneMatch(
+            subject -> {
+              if (subject.getDigest().containsKey("sha256")) {
+                try {
+                  var digestBytes = Hex.decode(subject.getDigest().get("sha256"));
+                  return Arrays.equals(artifactDigest, digestBytes);
+                } catch (DecoderException de) {
+                  // ignore and return false
+                }
+              }
+              return false;
+            })) {
+      var providedHashes =
+          payload.getSubject().stream()
+              .map(s -> s.getDigest().getOrDefault("sha256", "no-sha256-hash"))
+              .collect(Collectors.joining(",", "[", "]"));
+
+      throw new KeylessVerificationException(
+          "Provided artifact digest does not match any subject sha256 digests in DSSE payload"
+              + "\nprovided(hex) : "
+              + Hex.toHexString(artifactDigest)
+              + "\nverification  : "
+              + providedHashes);
+    }
+  }
+
+  // check if the digest over the dsse payload matches the digest in the rekorEntry
+  public void checkRekorDsse(RekorEntry rekorEntry, DsseEnvelope dsseEnvelope)
+      throws KeylessVerificationException {
+
+    Dsse dsse;
+    try {
+      dsse = RekorTypes.getDsse(rekorEntry);
+    } catch (RekorTypeException re) {
+      throw new KeylessVerificationException("Unexpected rekor type", re);
+    }
+
+    var algorithm = dsse.getPayloadHash().getAlgorithm();
+    if (algorithm != PayloadHash.Algorithm.SHA_256) {
+      throw new KeylessVerificationException(
+          "Cannot process dsse entry with hashing algorithm " + algorithm.toString());
+    }
+
+    byte[] payloadDigest;
+    try {
+      payloadDigest = Hex.decode(dsse.getPayloadHash().getValue());
+    } catch (DecoderException de) {
+      throw new KeylessVerificationException(
+          "Could not decode hex sha256 artifact hash in hashrekord", de);
+    }
+
+    byte[] calculatedDigest = Hashing.sha256().hashBytes(dsseEnvelope.getPayload()).asBytes();
+    if (!Arrays.equals(calculatedDigest, payloadDigest)) {
+      throw new KeylessVerificationException(
+          "Digest of dsse payload in bundle does not match dsse payload digest in log entry"
+              + "\nbundle(hex) : "
+              + Hex.toHexString(calculatedDigest)
+              + "\nlog(hex) : "
+              + Hex.toHexString(payloadDigest));
+    }
+  }
 }
diff --git a/sigstore-java/src/main/java/dev/sigstore/bundle/Bundle.java b/sigstore-java/src/main/java/dev/sigstore/bundle/Bundle.java
index de9a7e04..5516265b 100644
--- a/sigstore-java/src/main/java/dev/sigstore/bundle/Bundle.java
+++ b/sigstore-java/src/main/java/dev/sigstore/bundle/Bundle.java
@@ -138,7 +138,7 @@ public interface MessageDigest {
   public interface DsseEnvelope {
 
     /** An arbitrary payload that does not need to be parsed to be validated */
-    String getPayload();
+    byte[] getPayload();
 
     /** Information on how to interpret the payload */
     String getPayloadType();
@@ -158,12 +158,18 @@ default byte[] getPAE() {
               + " "
               + getPayloadType()
               + " "
-              + getPayload().length()
+              + getPayloadAsString().length()
               + " "
-              + getPayload())
+              + getPayloadAsString())
           .getBytes(StandardCharsets.UTF_8);
     }
 
+    @Lazy
+    @Gson.Ignore
+    default String getPayloadAsString() {
+      return new String(getPayload(), StandardCharsets.UTF_8);
+    }
+
     @Lazy
     @Gson.Ignore
     default byte[] getSignature() {
@@ -197,4 +203,9 @@ public static Bundle from(Path file, Charset cs) throws BundleParseException, IO
   public String toJson() {
     return BundleWriter.writeBundle(this);
   }
+
+  /** Check if this bundle has MessageSignature, use to determine what verification method to use */
+  public static boolean hasMessageSignature(Bundle bundle) {
+    return bundle.getMessageSignature().isPresent();
+  }
 }
diff --git a/sigstore-java/src/main/java/dev/sigstore/bundle/BundleReader.java b/sigstore-java/src/main/java/dev/sigstore/bundle/BundleReader.java
index 4f71f864..73ef6f6a 100644
--- a/sigstore-java/src/main/java/dev/sigstore/bundle/BundleReader.java
+++ b/sigstore-java/src/main/java/dev/sigstore/bundle/BundleReader.java
@@ -103,7 +103,7 @@ static Bundle readBundle(Reader jsonReader) throws BundleParseException {
       var dsseEnvelopeProto = protoBundle.getDsseEnvelope();
       var dsseEnvelopeBuilder =
           ImmutableDsseEnvelope.builder()
-              .payload(dsseEnvelopeProto.getPayload().toStringUtf8())
+              .payload(dsseEnvelopeProto.getPayload().toByteArray())
               .payloadType(dsseEnvelopeProto.getPayloadType());
       for (int sigIndex = 0; sigIndex < dsseEnvelopeProto.getSignaturesCount(); sigIndex++) {
         dsseEnvelopeBuilder.addSignatures(
diff --git a/sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java b/sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java
index 8e381ae4..79d309e5 100644
--- a/sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java
+++ b/sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java
@@ -52,6 +52,6 @@ interface Subject {
   }
 
   static InTotoPayload from(DsseEnvelope dsseEnvelope) {
-    return GSON.get().fromJson(dsseEnvelope.getPayload(), InTotoPayload.class);
+    return GSON.get().fromJson(dsseEnvelope.getPayloadAsString(), InTotoPayload.class);
   }
 }
diff --git a/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java
index bdee62b6..8dec6c02 100644
--- a/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java
+++ b/sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java
@@ -16,6 +16,7 @@
 package dev.sigstore;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Hashing;
 import com.google.common.io.Resources;
 import dev.sigstore.VerificationOptions.CertificateMatcher;
 import dev.sigstore.bundle.Bundle;
@@ -27,6 +28,8 @@
 import java.nio.file.Path;
 import java.security.cert.X509Certificate;
 import java.util.List;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -105,26 +108,6 @@ public void testVerify_badCheckpointSignature() throws Exception {
                 VerificationOptions.empty()));
   }
 
-  @Test
-  public void testVerify_errorsOnDSSEBundle() throws Exception {
-    var bundleFile =
-        Resources.toString(
-            Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
-            StandardCharsets.UTF_8);
-    var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
-
-    var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
-    var ex =
-        Assertions.assertThrows(
-            KeylessVerificationException.class,
-            () ->
-                verifier.verify(
-                    Path.of(artifact),
-                    Bundle.from(new StringReader(bundleFile)),
-                    VerificationOptions.empty()));
-    Assertions.assertEquals("Cannot verify DSSE signature based bundles", ex.getMessage());
-  }
-
   @Test
   public void testVerify_canVerifyV01Bundle() throws Exception {
     // note that this v1 bundle contains an inclusion proof
@@ -231,4 +214,61 @@ public void verifyCertificateMatches_noneMatch() throws Exception {
         "No provided certificate identities matched values in certificate: [{issuer:'String: not-match',san:'String: not-match'},{issuer:'String: not-match-again',san:'String: not-match-again'}]",
         ex.getMessage());
   }
+
+  @Test
+  public void testVerify_dsseBundle() throws Exception {
+    var bundleFile =
+        Resources.toString(
+            Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.sigstore"),
+            StandardCharsets.UTF_8);
+    var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
+
+    var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
+    verifier.verify(
+        Path.of(artifact), Bundle.from(new StringReader(bundleFile)), VerificationOptions.empty());
+  }
+
+  @Test
+  public void testVerify_dsseBundleBadSignature() throws Exception {
+    var bundleFile =
+        Resources.toString(
+            Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.bad.sig.sigstore"),
+            StandardCharsets.UTF_8);
+    var artifact = Resources.getResource("dev/sigstore/samples/bundles/artifact.txt").getPath();
+    var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
+
+    var ex =
+        Assertions.assertThrows(
+            KeylessVerificationException.class,
+            () ->
+                verifier.verify(
+                    Path.of(artifact),
+                    Bundle.from(new StringReader(bundleFile)),
+                    VerificationOptions.empty()));
+    Assertions.assertEquals("DSSE signature was not valid", ex.getMessage());
+  }
+
+  @Test
+  public void testVerify_dsseBundleArtifactNotInSubjects() throws Exception {
+    var bundleFile =
+        Resources.toString(
+            Resources.getResource("dev/sigstore/samples/bundles/bundle.dsse.bad.sig.sigstore"),
+            StandardCharsets.UTF_8);
+    var badArtifactDigest =
+        Hashing.sha256().hashString("nonsense", StandardCharsets.UTF_8).asBytes();
+    var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
+
+    var ex =
+        Assertions.assertThrows(
+            KeylessVerificationException.class,
+            () ->
+                verifier.verify(
+                    badArtifactDigest,
+                    Bundle.from(new StringReader(bundleFile)),
+                    VerificationOptions.empty()));
+    MatcherAssert.assertThat(
+        ex.getMessage(),
+        CoreMatchers.startsWith(
+            "Provided artifact digest does not match any subject sha256 digests in DSSE payload"));
+  }
 }
diff --git a/sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle.dsse.bad.sig.sigstore b/sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle.dsse.bad.sig.sigstore
new file mode 100644
index 00000000..52d28cda
--- /dev/null
+++ b/sigstore-java/src/test/resources/dev/sigstore/samples/bundles/bundle.dsse.bad.sig.sigstore
@@ -0,0 +1,59 @@
+{
+  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
+  "verificationMaterial": {
+    "tlogEntries": [
+      {
+        "logIndex": "150322684",
+        "logId": {
+          "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
+        },
+        "kindVersion": {
+          "kind": "dsse",
+          "version": "0.0.1"
+        },
+        "integratedTime": "1732135425",
+        "inclusionPromise": {
+          "signedEntryTimestamp": "MEUCIF/Y+XbEXKAblX/ohWp+wXIff65mYTzEUf+p557ocUEZAiEAjSHzJCkwJPP+8YF6bvmpuEl+sXb84RL1wf9zVnfLns4="
+        },
+        "inclusionProof": {
+          "logIndex": "28418422",
+          "rootHash": "+QIOalcm4FuDhJj9qII/2u9Nypdzjk0c9NK7hGTgac8=",
+          "treeSize": "28418423",
+          "hashes": [
+            "wB0Bax3k2EbrDhHe/Am7xWtmsX0kV75PD38gAOQ6V/4=",
+            "ljcbev25ePyz7Ns8nCGnARFqCQ9gEy0J6nZLllEjx5w=",
+            "gSbCTgtyNMJHo+eX5BrdCm2lViZxYwdu3F0QVBhcxj8=",
+            "GdXev4gNvvFgWH0cLpbpKAYhfflAN2k4JyWFw9O5hts=",
+            "QamBghNmsUsGreZ9zxBhz7ynJzdS8Wt34XSWduDhJe4=",
+            "QmKt9GDPnFWd2pjpwiF9anMwu0zKnqqd9uSsS+Ghm0E=",
+            "zGmHsTSEnYk656ZFm3nFkDh/8cEiIYAqh1zD7l6Wl5M=",
+            "ggdeKtYR4Qf6kMEDhKtGxCnbgcZzb3YtF3fczGIjWCI=",
+            "iF6rmo01zrn23pMgcPKlXOufqui4F8Q1+hj8PHL6XuY=",
+            "bulsENariUUsC4xiR1yFtqKzD8evI9p/s+YCpl8t9tE=",
+            "E2rLOYPJFKiizYiyu07QLqkMVTVL7i2ZgXiQywdI9KQ=",
+            "4lUF0YOu9XkIDXKXA0wMSzd6VeDY3TZAgmoOeWmS2+Y=",
+            "gf+9m552B3PnkWnO0o4KdVvjcT3WVHLrCbf1DoVYKFw="
+          ],
+          "checkpoint": {
+            "envelope": "rekor.sigstore.dev - 1193050959916656506\n28418423\n+QIOalcm4FuDhJj9qII/2u9Nypdzjk0c9NK7hGTgac8=\n\n— rekor.sigstore.dev wNI9ajBGAiEA5TnJBQi/DYgn5WvcQvZi0q5tlOF/h3sxLW2nztOtgfwCIQCrliHf+bgbEvlXQNw3XserTuIeSYrG6aMo8SHVOR7Pmg==\n"
+          }
+        },
+        "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNzhiZmM3ZDNhNGVjOThiZjJlZjMyYmFmOTEyOGU0MTg5ZmZlY2JkOTIzYWIwMjQ2YWU1Y2NiNDE1Y2FmNmY0MSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImQ2YjIwOWJhOWRkZTNiMWVlNWVkZWJiODhkMWIxYjkxN2I3MmVlYjYxNDMyOGU3YmE2NjdmNWI3YWI3MGU5ODcifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRRFZHcFZMc3ZFWTNscHM2MmNMR3ptemdPVlJjZ2d5U0xwQVJObDlaRWp6clFJZ0pZVmlEa3U3RTBKZ1lJMnRPRG1NaXFhNVh6OWVFTHBUbHB3L1JwSmRrV009IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VjMlZFTkRRbTVEWjBGM1NVSkJaMGxWUlZONk5XWmlWWEY0Y3pKV2EwaG5NbTEyV1ZNNFJuRk9WM1J6ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmVFMVVTWGROYWtFd1RYcFJNVmRvWTA1TmFsRjRUVlJKZDAxcVFURk5lbEV4VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVkhiRFJzZFVOWVJWVXJOMFJXYVVWaWRuSjNSM3BKUTJONVIyRlRXQ3QyYzBseUwzVUthV1ZZZGxwRmVteG5XWGRoUzJkMFZtWTNZV3RFZUUxQ1ZqQTBNamxOZDJGbGVHVmtUM0paYURoeE5uWkxkRFUwY1hGUFEwSlpPSGRuWjFkTVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVlNXVVpKQ201clZYZENiMVpCYTBsWVUwOHljRmRLZEU5VE0wTjNkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMXAzV1VSV1VqQlNRVkZJTDBKR01IZFhORnBhWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREo0ZG1JelRteFpiVVkyWWpJNWNncFpVemxvV1ZNeE1GcFlUakJNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIcE1NMEo1WWpOYWJHSnRSblZaTWxWMVpWZEdkR0pGUW5sYVYxcDZDa3d5YUd4WlYxSjZUREl4YUdGWE5IZFBVVmxMUzNkWlFrSkJSMFIyZWtGQ1FWRlJjbUZJVWpCalNFMDJUSGs1TUdJeWRHeGlhVFZvV1ROU2NHSXlOWG9LVEcxa2NHUkhhREZaYmxaNldsaEthbUl5TlRCYVZ6VXdURzFPZG1KVVFXWkNaMjl5UW1kRlJVRlpUeTlOUVVWRFFrSkdNMkl6U25KYWJYaDJaREU1YXdwaFdFNTNXVmhTYW1GRVFUSkNaMjl5UW1kRlJVRlpUeTlOUVVWRVFrTm9iRmx0V20xUFIxSnRXVzFSTWsxRWJHbE9Na2w1VFdwSmVrNHlUVE5PZWtVMUNsa3lWWGRPTWxsNVdrZE5NMDlVVFRCYWFsWnRUVU5GUjBOcGMwZEJVVkZDWnpjNGQwRlJVVVZGTUdSc1ltMVdlVmxZVW14SlJrSjVZak5hYkdKdFJuVUtXVEpWZDBsbldVdExkMWxDUWtGSFJIWjZRVUpDVVZGVllrYzVkbU15Vm1sWldIQjJZakowYUV3eVJtaE1XRkpzWXpOUmQwaFJXVXRMZDFsQ1FrRkhSQXAyZWtGQ1FtZFJVR050Vm0xamVUbHZXbGRHYTJONU9YUlpWMngxVFVSelIwTnBjMGRCVVZGQ1p6YzRkMEZSWjBWTVVYZHlZVWhTTUdOSVRUWk1lVGt3Q21JeWRHeGlhVFZvV1ROU2NHSXlOWHBNYldSd1pFZG9NVmx1Vm5wYVdFcHFZakkxTUZwWE5UQk1iVTUyWWxSQ2NFSm5iM0pDWjBWRlFWbFBMMDFCUlVvS1FrWnpUVmRYYURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTWpsNldsZEthR1Z0T1haaE1rVjJXVmRGZEdSSFZucGtRemgxV2pKc01BcGhTRlpwVEROa2RtTnRkRzFpUnprelkzazVkMk50T1RKYVZ6Vm9ZbTFPYkV4dWJHaGlWM2hCWTIxV2JXTjVPVzlhVjBaclkzazVkRmxYYkhWTlJHZEhDa05wYzBkQlVWRkNaemM0ZDBGUmIwVkxaM2R2V2xkS2JWcHFhR3RhYlVwclRtcEJOVmxxWkdsTmFrbDVUWHBrYWs1NlkzaFBWMDVzVFVSa2JVMXRVbW9LVG5wcmVrNUhXVEZhYWtGa1FtZHZja0puUlVWQldVOHZUVUZGVEVKQk9FMUVWMlJ3WkVkb01WbHBNVzlpTTA0d1dsZFJkMDUzV1V0TGQxbENRa0ZIUkFwMmVrRkNSRUZSY0VSRFpHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaaVJ6bDJZekpXYVZsWWNIWmlNblJvVERKR2FFeFlVbXhqTTFGM0NrOUJXVXRMZDFsQ1FrRkhSSFo2UVVKRVVWRnhSRU5vYkZsdFdtMVBSMUp0V1cxUk1rMUViR2xPTWtsNVRXcEplazR5VFROT2VrVTFXVEpWZDA0eVdYa0tXa2ROTTA5VVRUQmFhbFp0VFVJNFIwTnBjMGRCVVZGQ1p6YzRkMEZSTkVWRlVYZFFZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUW10SFEybHpSd3BCVVZGQ1p6YzRkMEZST0VWRGQzZEtUMFJyZUU1NlJURk9SRkV3VFVNNFIwTnBjMGRCVVZGQ1p6YzRkMEZTUVVWSlVYZG1ZVWhTTUdOSVRUWk1lVGx1Q21GWVVtOWtWMGwxV1RJNWRFd3llSFppTTA1c1dXMUdObUl5T1hKWlZFRllRbWR2Y2tKblJVVkJXVTh2VFVGRlVrSkJhMDFDZWtWNlRVUlJORTFxV1hjS1lWRlpTMHQzV1VKQ1FVZEVkbnBCUWtWblVtSkVSbXh2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2M1ZG1NeVZtbFpXSEIyWWpKMGFBcE1Na1pvVEZoU2JHTXpVWFpNYldSd1pFZG9NVmxwT1ROaU0wcHlXbTE0ZG1RelRYWmpTRXAyWkcxV2RWbFhOV3BhVXpVMVdWY3hjMUZJU214YWJrMTJDbUZIVm1oYVNFMTJZbGRHY0dKcVFUUkNaMjl5UW1kRlJVRlpUeTlOUVVWVVFrTnZUVXRIVm1sYWJWazBXa2RhYVZwRVdYZFBWMGt6V1dwSmVVMXFUVE1LV1hwak0wMVViR3BhVkVFeldtcEthMWw2WXpWTmVsSnRUbGRaZDBsUldVdExkMWxDUWtGSFJIWjZRVUpHUVZGVVJFSkdNMkl6U25KYWJYaDJaREU1YXdwaFdFNTNXVmhTYW1GRVFtSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVXdUVk15YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTWpsNkNscFhTbWhsYlRsMllUSkZkbGxYUlhSa1IxWjZaRU01YUZrelVuQmlNalY2VEROS01XSnVUWFpOVkVVMVRrUkZNRTFxVlRCUFJHTjJXVmhTTUZwWE1YY0taRWhOZGsxVVFWZENaMjl5UW1kRlJVRlpUeTlOUVVWWFFrRm5UVUp1UWpGWmJYaHdXWHBEUW1sUldVdExkMWxDUWtGSVYyVlJTVVZCWjFJM1FraHJRUXBrZDBJeFFVNHdPVTFIY2tkNGVFVjVXWGhyWlVoS2JHNU9kMHRwVTJ3Mk5ETnFlWFF2TkdWTFkyOUJka3RsTms5QlFVRkNhekIwVWtaU1kwRkJRVkZFQ2tGRldYZFNRVWxuUlhobE1tdFdlSFExWkdGRVdVUmlZMWh4WjBnemJrTkhkVUl3UVhGVVozVmFTbXhaUTBGMlFVUnRiME5KUTIxT2RuSmtWRXBPY0VZS2JqVnJTbWRyYUdsVVIzWlNNSGxMTWxreFExb3lLelYzWlhNd1pVSmtjMWxOUVc5SFEwTnhSMU5OTkRsQ1FVMUVRVEpqUVUxSFVVTk5Selp5V0d4RlNRcGhTazkzWlhST1RITlVZVVJMTjBod01ITlZkVUoxYTBaVFMwZ3dVbTFQSzA1RU1HRm9jV3hTVmxkYU9ERkJXbUYxVVhjelFreGtSbFpuU1hkVGRESTBDbWswYjBKSWVFVXpNVTlOU1d4M1JVTTVWakJUUzA1SlRGSmljVVZvVG1KdmIyUkpjWHB1TW1kMVFYVTRUbkpWVkhGbGRtdFRTV0pOZHpNdmFnb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifV19fQ=="
+      }
+    ],
+    "timestampVerificationData": {
+    },
+    "certificate": {
+      "rawBytes": "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"
+    }
+  },
+  "dsseEnvelope": {
+    "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL2xvb3NlYmF6b29rYS9hYS10ZXN0IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL3Byb3ZlbmFuY2UueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODkxNzE1NDQ0IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6IjEzMDQ4MjYiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9sb29zZWJhem9va2EvYWEtdGVzdEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiZWJmZjhkZmJkNjA5YjdiMjIyMzdjNzcxOWNlMDdmMmRjNzkzNGY1ZiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvYWN0aW9ucy9ydW5zLzExOTQxNDI1NDg3L2F0dGVtcHRzLzEifX19fQ==",
+    "payloadType": "application/vnd.in-toto+json",
+    "signatures": [
+      {
+        "sig": "MEUCIQDVGpVLsvEY3lps62cLGzmzgOVRcggySLpARNl9ZEjzcQIgJYViDku7E0JgYI2tODmMiqa5Xz9eELpTlpw/RpJdkWM="
+      }
+    ]
+  }
+}