Skip to content

Commit 71dc00c

Browse files
committed
experimental DO NOT MERGE dsse signer
Signed-off-by: Appu Goundan <[email protected]>
1 parent b9c5bf5 commit 71dc00c

File tree

7 files changed

+276
-25
lines changed

7 files changed

+276
-25
lines changed

sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
import com.google.errorprone.annotations.CheckReturnValue;
2424
import com.google.errorprone.annotations.concurrent.GuardedBy;
2525
import com.google.protobuf.ByteString;
26-
import dev.sigstore.bundle.Bundle;
26+
import com.google.protobuf.InvalidProtocolBufferException;
27+
import com.google.protobuf.util.JsonFormat;
28+
import dev.sigstore.bundle.*;
2729
import dev.sigstore.bundle.Bundle.MessageSignature;
28-
import dev.sigstore.bundle.ImmutableBundle;
29-
import dev.sigstore.bundle.ImmutableTimestamp;
3030
import dev.sigstore.encryption.certificates.Certificates;
3131
import dev.sigstore.encryption.signers.Signer;
3232
import dev.sigstore.encryption.signers.Signers;
@@ -42,6 +42,7 @@
4242
import dev.sigstore.oidc.client.OidcTokenMatcher;
4343
import dev.sigstore.proto.ProtoMutators;
4444
import dev.sigstore.proto.common.v1.X509Certificate;
45+
import dev.sigstore.proto.rekor.v2.DSSERequestV002;
4546
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
4647
import dev.sigstore.proto.rekor.v2.Signature;
4748
import dev.sigstore.proto.rekor.v2.Verifier;
@@ -65,6 +66,7 @@
6566
import dev.sigstore.trustroot.Service;
6667
import dev.sigstore.trustroot.SigstoreConfigurationException;
6768
import dev.sigstore.tuf.SigstoreTufClient;
69+
import io.intoto.EnvelopeOuterClass;
6870
import java.io.IOException;
6971
import java.nio.charset.StandardCharsets;
7072
import java.nio.file.Path;
@@ -379,6 +381,140 @@ public Builder sigstoreStagingDefaults() {
379381
}
380382
}
381383

