From ef412b2ba1958c9c6ead5d280d9179eab5e2fff1 Mon Sep 17 00:00:00 2001 From: Aaron Lew <64337293+aaronlew02@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:30:34 -0400 Subject: [PATCH] Add support for creating and verifying DSSE attestations Signed-off-by: Aaron Lew <64337293+aaronlew02@users.noreply.github.com> --- .../main/java/dev/sigstore/KeylessSigner.java | 167 ++++++++++++++++++ .../dev/sigstore/bundle/BundleWriter.java | 60 +++++-- .../java/dev/sigstore/dsse/InTotoPayload.java | 6 +- .../rekor/v2/client/RekorV2Client.java | 9 + .../rekor/v2/client/RekorV2ClientHttp.java | 19 +- .../java/dev/sigstore/KeylessSignerTest.java | 14 ++ .../test/java/dev/sigstore/KeylessTest.java | 29 +++ .../v2/client/RekorV2ClientHttpTest.java | 64 +++++++ 8 files changed, 343 insertions(+), 25 deletions(-) diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java index 42398efa..363ce71f 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java @@ -22,11 +22,15 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.concurrent.GuardedBy; +import com.google.gson.JsonSyntaxException; import com.google.protobuf.ByteString; import dev.sigstore.bundle.Bundle; import dev.sigstore.bundle.Bundle.MessageSignature; import dev.sigstore.bundle.ImmutableBundle; +import dev.sigstore.bundle.ImmutableDsseEnvelope; +import dev.sigstore.bundle.ImmutableSignature; import dev.sigstore.bundle.ImmutableTimestamp; +import dev.sigstore.dsse.InTotoPayload; import dev.sigstore.encryption.certificates.Certificates; import dev.sigstore.encryption.signers.Signer; import dev.sigstore.encryption.signers.Signers; @@ -42,6 +46,7 @@ import dev.sigstore.oidc.client.OidcTokenMatcher; import dev.sigstore.proto.ProtoMutators; import dev.sigstore.proto.common.v1.X509Certificate; +import dev.sigstore.proto.rekor.v2.DSSERequestV002; import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002; import dev.sigstore.proto.rekor.v2.Signature; import dev.sigstore.proto.rekor.v2.Verifier; @@ -65,6 +70,7 @@ import dev.sigstore.trustroot.Service; import dev.sigstore.trustroot.SigstoreConfigurationException; import dev.sigstore.tuf.SigstoreTufClient; +import io.intoto.EnvelopeOuterClass; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -102,6 +108,8 @@ public class KeylessSigner implements AutoCloseable { */ public static final Duration DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME = Duration.ofMinutes(5); + public static final String DEFAULT_INTOTO_PAYLOAD_TYPE = "https://in-toto.io/Statement/v1"; + private final FulcioClient fulcioClient; private final FulcioVerifier fulcioVerifier; private final RekorClient rekorClient; @@ -671,4 +679,163 @@ public Map signFiles(List artifacts) throws KeylessSignerExc public Bundle signFile(Path artifact) throws KeylessSignerException { return signFiles(List.of(artifact)).get(artifact); } + + public Bundle attest(String payload) throws KeylessSignerException { + if (rekorV2Client != null) { // Using Rekor v2 and a TSA + Preconditions.checkNotNull( + timestampClient, "Timestamp client must be configured for Rekor v2"); + Preconditions.checkNotNull( + timestampVerifier, "Timestamp verifier must be configured for Rekor v2"); + } else { + throw new IllegalStateException("No rekor v2 client was configured."); + } + + if (payload == null || payload.isEmpty()) { + throw new IllegalArgumentException("Payload must be non-empty"); + } + + InTotoPayload inTotoPayload; + try { + inTotoPayload = InTotoPayload.from(payload); + } catch (JsonSyntaxException jse) { + throw new IllegalArgumentException("Payload is not a valid in-toto statement"); + } + + if (!inTotoPayload.getType().equals(DEFAULT_INTOTO_PAYLOAD_TYPE)) { + throw new IllegalArgumentException( + "Payload must be of type \"" + + DEFAULT_INTOTO_PAYLOAD_TYPE + + "\" but was \"" + + inTotoPayload.getType() + + "\""); + } + + if (inTotoPayload.getSubject() == null || inTotoPayload.getSubject().isEmpty()) { + throw new IllegalArgumentException("Payload must contain at least one subject"); + } + + for (var subject : inTotoPayload.getSubject()) { + if (subject.getName() != null && !subject.getName().isEmpty()) { + continue; + } + throw new IllegalArgumentException("Payload must contain at least one non-empty subject"); + } + + // Technically speaking, it is unlikely the certificate will expire between signing artifacts + // However, files might be large, and it might take time to talk to Rekor + // so we check the certificate expiration here. + try { + renewSigningCertificate(); + } catch (FulcioVerificationException + | UnsupportedAlgorithmException + | OidcException + | IOException + | InterruptedException + | InvalidKeyException + | NoSuchAlgorithmException + | SignatureException + | CertificateException ex) { + throw new KeylessSignerException("Failed to obtain signing certificate", ex); + } + + CertPath signingCert; + byte[] encodedCert; + lock.readLock().lock(); + try { + signingCert = this.signingCert; + encodedCert = this.encodedCert; + if (signingCert == null) { + throw new IllegalStateException("Signing certificate is null"); + } + } finally { + lock.readLock().unlock(); + } + + var bundleBuilder = ImmutableBundle.builder().certPath(signingCert); + + var dsse = + ImmutableDsseEnvelope.builder() + .payload(payload.getBytes(StandardCharsets.UTF_8)) + .payloadType("application/vnd.in-toto+json") + .build(); + + var pae = dsse.getPAE(); + + Bundle.DsseEnvelope dsseSigned; + try { + var sig = signer.sign(pae); + dsseSigned = + ImmutableDsseEnvelope.builder() + .from(dsse) + .addSignatures(ImmutableSignature.builder().sig(sig).build()) + .build(); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException ex) { + throw new KeylessSignerException("Failed to sign artifact", ex); + } + + var verifier = + Verifier.newBuilder() + .setX509Certificate( + X509Certificate.newBuilder().setRawBytes(ByteString.copyFrom(encodedCert)).build()) + .setKeyDetails(ProtoMutators.toPublicKeyDetails(signingAlgorithm)) + .build(); + + var dsseRequest = + DSSERequestV002.newBuilder() + .setEnvelope( + EnvelopeOuterClass.Envelope.newBuilder() + .setPayload(ByteString.copyFrom(dsseSigned.getPayload())) + .setPayloadType(dsseSigned.getPayloadType()) + .addSignatures( + EnvelopeOuterClass.Signature.newBuilder() + .setSig(ByteString.copyFrom(dsseSigned.getSignature()))) + .build()) + .addVerifiers(verifier) + .build(); + + var signatureDigest = Hashing.sha256().hashBytes(dsseSigned.getSignature()).asBytes(); + + var tsReq = + ImmutableTimestampRequest.builder() + .hashAlgorithm(dev.sigstore.timestamp.client.HashAlgorithm.SHA256) + .hash(signatureDigest) + .build(); + + TimestampResponse tsResp; + try { + tsResp = timestampClient.timestamp(tsReq); + } catch (TimestampException ex) { + throw new KeylessSignerException("Failed to generate timestamp", ex); + } + + try { + timestampVerifier.verify(tsResp, dsseSigned.getSignature()); + } catch (TimestampVerificationException ex) { + throw new KeylessSignerException("Returned timestamp was invalid", ex); + } + + Bundle.Timestamp timestamp = + ImmutableTimestamp.builder().rfc3161Timestamp(tsResp.getEncoded()).build(); + + bundleBuilder.addTimestamps(timestamp); + + RekorEntry entry; + try { + entry = rekorV2Client.putEntry(dsseRequest); + } catch (IOException | RekorParseException ex) { + throw new KeylessSignerException("Failed to put entry in rekor", ex); + } + + try { + rekorVerifier.verifyEntry(entry); + } catch (RekorVerificationException ex) { + throw new KeylessSignerException("Failed to validate rekor entry after signing", ex); + } + + bundleBuilder.dsseEnvelope(dsseSigned); + + bundleBuilder.addEntries(entry); + + return bundleBuilder.build(); + } } diff --git a/sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java b/sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java index 72d7c738..b163f705 100644 --- a/sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java +++ b/sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java @@ -33,6 +33,7 @@ import dev.sigstore.proto.rekor.v1.KindVersion; import dev.sigstore.proto.rekor.v1.TransparencyLogEntry; import dev.sigstore.rekor.client.RekorEntry; +import io.intoto.EnvelopeOuterClass.Envelope; import java.security.cert.CertificateEncodingException; import java.util.Base64; import java.util.List; @@ -78,28 +79,51 @@ static String writeBundle(Bundle signingResult) { * @return Sigstore Bundle in protobuf builder format */ static dev.sigstore.proto.bundle.v1.Bundle.Builder createBundleBuilder(Bundle bundle) { - if (bundle.getMessageSignature().isEmpty()) { - throw new IllegalStateException("can only serialize bundles with message signatures"); + if (bundle.getMessageSignature().isEmpty() && bundle.getDsseEnvelope().isEmpty()) { + throw new IllegalStateException("Either message signature or DSSE envelope must be present"); } - var messageSignature = bundle.getMessageSignature().get(); - if (messageSignature.getMessageDigest().isEmpty()) { + if (bundle.getMessageSignature().isPresent() && bundle.getDsseEnvelope().isPresent()) { + throw new IllegalStateException( + "Only one of message signature or DSSE envelope must be present"); + } + if (bundle.getMessageSignature().isPresent() + && bundle.getMessageSignature().get().getMessageDigest().isEmpty()) { throw new IllegalStateException( "keyless signature must have artifact digest when serializing to bundle"); } - return dev.sigstore.proto.bundle.v1.Bundle.newBuilder() - .setMediaType(bundle.getMediaType()) - .setVerificationMaterial(buildVerificationMaterial(bundle)) - .setMessageSignature( - MessageSignature.newBuilder() - .setMessageDigest( - HashOutput.newBuilder() - .setAlgorithm( - ProtoMutators.toProtoHashAlgorithm( - messageSignature.getMessageDigest().get().getHashAlgorithm())) - .setDigest( - ByteString.copyFrom( - messageSignature.getMessageDigest().get().getDigest()))) - .setSignature(ByteString.copyFrom(messageSignature.getSignature()))); + dev.sigstore.proto.bundle.v1.Bundle.Builder builder = + dev.sigstore.proto.bundle.v1.Bundle.newBuilder() + .setMediaType(bundle.getMediaType()) + .setVerificationMaterial(buildVerificationMaterial(bundle)); + if (bundle.getMessageSignature().isPresent()) { + var messageSignature = bundle.getMessageSignature().get(); + builder.setMessageSignature( + MessageSignature.newBuilder() + .setMessageDigest( + HashOutput.newBuilder() + .setAlgorithm( + ProtoMutators.toProtoHashAlgorithm( + messageSignature.getMessageDigest().get().getHashAlgorithm())) + .setDigest( + ByteString.copyFrom( + messageSignature.getMessageDigest().get().getDigest()))) + .setSignature(ByteString.copyFrom(messageSignature.getSignature()))); + } + if (bundle.getDsseEnvelope().isPresent()) { + var dsseEnvelope = bundle.getDsseEnvelope().get(); + var envelopeBuilder = + Envelope.newBuilder() + .setPayload(ByteString.copyFrom(dsseEnvelope.getPayload())) + .setPayloadType(dsseEnvelope.getPayloadType()); + for (var sig : dsseEnvelope.getSignatures()) { + envelopeBuilder.addSignatures( + io.intoto.EnvelopeOuterClass.Signature.newBuilder() + .setSig(ByteString.copyFrom(sig.getSig())) + .build()); + } + builder.setDsseEnvelope(envelopeBuilder.build()); + } + return builder; } private static VerificationMaterial.Builder buildVerificationMaterial(Bundle bundle) { 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 79d309e5..b7cd8cf3 100644 --- a/sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java +++ b/sigstore-java/src/main/java/dev/sigstore/dsse/InTotoPayload.java @@ -51,7 +51,11 @@ interface Subject { Map getDigest(); } + static InTotoPayload from(String payload) { + return GSON.get().fromJson(payload, InTotoPayload.class); + } + static InTotoPayload from(DsseEnvelope dsseEnvelope) { - return GSON.get().fromJson(dsseEnvelope.getPayloadAsString(), InTotoPayload.class); + return from(dsseEnvelope.getPayloadAsString()); } } diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2Client.java b/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2Client.java index b71c8007..46cb03e4 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2Client.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2Client.java @@ -15,6 +15,7 @@ */ package dev.sigstore.rekor.v2.client; +import dev.sigstore.proto.rekor.v2.DSSERequestV002; import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002; import dev.sigstore.rekor.client.RekorEntry; import dev.sigstore.rekor.client.RekorParseException; @@ -30,4 +31,12 @@ public interface RekorV2Client { */ RekorEntry putEntry(HashedRekordRequestV002 hashedRekordRequest) throws IOException, RekorParseException; + + /** + * Put a new dsse entry on the Rekor log. + * + * @param dsseRequest the request to send to rekor + * @return a {@link RekorEntry} with information about the log entry + */ + RekorEntry putEntry(DSSERequestV002 dsseRequest) throws IOException, RekorParseException; } diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttp.java b/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttp.java index 6dddc4a5..ec4b11e1 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttp.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttp.java @@ -25,6 +25,7 @@ import dev.sigstore.http.HttpParams; import dev.sigstore.http.ImmutableHttpParams; import dev.sigstore.proto.rekor.v2.CreateEntryRequest; +import dev.sigstore.proto.rekor.v2.DSSERequestV002; import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002; import dev.sigstore.rekor.client.RekorEntry; import dev.sigstore.rekor.client.RekorParseException; @@ -77,20 +78,26 @@ public RekorV2ClientHttp build() { @Override public RekorEntry putEntry(HashedRekordRequestV002 hashedRekordRequest) throws IOException, RekorParseException { + return putEntry( + CreateEntryRequest.newBuilder().setHashedRekordRequestV002(hashedRekordRequest).build()); + } + + @Override + public RekorEntry putEntry(DSSERequestV002 dsseRequest) throws IOException, RekorParseException { + return putEntry(CreateEntryRequest.newBuilder().setDsseRequestV002(dsseRequest).build()); + } + + private RekorEntry putEntry(CreateEntryRequest request) throws IOException, RekorParseException { URI rekorPutEndpoint = uri.resolve(REKOR_ENTRIES_PATH); - String jsonPayload = - JsonFormat.printer() - .print( - CreateEntryRequest.newBuilder() - .setHashedRekordRequestV002(hashedRekordRequest) - .build()); + String jsonPayload = JsonFormat.printer().print(request); HttpRequest req = HttpClients.newRequestFactory(httpParams) .buildPostRequest( new GenericUrl(rekorPutEndpoint), ByteArrayContent.fromString("application/json", jsonPayload)); + req.getHeaders().set("Accept", "application/json"); req.getHeaders().set("Content-Type", "application/json"); diff --git a/sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java b/sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java index 5b98797f..152106bc 100644 --- a/sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; /** @@ -50,6 +51,8 @@ public class KeylessSignerTest { public static List artifacts; public static List signingResults; public static KeylessSigner signer; + public static String attestation; + public static Bundle attestationResult; @BeforeAll public static void setup() throws Exception { @@ -70,6 +73,9 @@ public static void setup() throws Exception { signingResults.add(Mockito.mock(Bundle.class)); } + attestation = "some attestation"; + attestationResult = Mockito.mock(Bundle.class); + // make sure our mock signing results are not equal Assertions.assertNotEquals(signingResults.get(0), signingResults.get(1)); @@ -80,6 +86,9 @@ public static void setup() throws Exception { Mockito.doReturn(signingResults) .when(signer) .sign(Mockito.argThat(new ByteArrayListMatcher(artifactHashes))); + Mockito.doReturn(attestationResult) + .when(signer) + .attest(ArgumentMatchers.argThat(attestation::equals)); } @Test @@ -101,6 +110,11 @@ public void sign_digest() throws Exception { Assertions.assertEquals(signingResults.get(0), signer.sign(artifactHashes.get(0))); } + @Test + public void attest_validation() throws Exception { + Assertions.assertEquals(attestationResult, signer.attest(attestation)); + } + @Test @EnabledIfOidcExists(provider = OidcProviderType.GITHUB) public void sign_failGithubOidcCheck() throws Exception { diff --git a/sigstore-java/src/test/java/dev/sigstore/KeylessTest.java b/sigstore-java/src/test/java/dev/sigstore/KeylessTest.java index c687c389..84e4b319 100644 --- a/sigstore-java/src/test/java/dev/sigstore/KeylessTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/KeylessTest.java @@ -17,6 +17,7 @@ import com.google.common.hash.Hashing; import dev.sigstore.bundle.Bundle; +import dev.sigstore.dsse.InTotoPayload; import dev.sigstore.testkit.annotations.DisabledIfSkipStaging; import dev.sigstore.testkit.annotations.EnabledIfOidcExists; import dev.sigstore.testkit.annotations.OidcProviderType; @@ -29,6 +30,8 @@ import java.util.List; import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -40,6 +43,7 @@ public class KeylessTest { @TempDir public static Path testRoot; public static List artifactDigests; + public static String payload; @BeforeAll public static void setupArtifact() throws IOException { @@ -56,6 +60,12 @@ public static void setupArtifact() throws IOException { .asBytes(); artifactDigests.add(digest); } + + payload = + new String( + Base64.decode( + "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL2xvb3NlYmF6b29rYS9hYS10ZXN0IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL3Byb3ZlbmFuY2UueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODkxNzE1NDQ0IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6IjEzMDQ4MjYiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9sb29zZWJhem9va2EvYWEtdGVzdEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiZWJmZjhkZmJkNjA5YjdiMjIyMzdjNzcxOWNlMDdmMmRjNzkzNGY1ZiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvYWN0aW9ucy9ydW5zLzExOTQxNDI1NDg3L2F0dGVtcHRzLzEifX19fQ=="), + StandardCharsets.UTF_8); } @Test @@ -90,6 +100,25 @@ public void sign_staging(boolean enableRekorV2) throws Exception { } } + @Test + @EnabledIfOidcExists(provider = OidcProviderType.ANY) + @DisabledIfSkipStaging + public void attest_staging() throws Exception { + var signer = KeylessSigner.builder().sigstoreStagingDefaults().enableRekorV2(true).build(); + var result = signer.attest(payload); + + Assertions.assertNotNull(result.getDsseEnvelope().get()); + Assertions.assertEquals(payload, result.getDsseEnvelope().get().getPayloadAsString()); + Assertions.assertEquals(1, result.getEntries().size()); + Assertions.assertEquals("0.0.2", result.getEntries().get(0).getBodyDecoded().getApiVersion()); + + var verifier = KeylessVerifier.builder().sigstoreStagingDefaults().build(); + var intotoPayload = InTotoPayload.from(result.getDsseEnvelope().get()); + var artifactDigest = Hex.decode(intotoPayload.getSubject().get(0).getDigest().get("sha256")); + verifier.verify(artifactDigest, result, VerificationOptions.empty()); + checkBundleSerialization(result); + } + private void verifySigningResult(List results, boolean enableRekorV2) throws IOException { Assertions.assertEquals(artifactDigests.size(), results.size()); diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttpTest.java b/sigstore-java/src/test/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttpTest.java index 24c06afa..a624e7b8 100644 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttpTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttpTest.java @@ -20,9 +20,12 @@ import com.google.protobuf.ByteString; import dev.sigstore.AlgorithmRegistry; +import dev.sigstore.bundle.ImmutableDsseEnvelope; +import dev.sigstore.bundle.ImmutableSignature; import dev.sigstore.encryption.signers.Signers; import dev.sigstore.proto.common.v1.PublicKeyDetails; import dev.sigstore.proto.common.v1.X509Certificate; +import dev.sigstore.proto.rekor.v2.DSSERequestV002; import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002; import dev.sigstore.proto.rekor.v2.Signature; import dev.sigstore.proto.rekor.v2.Verifier; @@ -30,6 +33,7 @@ import dev.sigstore.testing.CertGenerator; import dev.sigstore.trustroot.Service; import dev.sigstore.tuf.SigstoreTufClient; +import io.intoto.EnvelopeOuterClass; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; @@ -72,6 +76,17 @@ public void putEntry() throws Exception { assertNotNull(entry.getLogID()); } + @Test + public void putEntry_dsse() throws Exception { + var req = createDsseRequest(); + var entry = client.putEntry(req); + + assertNotNull(entry); + assertNotNull(entry.getVerification().getInclusionProof()); + assertTrue(entry.getLogIndex() >= 0); + assertNotNull(entry.getLogID()); + } + @NotNull private static HashedRekordRequestV002 createdRekorRequest() throws NoSuchAlgorithmException, @@ -112,4 +127,53 @@ private static HashedRekordRequestV002 createdRekorRequest() .setSignature(signature) .build(); } + + @NotNull + private static DSSERequestV002 createDsseRequest() + throws NoSuchAlgorithmException, + InvalidKeyException, + SignatureException, + OperatorCreationException, + CertificateException, + IOException { + var payload = "{\"foo\":\"bar\"}"; + var payloadType = "application/vnd.in-toto+json"; + + // sign the full content (these signers do the artifact hashing themselves) + var signer = Signers.from(AlgorithmRegistry.SigningAlgorithm.PKIX_ECDSA_P256_SHA_256); + + // create a fake signing cert (not fulcio/dex) + var cert = CertGenerator.newCert(signer.getPublicKey()).getEncoded(); + + var dsse = + ImmutableDsseEnvelope.builder() + .payload(payload.getBytes(StandardCharsets.UTF_8)) + .payloadType(payloadType) + .build(); + + var pae = dsse.getPAE(); + var sig = signer.sign(pae); + var dsseSigned = + ImmutableDsseEnvelope.builder() + .from(dsse) + .addSignatures(ImmutableSignature.builder().sig(sig).build()) + .build(); + + Verifier verifier = + Verifier.newBuilder() + .setX509Certificate( + X509Certificate.newBuilder().setRawBytes(ByteString.copyFrom(cert)).build()) + .setKeyDetails(PublicKeyDetails.PKIX_ECDSA_P256_SHA_256) + .build(); + + return DSSERequestV002.newBuilder() + .setEnvelope( + EnvelopeOuterClass.Envelope.newBuilder() + .setPayload(ByteString.copyFrom(dsseSigned.getPayload())) + .setPayloadType(dsseSigned.getPayloadType()) + .addSignatures( + EnvelopeOuterClass.Signature.newBuilder().setSig(ByteString.copyFrom(sig)))) + .addVerifiers(verifier) + .build(); + } }