Skip to content

Commit

Permalink
Merge pull request #671 from sigstore/client-init
Browse files Browse the repository at this point in the history
Rework signing initialization
  • Loading branch information
loosebazooka committed Mar 29, 2024
2 parents 9ec514d + 03433f6 commit 36a6b2e
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 100 deletions.
15 changes: 10 additions & 5 deletions sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,11 @@ public KeylessSigner build()
Preconditions.checkNotNull(sigstoreTufClient, "sigstoreTufClient");
sigstoreTufClient.update();
var trustedRoot = sigstoreTufClient.getSigstoreTrustedRoot();
var fulcioClient = FulcioClient.builder().setCertificateAuthority(trustedRoot).build();
var fulcioClient =
FulcioClient.builder().setUri(trustedRoot.getCAs().current().getUri()).build();
var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot);
var rekorClient = RekorClient.builder().setTransparencyLog(trustedRoot).build();
var rekorClient =
RekorClient.builder().setUri(trustedRoot.getTLogs().current().getBaseUrl()).build();
var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot);
return new KeylessSigner(
fulcioClient,
Expand Down Expand Up @@ -354,7 +356,7 @@ private void renewSigningCertificate()
}
}

CertPath signingCert =
CertPath renewedSigningCert =
fulcioClient.signingCertificate(
CertificateRequest.newCertificateRequest(
signer.getPublicKey(),
Expand All @@ -363,8 +365,11 @@ private void renewSigningCertificate()
tokenInfo.getSubjectAlternativeName().getBytes(StandardCharsets.UTF_8))));
// TODO: this signing workflow mandates SCTs, but fulcio itself doesn't, figure out a way to
// allow that to be known
fulcioVerifier.verifySigningCertificate(signingCert);
this.signingCert = signingCert;

var trimmed = fulcioVerifier.trimTrustedParent(renewedSigningCert);

fulcioVerifier.verifySigningCertificate(trimmed);
this.signingCert = trimmed;
signingCertPemBytes = Certificates.toPemBytes(signingCert);
} finally {
lock.writeLock().unlock();
Expand Down
43 changes: 31 additions & 12 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,39 @@
import dev.sigstore.rekor.client.RekorParseException;
import dev.sigstore.rekor.client.RekorVerificationException;
import dev.sigstore.rekor.client.RekorVerifier;
import dev.sigstore.trustroot.TransparencyLog;
import dev.sigstore.tuf.SigstoreTufClient;
import java.io.IOException;
import java.nio.file.Path;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.sql.Date;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.bouncycastle.util.encoders.Hex;

/** Verify hashrekords from rekor signed using the keyless signing flow with fulcio certificates. */
public class KeylessVerifier {
private final FulcioVerifier fulcioVerifier;
private final RekorVerifier rekorVerifier;
private final RekorClient rekorClient;

// a client per remote trusted log
private final List<RekorClient> rekorClients;

private KeylessVerifier(
FulcioVerifier fulcioVerifier, RekorClient rekorClient, RekorVerifier rekorVerifier) {
FulcioVerifier fulcioVerifier, List<RekorClient> rekorClients, RekorVerifier rekorVerifier) {
this.fulcioVerifier = fulcioVerifier;
this.rekorClient = rekorClient;
this.rekorVerifier = rekorVerifier;
this.rekorClients = rekorClients;
}

public static KeylessVerifier.Builder builder() {
Expand All @@ -72,9 +77,14 @@ public KeylessVerifier build()
Preconditions.checkNotNull(trustedRootProvider);
var trustedRoot = trustedRootProvider.get();
var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot);
var rekorClient = RekorClient.builder().setTransparencyLog(trustedRoot).build();
var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot);
return new KeylessVerifier(fulcioVerifier, rekorClient, rekorVerifier);
var rekorClients =
trustedRoot.getTLogs().getTransparencyLogs().stream()
.map(TransparencyLog::getBaseUrl)
.distinct()
.map(uri -> RekorClient.builder().setUri(uri).build())
.collect(Collectors.toList());
return new KeylessVerifier(fulcioVerifier, rekorClients, rekorVerifier);
}

public Builder sigstorePublicDefaults() throws IOException {
Expand Down Expand Up @@ -207,7 +217,7 @@ public void verify(byte[] artifactDigest, KeylessVerificationRequest request)
}