384+
public Bundle attest(String payload) throws KeylessSignerException {
385+
// Technically speaking, it is unlikely the certificate will expire between signing artifacts
386+
// However, files might be large, and it might take time to talk to Rekor
387+
// so we check the certificate expiration here.
388+
try {
389+
renewSigningCertificate();
390+
} catch (FulcioVerificationException
391+
| UnsupportedAlgorithmException
392+
| OidcException
393+
| IOException
394+
| InterruptedException
395+
| InvalidKeyException
396+
| NoSuchAlgorithmException
397+
| SignatureException
398+
| CertificateException ex) {
399+
throw new KeylessSignerException("Failed to obtain signing certificate", ex);
400+
}
401+
CertPath signingCert;
402+
byte[] signingCertPemBytes;
403+
byte[] encodedCert;
404+
lock.readLock().lock();
405+
try {
406+
signingCert = this.signingCert;
407+
signingCertPemBytes = this.signingCertPemBytes;
408+
encodedCert = this.encodedCert;
409+
if (signingCert == null) {
410+
throw new IllegalStateException("Signing certificate is null");
411+
}
412+
} finally {
413+
lock.readLock().unlock();
414+
}
415+
416+
var bundleBuilder = ImmutableBundle.builder().certPath(signingCert);
417+
418+
if (rekorV2Client != null) { // Using Rekor v2 and a TSA
419+
Preconditions.checkNotNull(
420+
timestampClient, "Timestamp client must be configured for Rekor v2");
421+
Preconditions.checkNotNull(
422+
timestampVerifier, "Timestamp verifier must be configured for Rekor v2");
423+
424+
var verifier =
425+
Verifier.newBuilder()
426+
.setX509Certificate(
427+
X509Certificate.newBuilder()
428+
.setRawBytes(ByteString.copyFrom(encodedCert))
429+
.build())
430+
.setKeyDetails(ProtoMutators.toPublicKeyDetails(signingAlgorithm))
431+
.build();
432+
433+
var dsse =
434+
ImmutableDsseEnvelope.builder()
435+
.payload(payload.getBytes(StandardCharsets.UTF_8))
436+
.payloadType("application/vnd.in-toto+json")
437+
.build();
438+
var pae = dsse.getPAE();
439+
Bundle.DsseEnvelope dsseSigned;
440+
try {
441+
var sig = signer.sign(pae);
442+
dsseSigned =
443+
ImmutableDsseEnvelope.builder()
444+
.from(dsse)
445+
.addSignatures(ImmutableSignature.builder().sig(sig).build())
446+
.build();
447+
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
448+
throw new RuntimeException(e);
449+
}
450+
451+
var dsseRequest =
452+
DSSERequestV002.newBuilder()
453+
.setEnvelope(
454+
EnvelopeOuterClass.Envelope.newBuilder()
455+
.setPayload(ByteString.copyFrom(dsseSigned.getPayload()))
456+
.setPayloadType(dsseSigned.getPayloadType())
457+
.addSignatures(
458+
EnvelopeOuterClass.Signature.newBuilder()
459+
.setSig(ByteString.copyFrom(dsseSigned.getSignature())))
460+
.build())
461+
.addVerifiers(verifier)
462+
.build();
463+
464+
try {
465+
System.out.println(JsonFormat.printer().print(dsseRequest));
466+
} catch (InvalidProtocolBufferException e) {
467+
throw new RuntimeException(e);
468+
}
469+
470+
var signatureDigest = Hashing.sha256().hashBytes(dsseSigned.getSignature()).asBytes();
471+
472+
var tsReq =
473+
ImmutableTimestampRequest.builder()
474+
.hashAlgorithm(dev.sigstore.timestamp.client.HashAlgorithm.SHA256)
475+
.hash(signatureDigest)
476+
.build();
477+
478+
TimestampResponse tsResp;
479+
try {
480+
tsResp = timestampClient.timestamp(tsReq);
481+
} catch (TimestampException ex) {
482+
throw new KeylessSignerException("Failed to generate timestamp", ex);
483+
}
484+
485+
try {
486+
timestampVerifier.verify(tsResp, dsseSigned.getSignature());
487+
} catch (TimestampVerificationException ex) {
488+
throw new KeylessSignerException("Returned timestamp was invalid", ex);
489+
}
490+
491+
Bundle.Timestamp timestamp =
492+
ImmutableTimestamp.builder().rfc3161Timestamp(tsResp.getEncoded()).build();
493+
494+
bundleBuilder.addTimestamps(timestamp);
495+
496+
RekorEntry entry;
497+
try {
498+
entry = rekorV2Client.putEntry(dsseRequest);
499+
} catch (IOException | RekorParseException ex) {
500+
throw new KeylessSignerException("Failed to put entry in rekor", ex);
501+
}
502+
503+
try {
504+
rekorVerifier.verifyEntry(entry);
505+
} catch (RekorVerificationException ex) {
506+
throw new KeylessSignerException("Failed to validate rekor entry after signing", ex);
507+
}
508+
509+
bundleBuilder.dsseEnvelope(dsseSigned);
510+
511+
bundleBuilder.addEntries(entry);
512+
} else {
513+
throw new IllegalStateException("Rekor v2 client was not configured.");
514+
}
515+
return bundleBuilder.build();
516+
}
517+
382518
/**
383519
* Sign one or more artifact digests using the keyless signing workflow. The oidc/fulcio dance to
384520
* obtain a signing certificate will only occur once. The same ephemeral private key will be used

sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import dev.sigstore.proto.rekor.v1.KindVersion;
3434
import dev.sigstore.proto.rekor.v1.TransparencyLogEntry;
3535
import dev.sigstore.rekor.client.RekorEntry;
36+
import io.intoto.EnvelopeOuterClass;
3637
import java.security.cert.CertificateEncodingException;
3738
import java.util.Base64;
3839
import java.util.List;
@@ -78,28 +79,44 @@ static String writeBundle(Bundle signingResult) {
7879
* @return Sigstore Bundle in protobuf builder format
7980
*/
8081
static dev.sigstore.proto.bundle.v1.Bundle.Builder createBundleBuilder(Bundle bundle) {
81-
if (bundle.getMessageSignature().isEmpty()) {
82-
throw new IllegalStateException("can only serialize bundles with message signatures");
83-
}
84-
var messageSignature = bundle.getMessageSignature().get();
85-
if (messageSignature.getMessageDigest().isEmpty()) {
82+
// if (bundle.getMessageSignature().isEmpty()) {
83+
// throw new IllegalStateException("can only serialize bundles with message signatures");
84+
// }
85+
var builder =
86+
dev.sigstore.proto.bundle.v1.Bundle.newBuilder()
87+
.setMediaType(bundle.getMediaType())
88+
.setVerificationMaterial(buildVerificationMaterial(bundle));
89+
if (bundle.getMessageSignature().isPresent()) {
90+
var messageSignature = bundle.getMessageSignature().get();
91+
if (messageSignature.getMessageDigest().isEmpty()) {
92+
throw new IllegalStateException(
93+
"keyless signature must have artifact digest when serializing to bundle");
94+
}
95+
builder.setMessageSignature(
96+
MessageSignature.newBuilder()
97+
.setMessageDigest(
98+
HashOutput.newBuilder()
99+
.setAlgorithm(
100+
ProtoMutators.toProtoHashAlgorithm(
101+
messageSignature.getMessageDigest().get().getHashAlgorithm()))
102+
.setDigest(
103+
ByteString.copyFrom(
104+
messageSignature.getMessageDigest().get().getDigest())))
105+
.setSignature(ByteString.copyFrom(messageSignature.getSignature())));
106+
} else if (bundle.getDsseEnvelope().isPresent()) {
107+
var de = bundle.getDsseEnvelope().get();
108+
builder.setDsseEnvelope(
109+
EnvelopeOuterClass.Envelope.newBuilder()
110+
.setPayload(ByteString.copyFrom(de.getPayload()))
111+
.setPayloadType(de.getPayloadType())
112+
.addSignatures(
113+
EnvelopeOuterClass.Signature.newBuilder()
114+
.setSig(ByteString.copyFrom(de.getSignature()))));
115+
} else {
86116
throw new IllegalStateException(
87-
"keyless signature must have artifact digest when serializing to bundle");
117+
"can only serialize bundles with message signature or dsse envelope");
88118
}
89-
return dev.sigstore.proto.bundle.v1.Bundle.newBuilder()
90-
.setMediaType(bundle.getMediaType())
91-
.setVerificationMaterial(buildVerificationMaterial(bundle))
92-
.setMessageSignature(
93-
MessageSignature.newBuilder()
94-
.setMessageDigest(
95-
HashOutput.newBuilder()
96-
.setAlgorithm(
97-
ProtoMutators.toProtoHashAlgorithm(
98-
messageSignature.getMessageDigest().get().getHashAlgorithm()))
99-
.setDigest(
100-
ByteString.copyFrom(
101-
messageSignature.getMessageDigest().get().getDigest())))
102-
.setSignature(ByteString.copyFrom(messageSignature.getSignature())));
119+
return builder;
103120
}
104121

