FulcioClientHttp.java
/*
* Copyright 2026 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 static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.SIGNED_CERTIFICATE_EMBEDDED_SCT;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.util.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest;
import dev.sigstore.fulcio.v2.PublicKey;
import dev.sigstore.fulcio.v2.PublicKeyRequest;
import dev.sigstore.fulcio.v2.SigningCertificate;
import dev.sigstore.http.HttpClients;
import dev.sigstore.http.HttpParams;
import dev.sigstore.http.URIFormat;
import dev.sigstore.json.ProtoJson;
import dev.sigstore.trustroot.Service;
import java.io.IOException;
import java.net.URI;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.Locale;
/** A client to communicate with a fulcio service instance over HTTP. */
public class FulcioClientHttp implements FulcioClient {
public static final String FULCIO_SIGNING_CERT_PATH = "/api/v2/signingCert";
private final HttpParams httpParams;
private final URI uri;
public static Builder builder() {
return new Builder();
}
private FulcioClientHttp(HttpParams httpParams, URI uri) {
this.uri = uri;
this.httpParams = httpParams;
}
public static class Builder {
private HttpParams httpParams = HttpParams.builder().build();
private Service service;
private Builder() {}
/** Configure the http properties, see {@link HttpParams}. */
public Builder setHttpParams(HttpParams httpParams) {
this.httpParams = httpParams;
return this;
}
/** Service information for a remote fulcio instance. */
public Builder setService(Service service) {
this.service = service;
return this;
}
public FulcioClientHttp build() {
Preconditions.checkNotNull(service);
return new FulcioClientHttp(httpParams, service.getUrl());
}
}
/**
* Request a signing certificate from fulcio over HTTP.
*
* @param request certificate request parameters
* @return a {@link CertPath} from fulcio
*/
@Override
public CertPath signingCertificate(CertificateRequest request) throws CertificateException {
URI endpoint = URIFormat.appendPath(uri, FULCIO_SIGNING_CERT_PATH);
String pemEncodedPublicKey =
"-----BEGIN PUBLIC KEY-----\n"
+ Base64.getEncoder().encodeToString(request.getPublicKey().getEncoded())
+ "\n-----END PUBLIC KEY-----";
var createSigningCertificateRequest =
CreateSigningCertificateRequest.newBuilder()
.setPublicKeyRequest(
PublicKeyRequest.newBuilder()
.setPublicKey(
PublicKey.newBuilder()
.setAlgorithm(request.getPublicKeyAlgorithm())
.setContent(pemEncodedPublicKey)
.build())
.setProofOfPossession(ByteString.copyFrom(request.getProofOfPossession()))
.build())
.build();
String jsonPayload;
try {
jsonPayload = JsonFormat.printer().print(createSigningCertificateRequest);
} catch (InvalidProtocolBufferException e) {
throw new CertificateException("Failed to serialize certificate request", e);
}
String responseJson;
try {
var httpRequest =
HttpClients.newRequestFactory(httpParams)
.buildPostRequest(
new GenericUrl(endpoint),
ByteArrayContent.fromString("application/json", jsonPayload));
httpRequest.getHeaders().set("Accept", "application/json");
httpRequest.getHeaders().set("Content-Type", "application/json");
httpRequest.getHeaders().set("Authorization", "Bearer " + request.getIdToken());
httpRequest.setThrowExceptionOnExecuteError(false);
var resp = httpRequest.execute();
responseJson = resp.parseAsString();
if (resp.getStatusCode() != 200) {
throw new CertificateException(
String.format(
Locale.ROOT, "bad response from fulcio @ '%s' : %s", endpoint, responseJson));
}
} catch (IOException e) {
throw new CertificateException("Failed to request signing certificate from fulcio", e);
}
var signingCertBuilder = SigningCertificate.newBuilder();
try {
ProtoJson.parser().merge(responseJson, signingCertBuilder);
} catch (InvalidProtocolBufferException e) {
throw new CertificateException("Failed to parse signing certificate response from fulcio", e);
}
var signingCert = signingCertBuilder.build();
if (signingCert.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) {
throw new CertificateException("Detached SCTs are not supported");
}
if (signingCert.getCertificateCase() != SIGNED_CERTIFICATE_EMBEDDED_SCT) {
throw new CertificateException("No certificate was found in response from fulcio");
}
return FulcioClient.decodeCerts(signingCert.getSignedCertificateEmbeddedSct().getChain());
}
}