private RekorEntry getEntryFromRekor(
byte[] artifactDigest, Certificate leafCert, byte[] signature)
byte[] artifactDigest, X509Certificate leafCert, byte[] signature)
throws KeylessVerificationException {
// rebuild the hashedRekord so we can query the log for it
HashedRekordRequest hashedRekordRequest = null;
Expand All @@ -221,15 +231,24 @@ private RekorEntry getEntryFromRekor(
}
Optional<RekorEntry> rekorEntry;

// attempt to grab the rekord from the rekor instance
// attempt to grab a valid rekord from all known rekor instances
try {
rekorEntry = rekorClient.getEntry(hashedRekordRequest);
if (rekorEntry.isEmpty()) {
throw new KeylessVerificationException("Rekor entry was not found");
for (var rekorClient : rekorClients) {
rekorEntry = rekorClient.getEntry(hashedRekordRequest);
if (rekorEntry.isPresent()) {
var entryTime = Date.from(rekorEntry.get().getIntegratedTimeInstant());
try {
// only return this entry if it's valid for the certificate
leafCert.checkValidity(entryTime);
} catch (CertificateExpiredException | CertificateNotYetValidException ex) {
continue;
}
return rekorEntry.get();
}
}
} catch (IOException | RekorParseException e) {
throw new KeylessVerificationException("Could not retrieve rekor entry", e);
}
return rekorEntry.get();
throw new KeylessVerificationException("No valid rekor entry was not found in any known logs");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import dev.sigstore.encryption.certificates.Certificates;
import dev.sigstore.fulcio.v2.CAGrpc;
import dev.sigstore.fulcio.v2.CertificateChain;
import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest;
Expand All @@ -29,9 +28,8 @@
import dev.sigstore.http.GrpcChannels;
import dev.sigstore.http.HttpParams;
import dev.sigstore.http.ImmutableHttpParams;
import dev.sigstore.trustroot.CertificateAuthority;
import dev.sigstore.trustroot.SigstoreTrustedRoot;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
Expand All @@ -45,19 +43,19 @@
public class FulcioClient {

private final HttpParams httpParams;
private final CertificateAuthority certificateAuthority;
private final URI uri;

public static Builder builder() {
return new Builder();
}

private FulcioClient(HttpParams httpParams, CertificateAuthority certificateAuthority) {
this.certificateAuthority = certificateAuthority;
private FulcioClient(HttpParams httpParams, URI uri) {
this.uri = uri;
this.httpParams = httpParams;
}

public static class Builder {
private CertificateAuthority certificateAuthority;
private URI uri = URI.create("https://fulcio.sigstore.dev");
private HttpParams httpParams = ImmutableHttpParams.builder().build();

private Builder() {}
Expand All @@ -68,20 +66,14 @@ public Builder setHttpParams(HttpParams httpParams) {
return this;
}

/** The remote fulcio instance. */
public Builder setCertificateAuthority(CertificateAuthority certificateAuthority) {
this.certificateAuthority = certificateAuthority;
return this;
}

/** The remote fulcio instance inferred from a trustedRoot. */
public Builder setCertificateAuthority(SigstoreTrustedRoot trustedRoot) {
this.certificateAuthority = trustedRoot.getCAs().current();
/** Base url of the remote fulcio instance. */
public Builder setUri(URI uri) {
this.uri = uri;
return this;
}

public FulcioClient build() {
return new FulcioClient(httpParams, certificateAuthority);
return new FulcioClient(httpParams, uri);
}
}

Expand All @@ -93,16 +85,11 @@ public FulcioClient build() {
*/
public CertPath signingCertificate(CertificateRequest request)
throws InterruptedException, CertificateException {
if (!certificateAuthority.isCurrent()) {
throw new RuntimeException(
"Certificate Authority '" + certificateAuthority.getUri() + "' is not current");
}
// TODO: 1. If we want to reduce the cost of creating channels/connections, we could try
// to make a new connection once per batch of fulcio requests, but we're not really
// at that point yet.
// TODO: 2. getUri().getAuthority() is potentially prone to error if we don't get a good URI
var channel =
GrpcChannels.newManagedChannel(certificateAuthority.getUri().getAuthority(), httpParams);
var channel = GrpcChannels.newManagedChannel(uri.getAuthority(), httpParams);

try {
var client = CAGrpc.newBlockingStub(channel);
Expand Down Expand Up @@ -135,9 +122,7 @@ public CertPath signingCertificate(CertificateRequest request)
if (certs.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) {
throw new CertificateException("Detached SCTs are not supported");
}
return Certificates.trimParent(
decodeCerts(certs.getSignedCertificateEmbeddedSct().getChain()),
certificateAuthority.getCertPath());
return decodeCerts(certs.getSignedCertificateEmbeddedSct().getChain());
} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ public void verifySigningCertificate(CertPath signingCertificate)
verifySct(fullCertPath);
}

public CertPath trimTrustedParent(CertPath signingCertificate)
throws FulcioVerificationException, CertificateException {
for (var ca : cas) {
if (Certificates.containsParent(signingCertificate, ca.getCertPath())) {
return Certificates.trimParent(signingCertificate, ca.getCertPath());
}
}
throw new FulcioVerificationException("Certificate does not chain to trusted roots");
}

/**
* Find a valid cert path that chains back up to the trusted root certs and reconstruct a
* certificate path combining the provided un-trusted certs and a known set of trusted and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.common.base.Preconditions;
import dev.sigstore.http.HttpClients;
import dev.sigstore.http.HttpParams;
import dev.sigstore.http.ImmutableHttpParams;
import dev.sigstore.trustroot.SigstoreTrustedRoot;
import dev.sigstore.trustroot.TransparencyLog;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
Expand All @@ -42,20 +39,20 @@ public class RekorClient {
public static final String REKOR_INDEX_SEARCH_PATH = "/api/v1/index/retrieve";

private final HttpParams httpParams;
private final TransparencyLog tlog;
private final URI uri;

public static RekorClient.Builder builder() {
return new RekorClient.Builder();
}

private RekorClient(HttpParams httpParams, TransparencyLog tlog) {
this.tlog = tlog;
private RekorClient(HttpParams httpParams, URI uri) {
this.uri = uri;
this.httpParams = httpParams;
}

public static class Builder {
private HttpParams httpParams = ImmutableHttpParams.builder().build();
private TransparencyLog tlog;
private URI uri = URI.create("https://rekor.sigstore.dev");

private Builder() {}

Expand All @@ -65,21 +62,14 @@ public Builder setHttpParams(HttpParams httpParams) {
return this;
}

/** Configure the remote rekor instance to communicate with. */
public Builder setTransparencyLog(TransparencyLog tlog) {
this.tlog = tlog;
return this;
}

/** Configure the remote rekor instance to communicate with, inferred from a trusted root. */
public Builder setTransparencyLog(SigstoreTrustedRoot trustedRoot) {
this.tlog = trustedRoot.getTLogs().current();
/** Base url of the remote rekor instance. */
public Builder setUri(URI uri) {
this.uri = uri;
return this;
}

public RekorClient build() {
Preconditions.checkNotNull(tlog);
return new RekorClient(httpParams, tlog);
return new RekorClient(httpParams, uri);
}
}

Expand All @@ -91,7 +81,7 @@ public RekorClient build() {
*/
public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest)
throws IOException, RekorParseException {
URI rekorPutEndpoint = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH);
URI rekorPutEndpoint = uri.resolve(REKOR_ENTRIES_PATH);

HttpRequest req =
HttpClients.newRequestFactory(httpParams)
Expand All @@ -112,7 +102,7 @@ public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest)
resp.parseAsString()));
}

URI rekorEntryUri = tlog.getBaseUrl().resolve(resp.getHeaders().getLocation());
URI rekorEntryUri = uri.resolve(resp.getHeaders().getLocation());
String entry = resp.parseAsString();
return RekorResponse.newRekorResponse(rekorEntryUri, entry);
}
Expand All @@ -123,7 +113,7 @@ public Optional<RekorEntry> getEntry(HashedRekordRequest hashedRekordRequest)
}

public Optional<RekorEntry> getEntry(String UUID) throws IOException, RekorParseException {
URI getEntryURI = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH + "/" + UUID);
URI getEntryURI = uri.resolve(REKOR_ENTRIES_PATH + "/" + UUID);
HttpRequest req =
HttpClients.newRequestFactory(httpParams).buildGetRequest(new GenericUrl(getEntryURI));
req.getHeaders().set("Accept", "application/json");
Expand All @@ -149,7 +139,7 @@ public Optional<RekorEntry> getEntry(String UUID) throws IOException, RekorParse
public List<String> searchEntry(
String email, String hash, String publicKeyFormat, String publicKeyContent)
throws IOException {
URI rekorSearchEndpoint = tlog.getBaseUrl().resolve(REKOR_INDEX_SEARCH_PATH);
URI rekorSearchEndpoint = uri.resolve(REKOR_INDEX_SEARCH_PATH);

HashMap<String, Object> publicKeyParams = null;
if (publicKeyContent != null) {
Expand Down
Loading

0 comments on commit 36a6b2e

Please sign in to comment.