105122
private static VerificationMaterial.Builder buildVerificationMaterial(Bundle bundle) {

sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2Client.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package dev.sigstore.rekor.v2.client;
1717

18+
import dev.sigstore.proto.rekor.v2.DSSERequestV002;
1819
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
1920
import dev.sigstore.rekor.client.RekorEntry;
2021
import dev.sigstore.rekor.client.RekorParseException;
@@ -30,4 +31,6 @@ public interface RekorV2Client {
3031
*/
3132
RekorEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
3233
throws IOException, RekorParseException;
34+
35+
RekorEntry putEntry(DSSERequestV002 dsseRequestV002) throws IOException, RekorParseException;
3336
}

sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttp.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import dev.sigstore.http.HttpParams;
2626
import dev.sigstore.http.ImmutableHttpParams;
2727
import dev.sigstore.proto.rekor.v2.CreateEntryRequest;
28+
import dev.sigstore.proto.rekor.v2.DSSERequestV002;
2829
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
2930
import dev.sigstore.rekor.client.RekorEntry;
3031
import dev.sigstore.rekor.client.RekorParseException;
@@ -108,4 +109,36 @@ public RekorEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
108109

109110
return RekorEntry.fromTLogEntryJson(respEntryJson);
110111
}
112+
113+
@Override
114+
public RekorEntry putEntry(DSSERequestV002 dsseRequestV002)
115+
throws IOException, RekorParseException {
116+
URI rekorPutEndpoint = uri.resolve(REKOR_ENTRIES_PATH);
117+
118+
String jsonPayload =
119+
JsonFormat.printer()
120+
.print(CreateEntryRequest.newBuilder().setDsseRequestV002(dsseRequestV002).build());
121+
122+
HttpRequest req =
123+
HttpClients.newRequestFactory(httpParams)
124+
.buildPostRequest(
125+
new GenericUrl(rekorPutEndpoint),
126+
ByteArrayContent.fromString("application/json", jsonPayload));
127+
req.getHeaders().set("Accept", "application/json");
128+
req.getHeaders().set("Content-Type", "application/json");
129+
130+
HttpResponse resp = req.execute();
131+
if (resp.getStatusCode() != 201) {
132+
throw new IOException(
133+
String.format(
134+
Locale.ROOT,
135+
"bad response from rekor @ '%s' : %s",
136+
rekorPutEndpoint,
137+
resp.parseAsString()));
138+
}
139+
140+
String respEntryJson = resp.parseAsString();
141+
142+
return RekorEntry.fromTLogEntryJson(respEntryJson);
143+
}
111144
}

sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
1717

