Skip to content

Commit

Permalink
Rework signing initialization
Browse files Browse the repository at this point in the history
Make the signing clients configured with URIs instead of objects
from the trusted_root.json. This code still uses the URIs from
the trusted_root.json to populate those values, but this is a setup
towards using the SigningConfiguration type that is coming in the
future.

Signed-off-by: Appu Goundan <[email protected]>
  • Loading branch information
loosebazooka committed Mar 29, 2024
1 parent 9ec514d commit 03433f6
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 03433f6

Please sign in to comment.