Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -671,4 +679,163 @@ public Map<Path, Bundle> signFiles(List<Path> 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();
}
}
60 changes: 42 additions & 18 deletions sigstore-java/src/main/java/dev/sigstore/bundle/BundleWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ interface Subject {
Map<String, String> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");

Expand Down
Loading
Loading