1818
import com.google.common.hash.Hashing;
1919
import dev.sigstore.bundle.Bundle;
20+
import dev.sigstore.oidc.client.OidcClients;
2021
import dev.sigstore.oidc.client.OidcTokenMatcher;
22+
import dev.sigstore.oidc.client.TokenStringOidcClient;
2123
import dev.sigstore.strings.StringMatcher;
2224
import dev.sigstore.testing.matchers.ByteArrayListMatcher;
2325
import dev.sigstore.testkit.annotations.EnabledIfOidcExists;
2426
import dev.sigstore.testkit.annotations.OidcProviderType;
27+
import dev.sigstore.testkit.oidc.ConformanceTestingTokenProvider;
2528
import java.nio.charset.StandardCharsets;
2629
import java.nio.file.Files;
2730
import java.nio.file.Path;
2831
import java.util.ArrayList;
2932
import java.util.HashMap;
3033
import java.util.List;
3134
import java.util.UUID;
35+
import org.bouncycastle.util.encoders.Base64;
3236
import org.bouncycastle.util.encoders.Hex;
3337
import org.hamcrest.CoreMatchers;
3438
import org.hamcrest.MatcherAssert;
@@ -87,6 +91,31 @@ public void sign_file() throws Exception {
8791
Assertions.assertEquals(signingResults.get(0), signer.signFile(artifacts.get(0)));
8892
}
8993

94+
@Test
95+
public void sign_dssev2() throws Exception {
96+
var signer =
97+
KeylessSigner.builder()
98+
.sigstoreStagingDefaults()
99+
.forceCredentialProviders(OidcClients.of(TokenStringOidcClient.from(ConformanceTestingTokenProvider.newProvider())))
100+
.build();
101+
var bundle =
102+
signer.attest(
103+
new String(
104+
Base64.decode(
105+
"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL2xvb3NlYmF6b29rYS9hYS10ZXN0IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL3Byb3ZlbmFuY2UueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODkxNzE1NDQ0IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6IjEzMDQ4MjYiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9sb29zZWJhem9va2EvYWEtdGVzdEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiZWJmZjhkZmJkNjA5YjdiMjIyMzdjNzcxOWNlMDdmMmRjNzkzNGY1ZiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvYWN0aW9ucy9ydW5zLzExOTQxNDI1NDg3L2F0dGVtcHRzLzEifX19fQ=="),
106+
StandardCharsets.UTF_8));
107+
var bundle2 =
108+
signer.attest(
109+
new String(
110+
Base64.decode(
111+
"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL21vb3NlL2FhLXRlc3QiLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sIn19LCJpbnRlcm5hbFBhcmFtZXRlcnMiOnsiZ2l0aHViIjp7ImV2ZW50X25hbWUiOiJ3b3JrZmxvd19kaXNwYXRjaCIsInJlcG9zaXRvcnlfaWQiOiI4OTE3MTU0NDQiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMTMwNDgyNiIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL21vb3NlL2FhLXRlc3RAcmVmcy9oZWFkcy9tYWluIiwiZGlnZXN0Ijp7ImdpdENvbW1pdCI6ImViZmY4ZGZiZDYwOWI3YjIyMjM3Yzc3MTljZTA3ZjJkYzc5MzRmNWYifX1dfSwicnVuRGV0YWlscyI6eyJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly9naXRodWIuY29tL21vb3NlL2FhLXRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vbW9vc2UvYWEtdGVzdC9hY3Rpb25zL3J1bnMvMTE5NDE0MjU0ODcvYXR0ZW1wdHMvMSJ9fX19Cg=="),
112+
StandardCharsets.UTF_8));
113+
System.out.println("====================== bundle 1 =======================");
114+
System.out.println(bundle.toJson());
115+
System.out.println("====================== bundle 2 =======================");
116+
System.out.println(bundle2.toJson());
117+
}
118+
90119
@Test
91120
public void sign_files() throws Exception {
92121
var signingResultsMap = new HashMap<Path, Bundle>();

sigstore-java/src/test/java/dev/sigstore/KeylessTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ public void sign_production() throws Exception {
7878
@EnabledIfOidcExists(provider = OidcProviderType.ANY)
7979
@DisabledIfSkipStaging
8080
public void sign_staging(boolean enableRekorV2) throws Exception {
81-
var signer =
82-
KeylessSigner.builder().sigstoreStagingDefaults().enableRekorV2(enableRekorV2).build();
81+
var signer = KeylessSigner.builder().sigstoreStagingDefaults().build();
8382
var results = signer.sign(artifactDigests);
8483
verifySigningResult(results);
8584

0 commit comments

Comments
 (0)