Skip to content

Commit a9d331b

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

File tree

7 files changed

+241
-26
lines changed

7 files changed

+241
-26
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;
@@ -66,6 +67,7 @@
6667
import dev.sigstore.trustroot.Service;
6768
import dev.sigstore.trustroot.SigstoreConfigurationException;
6869
import dev.sigstore.tuf.SigstoreTufClient;
70+
import io.intoto.EnvelopeOuterClass;
6971
import java.io.IOException;
7072
import java.nio.charset.StandardCharsets;
7173
import java.nio.file.Path;
@@ -383,6 +385,140 @@ public Builder sigstoreStagingDefaults() {
383385
}
384386
}
385387

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ static SigstoreSigningConfig from(
6464
public static SigstoreSigningConfig STAGING =
6565
from(
6666
FULCIO_STAGING_URI,
67-
Service.of(REKOR_PUBLIC_GOOD_URI, 1),
67+
Service.of(REKOR_STAGING_URI, 1),
6868
DEX_STAGING_GOOD_URI,
6969
TSA_STAGING_URI);
7070
public static SigstoreSigningConfig STAGING_REKOR_V2 =

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
import dev.sigstore.testing.matchers.ByteArrayListMatcher;
2323
import dev.sigstore.testkit.annotations.EnabledIfOidcExists;
2424
import dev.sigstore.testkit.annotations.OidcProviderType;
25+
import dev.sigstore.trustroot.LegacySigningConfig;
2526
import java.nio.charset.StandardCharsets;
2627
import java.nio.file.Files;
2728
import java.nio.file.Path;
2829
import java.util.ArrayList;
2930
import java.util.HashMap;
3031
import java.util.List;
3132
import java.util.UUID;
33+
import org.bouncycastle.util.encoders.Base64;
3234
import org.bouncycastle.util.encoders.Hex;
3335
import org.hamcrest.CoreMatchers;
3436
import org.hamcrest.MatcherAssert;
@@ -87,6 +89,31 @@ public void sign_file() throws Exception {
8789
Assertions.assertEquals(signingResults.get(0), signer.signFile(artifacts.get(0)));
8890
}
8991

92+
@Test
93+
public void sign_dssev2() throws Exception {
94+
var signer =
95+
KeylessSigner.builder()
96+
.sigstoreStagingDefaults()
97+
.signingConfigProvider(() -> LegacySigningConfig.STAGING_REKOR_V2)
98+
.build();
99+
var bundle =
100+
signer.attest(
101+
new String(
102+
Base64.decode(
103+
"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL2xvb3NlYmF6b29rYS9hYS10ZXN0IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL3Byb3ZlbmFuY2UueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODkxNzE1NDQ0IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6IjEzMDQ4MjYiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9sb29zZWJhem9va2EvYWEtdGVzdEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiZWJmZjhkZmJkNjA5YjdiMjIyMzdjNzcxOWNlMDdmMmRjNzkzNGY1ZiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvYWN0aW9ucy9ydW5zLzExOTQxNDI1NDg3L2F0dGVtcHRzLzEifX19fQ=="),
104+
StandardCharsets.UTF_8));
105+
var bundle2 =
106+
signer.attest(
107+
new String(
108+
Base64.decode(
109+
"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL2xvb3NlYmF6b29rYS9hYS10ZXN0IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL3Byb3ZlbmFuY2UueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODkxNzE1NDQ0IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6IjEzMDQ4MjYiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9sb29zZWJhem9va2EvYWEtdGVzdEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiZWJmZjhkZmJkNjA5YjdiMjIyMzdjNzcxOWNlMDdmMmRjNzkzNGY1ZiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvcHJvdmVuYW5jZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vbG9vc2ViYXpvb2thL2FhLXRlc3QvYWN0aW9ucy9ydW5zLzExOTQxNDI1NDg3L2F0dGVtcHRzLzEifX19fQ=="),
110+
StandardCharsets.UTF_8));
111+
System.out.println("====================== bundle 1 =======================");
112+
System.out.println(bundle.toJson());
113+
System.out.println("====================== bundle 2 =======================");
114+
System.out.println(bundle2.toJson());
115+
}
116+
90117
@Test
91118
public void sign_files() throws Exception {
92119
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)