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 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=" + } + ] + } +}