FulcioClientGrpc.java
/*
* Copyright 2022 The Sigstore Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sigstore.fulcio.client;
import static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.SIGNED_CERTIFICATE_DETACHED_SCT;
import com.google.api.client.util.Preconditions;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import dev.sigstore.fulcio.v2.CAGrpc;
import dev.sigstore.fulcio.v2.CertificateChain;
import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest;
import dev.sigstore.fulcio.v2.Credentials;
import dev.sigstore.fulcio.v2.PublicKey;
import dev.sigstore.fulcio.v2.PublicKeyRequest;
import dev.sigstore.http.GrpcChannels;
import dev.sigstore.http.HttpParams;
import dev.sigstore.trustroot.Service;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
/** A client to communicate with a fulcio service instance over gRPC. */
public class FulcioClientGrpc implements FulcioClient {
private final HttpParams httpParams;
private final URI uri;
public static Builder builder() {
return new Builder();
}
private FulcioClientGrpc(HttpParams httpParams, URI uri) {
this.uri = uri;
this.httpParams = httpParams;
}
public static class Builder {
private Service service;
private HttpParams httpParams = HttpParams.builder().build();
private Builder() {}
/** Configure the http properties, see {@link HttpParams}. */
public Builder setHttpParams(HttpParams httpParams) {
this.httpParams = httpParams;
return this;
}
/** Base url of the remote fulcio instance. */
public Builder setService(Service service) {
this.service = service;
return this;
}
public FulcioClientGrpc build() {
Preconditions.checkNotNull(service);
return new FulcioClientGrpc(httpParams, service.getUrl());
}
}
/**
* Request a signing certificate from fulcio.
*
* @param request certificate request parameters
* @return a {@link CertPath} from fulcio
*/
@Override
public CertPath signingCertificate(CertificateRequest request)
throws InterruptedException, CertificateException {
// 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(uri.getAuthority(), httpParams);
try {
var client = CAGrpc.newBlockingStub(channel);
var credentials = Credentials.newBuilder().setOidcIdentityToken(request.getIdToken()).build();
String pemEncodedPublicKey =
"-----BEGIN PUBLIC KEY-----\n"
+ Base64.getEncoder().encodeToString(request.getPublicKey().getEncoded())
+ "\n-----END PUBLIC KEY-----";
var publicKeyRequest =
PublicKeyRequest.newBuilder()
.setPublicKey(
PublicKey.newBuilder()
.setAlgorithm(request.getPublicKeyAlgorithm())
.setContent(pemEncodedPublicKey)
.build())
.setProofOfPossession(ByteString.copyFrom(request.getProofOfPossession()))
.build();
var req =
CreateSigningCertificateRequest.newBuilder()
.setCredentials(credentials)
.setPublicKeyRequest(publicKeyRequest)
.build();
var certs =
client
.withDeadlineAfter(httpParams.getTimeout(), TimeUnit.SECONDS)
.createSigningCertificate(req);
if (certs.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) {
throw new CertificateException("Detached SCTs are not supported");
}
return decodeCerts(certs.getSignedCertificateEmbeddedSct().getChain());
} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
}
@VisibleForTesting
CertPath decodeCerts(CertificateChain certChain) throws CertificateException {
var certificateFactory = CertificateFactory.getInstance("X.509");
var certs = new ArrayList<X509Certificate>();
if (certChain.getCertificatesCount() == 0) {
throw new CertificateParsingException(
"no valid PEM certificates were found in response from Fulcio");
}
for (var cert : certChain.getCertificatesList().asByteStringList()) {
certs.add(
(X509Certificate)
certificateFactory.generateCertificate(new ByteArrayInputStream(cert.toByteArray())));
}
return certificateFactory.generateCertPath(certs);
}
}