Fulcio Integration in Java: Code Signing Certificate Infrastructure

Fulcio is a free Root Certificate Authority (CA) for code signing in the software supply chain. It issues short-lived certificates for signing container images and other software artifacts. This guide covers comprehensive Java integration with Fulcio for secure code signing.


Dependencies and Setup

1. Maven Dependencies
<properties>
<bouncycastle.version>1.74</bouncycastle.version>
<google-auth.version>1.19.0</google-auth.version>
<okhttp.version>4.11.0</okhttp.version>
<jackson.version>2.15.2</jackson.version>
<jose4j.version>0.9.3</jose4j.version>
</properties>
<dependencies>
<!-- Bouncy Castle for Cryptography -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- JWT and OAuth -->
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>${jose4j.version}</version>
</dependency>
<!-- Google Auth for OIDC -->
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>${google-auth.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
2. Project Structure
fulcio-java/
├── src/main/java/com/example/fulcio/
│   ├── config/
│   ├── client/
│   ├── model/
│   ├── certificate/
│   ├── signing/
│   └── verification/
└── src/test/java/com/example/fulcio/

Core Fulcio Integration

1. Fulcio Client Configuration
package com.example.fulcio.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.security.Security;
import java.time.Duration;
@Configuration
public class FulcioConfig {
private static final Logger logger = LoggerFactory.getLogger(FulcioConfig.class);
@Value("${fulcio.url:https://fulcio.sigstore.dev}")
private String fulcioUrl;
@Value("${fulcio.timeout.seconds:30}")
private int timeoutSeconds;
@Value("${fulcio.oidc.issuer:https://oauth2.sigstore.dev/auth}")
private String oidcIssuer;
@PostConstruct
public void init() {
// Register Bouncy Castle provider
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
logger.info("Bouncy Castle provider registered");
}
}
@Bean
public OkHttpClient fulcioHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
.readTimeout(Duration.ofSeconds(timeoutSeconds))
.writeTimeout(Duration.ofSeconds(timeoutSeconds))
.addInterceptor(chain -> {
logger.debug("Making request to Fulcio: {}", chain.request().url());
return chain.proceed(chain.request());
})
.build();
}
@Bean
public ObjectMapper fulcioObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Configure as needed
return mapper;
}
@Bean
public FulcioClient fulcioClient(OkHttpClient httpClient, 
ObjectMapper objectMapper) {
return new FulcioClient(httpClient, objectMapper, fulcioUrl, oidcIssuer);
}
}
2. Fulcio Client Implementation
package com.example.fulcio.client;
import com.example.fulcio.model.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;
@Component
public class FulcioClient {
private static final Logger logger = LoggerFactory.getLogger(FulcioClient.class);
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private final String fulcioBaseUrl;
private final String oidcIssuer;
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
public FulcioClient(OkHttpClient httpClient, ObjectMapper objectMapper, 
String fulcioBaseUrl, String oidcIssuer) {
this.httpClient = httpClient;
this.objectMapper = objectMapper;
this.fulcioBaseUrl = fulcioBaseUrl;
this.oidcIssuer = oidcIssuer;
}
public SigningCertificateResponse requestSigningCertificate(
String oidcToken, 
PublicKeyRequest publicKeyRequest) throws FulcioException {
try {
// Build the signing certificate request
SigningCertificateRequest request = new SigningCertificateRequest(
publicKeyRequest, 
oidcToken
);
String jsonBody = objectMapper.writeValueAsString(request);
Request httpRequest = new Request.Builder()
.url(fulcioBaseUrl + "/api/v2/signingCert")
.post(RequestBody.create(jsonBody, JSON))
.addHeader("Authorization", "Bearer " + oidcToken)
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
throw new FulcioException("Failed to request signing certificate: " + 
response.code() + " - " + response.message());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, SigningCertificateResponse.class);
}
} catch (IOException e) {
throw new FulcioException("Error requesting signing certificate", e);
}
}
public RootCertificate getRootCertificate() throws FulcioException {
try {
Request request = new Request.Builder()
.url(fulcioBaseUrl + "/api/v2/rootCert")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new FulcioException("Failed to get root certificate: " + 
response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, RootCertificate.class);
}
} catch (IOException e) {
throw new FulcioException("Error getting root certificate", e);
}
}
public TrustBundle getTrustBundle() throws FulcioException {
try {
Request request = new Request.Builder()
.url(fulcioBaseUrl + "/api/v2/trustBundle")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new FulcioException("Failed to get trust bundle: " + 
response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, TrustBundle.class);
}
} catch (IOException e) {
throw new FulcioException("Error getting trust bundle", e);
}
}
public boolean verifyCertificateChain(X509Certificate leafCertificate) throws FulcioException {
try {
// Encode certificate
String certPem = encodeCertificateToPem(leafCertificate);
VerificationRequest request = new VerificationRequest(certPem);
String jsonBody = objectMapper.writeValueAsString(request);
Request httpRequest = new Request.Builder()
.url(fulcioBaseUrl + "/api/v2/verify")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
return response.isSuccessful();
}
} catch (CertificateEncodingException | IOException e) {
throw new FulcioException("Error verifying certificate chain", e);
}
}
public HealthStatus getHealth() throws FulcioException {
try {
Request request = new Request.Builder()
.url(fulcioBaseUrl + "/healthz")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
boolean healthy = response.isSuccessful();
return new HealthStatus(healthy, "Fulcio CA");
}
} catch (IOException e) {
throw new FulcioException("Error checking health", e);
}
}
private String encodeCertificateToPem(X509Certificate certificate) 
throws CertificateEncodingException {
byte[] certBytes = certificate.getEncoded();
String base64Cert = Base64.getEncoder().encodeToString(certBytes);
return "-----BEGIN CERTIFICATE-----\n" +
base64Cert.replaceAll("(.{64})", "$1\n") +
"\n-----END CERTIFICATE-----";
}
}
3. Data Models
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SigningCertificateRequest {
@JsonProperty("publicKey")
private final PublicKeyRequest publicKey;
@JsonProperty("signedEmailAddress")
private final String signedEmailAddress;
@JsonProperty("proofOfPossession")
private final String proofOfPossession;
public SigningCertificateRequest(PublicKeyRequest publicKey, String oidcToken) {
this.publicKey = publicKey;
this.signedEmailAddress = oidcToken;
this.proofOfPossession = ""; // This would be populated with a signed proof
}
// Getters
public PublicKeyRequest getPublicKey() { return publicKey; }
public String getSignedEmailAddress() { return signedEmailAddress; }
public String getProofOfPossession() { return proofOfPossession; }
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PublicKeyRequest {
@JsonProperty("algorithm")
private final String algorithm;
@JsonProperty("content")
private final String content; // Base64 encoded public key
public PublicKeyRequest(String algorithm, String base64PublicKey) {
this.algorithm = algorithm;
this.content = base64PublicKey;
}
// Getters
public String getAlgorithm() { return algorithm; }
public String getContent() { return content; }
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SigningCertificateResponse {
@JsonProperty("signedCertificate")
private CertificateChain signedCertificate;
@JsonProperty("signedCertificateTimestamp")
private String signedCertificateTimestamp;
@JsonProperty("certificateChain")
private List<String> certificateChain;
// Getters and setters
public CertificateChain getSignedCertificate() { return signedCertificate; }
public void setSignedCertificate(CertificateChain signedCertificate) { 
this.signedCertificate = signedCertificate; 
}
public String getSignedCertificateTimestamp() { return signedCertificateTimestamp; }
public void setSignedCertificateTimestamp(String signedCertificateTimestamp) { 
this.signedCertificateTimestamp = signedCertificateTimestamp; 
}
public List<String> getCertificateChain() { return certificateChain; }
public void setCertificateChain(List<String> certificateChain) { 
this.certificateChain = certificateChain; 
}
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class CertificateChain {
@JsonProperty("certificate")
private String certificate; // PEM encoded
@JsonProperty("chain")
private String chain; // PEM encoded chain
// Getters and setters
public String getCertificate() { return certificate; }
public void setCertificate(String certificate) { this.certificate = certificate; }
public String getChain() { return chain; }
public void setChain(String chain) { this.chain = chain; }
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RootCertificate {
@JsonProperty("certificate")
private String certificate;
@JsonProperty("algorithm")
private String algorithm;
@JsonProperty("validity")
private ValidityPeriod validity;
// Getters and setters
public String getCertificate() { return certificate; }
public void setCertificate(String certificate) { this.certificate = certificate; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
public ValidityPeriod getValidity() { return validity; }
public void setValidity(ValidityPeriod validity) { this.validity = validity; }
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ValidityPeriod {
@JsonProperty("notBefore")
private String notBefore;
@JsonProperty("notAfter")
private String notAfter;
// Getters and setters
public String getNotBefore() { return notBefore; }
public void setNotBefore(String notBefore) { this.notBefore = notBefore; }
public String getNotAfter() { return notAfter; }
public void setNotAfter(String notAfter) { this.notAfter = notAfter; }
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class TrustBundle {
@JsonProperty("certificates")
private List<String> certificates;
@JsonProperty("timestamp")
private String timestamp;
// Getters and setters
public List<String> getCertificates() { return certificates; }
public void setCertificates(List<String> certificates) { this.certificates = certificates; }
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
}
package com.example.fulcio.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class VerificationRequest {
@JsonProperty("certificate")
private final String certificate;
public VerificationRequest(String certificatePem) {
this.certificate = certificatePem;
}
// Getters
public String getCertificate() { return certificate; }
}
package com.example.fulcio.model;
public class HealthStatus {
private final boolean healthy;
private final String service;
private final long timestamp;
public HealthStatus(boolean healthy, String service) {
this.healthy = healthy;
this.service = service;
this.timestamp = System.currentTimeMillis();
}
// Getters
public boolean isHealthy() { return healthy; }
public String getService() { return service; }
public long getTimestamp() { return timestamp; }
}
package com.example.fulcio.model;
public class FulcioException extends Exception {
public FulcioException(String message) {
super(message);
}
public FulcioException(String message, Throwable cause) {
super(message, cause);
}
}

Cryptography and Key Management

1. Key Pair Generator
package com.example.fulcio.certificate;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;
public class KeyManager {
private static final Logger logger = LoggerFactory.getLogger(KeyManager.class);
private static final String EC_CURVE = "secp256r1"; // P-256
private static final String SIGNATURE_ALGORITHM = "SHA256withECDSA";
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException, 
NoSuchProviderException, 
InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec(EC_CURVE);
keyGen.initialize(ecSpec, new SecureRandom());
return keyGen.generateKeyPair();
}
public static String encodePublicKeyToPem(PublicKey publicKey) {
byte[] encoded = publicKey.getEncoded();
String base64 = Base64.toBase64String(encoded);
return "-----BEGIN PUBLIC KEY-----\n" +
base64.replaceAll("(.{64})", "$1\n") +
"\n-----END PUBLIC KEY-----";
}
public static String encodePrivateKeyToPem(PrivateKey privateKey) {
byte[] encoded = privateKey.getEncoded();
String base64 = Base64.toBase64String(encoded);
return "-----BEGIN PRIVATE KEY-----\n" +
base64.replaceAll("(.{64})", "$1\n") +
"\n-----END PRIVATE KEY-----";
}
public static PublicKey decodePublicKeyFromPem(String pem) throws Exception {
String base64 = pem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.decode(base64);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
return keyFactory.generatePublic(keySpec);
}
public static PrivateKey decodePrivateKeyFromPem(String pem) throws Exception {
String base64 = pem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.decode(base64);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return keyFactory.generatePrivate(keySpec);
}
public static String signData(PrivateKey privateKey, byte[] data) 
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data);
byte[] signed = signature.sign();
return Base64.toBase64String(signed);
}
public static boolean verifySignature(PublicKey publicKey, byte[] data, String base64Signature) 
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicKey);
signature.update(data);
byte[] signatureBytes = Base64.decode(base64Signature);
return signature.verify(signatureBytes);
}
public static void saveKeyPairToFile(KeyPair keyPair, String basePath) throws IOException {
// Save public key
String publicKeyPem = encodePublicKeyToPem(keyPair.getPublic());
try (FileWriter writer = new FileWriter(basePath + ".pub")) {
writer.write(publicKeyPem);
}
// Save private key
String privateKeyPem = encodePrivateKeyToPem(keyPair.getPrivate());
try (FileWriter writer = new FileWriter(basePath + ".key")) {
writer.write(privateKeyPem);
}
logger.info("Key pair saved to: {}.pub and {}.key", basePath, basePath);
}
public static KeyPair loadKeyPairFromFile(String basePath) throws Exception {
// Load public key
String publicKeyPem;
try (BufferedReader reader = new BufferedReader(new FileReader(basePath + ".pub"))) {
publicKeyPem = reader.lines().collect(java.util.stream.Collectors.joining("\n"));
}
// Load private key
String privateKeyPem;
try (BufferedReader reader = new BufferedReader(new FileReader(basePath + ".key"))) {
privateKeyPem = reader.lines().collect(java.util.stream.Collectors.joining("\n"));
}
PublicKey publicKey = decodePublicKeyFromPem(publicKeyPem);
PrivateKey privateKey = decodePrivateKeyFromPem(privateKeyPem);
return new KeyPair(publicKey, privateKey);
}
}
2. Certificate Utilities
package com.example.fulcio.certificate;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.util.*;
public class CertificateUtils {
private static final Logger logger = LoggerFactory.getLogger(CertificateUtils.class);
public static X509Certificate parseCertificate(String pem) throws CertificateException {
pem = pem.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s", "");
byte[] certBytes = Base64.decode(pem);
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
}
public static List<X509Certificate> parseCertificateChain(String chainPem) 
throws CertificateException {
List<X509Certificate> certificates = new ArrayList<>();
String[] certs = chainPem.split("-----END CERTIFICATE-----");
for (String cert : certs) {
if (cert.trim().isEmpty()) continue;
String fullCert = cert + "-----END CERTIFICATE-----";
certificates.add(parseCertificate(fullCert));
}
return certificates;
}
public static String encodeCertificateToPem(X509Certificate certificate) 
throws CertificateEncodingException {
byte[] certBytes = certificate.getEncoded();
String base64 = Base64.toBase64String(certBytes);
return "-----BEGIN CERTIFICATE-----\n" +
base64.replaceAll("(.{64})", "$1\n") +
"\n-----END CERTIFICATE-----";
}
public static String encodeCertificateChainToPem(List<X509Certificate> chain) 
throws CertificateEncodingException {
StringBuilder builder = new StringBuilder();
for (X509Certificate cert : chain) {
builder.append(encodeCertificateToPem(cert)).append("\n");
}
return builder.toString();
}
public static boolean verifyCertificateChain(X509Certificate leafCert, 
List<X509Certificate> intermediateCerts,
X509Certificate rootCert) {
try {
// Create certificate chain
List<X509Certificate> certChain = new ArrayList<>();
certChain.add(leafCert);
certChain.addAll(intermediateCerts);
certChain.add(rootCert);
// Create certificate path
CertificateFactory factory = CertificateFactory.getInstance("X.509");
CertPath certPath = factory.generateCertPath(certChain);
// Set up trust anchor
Set<TrustAnchor> trustAnchors = new HashSet<>();
trustAnchors.add(new TrustAnchor(rootCert, null));
// Create PKIX parameters
PKIXParameters params = new PKIXParameters(trustAnchors);
params.setRevocationEnabled(false); // Fulcio certificates are short-lived
// Validate certificate path
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, params);
return true;
} catch (Exception e) {
logger.error("Certificate chain validation failed", e);
return false;
}
}
public static boolean isCertificateValidForCodeSigning(X509Certificate certificate) {
try {
// Check extended key usage
List<String> extendedKeyUsage = certificate.getExtendedKeyUsage();
if (extendedKeyUsage == null || 
!extendedKeyUsage.contains(KeyPurposeId.id_kp_codeSigning.getId())) {
return false;
}
// Check validity period (Fulcio certificates are short-lived)
Date now = new Date();
if (now.before(certificate.getNotBefore()) || 
now.after(certificate.getNotAfter())) {
return false;
}
// Check key usage
boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage == null || !keyUsage[0]) { // digitalSignature
return false;
}
return true;
} catch (Exception e) {
logger.error("Error checking certificate validity", e);
return false;
}
}
public static Map<String, Object> extractCertificateDetails(X509Certificate certificate) {
Map<String, Object> details = new HashMap<>();
try {
// Basic details
details.put("subject", certificate.getSubjectX500Principal().getName());
details.put("issuer", certificate.getIssuerX500Principal().getName());
details.put("serialNumber", certificate.getSerialNumber().toString(16));
details.put("notBefore", certificate.getNotBefore());
details.put("notAfter", certificate.getNotAfter());
// Key details
details.put("publicKeyAlgorithm", certificate.getPublicKey().getAlgorithm());
details.put("signatureAlgorithm", certificate.getSigAlgName());
// Extended details
details.put("keyUsage", Arrays.toString(certificate.getKeyUsage()));
List<String> extendedKeyUsage = certificate.getExtendedKeyUsage();
if (extendedKeyUsage != null) {
details.put("extendedKeyUsage", extendedKeyUsage);
}
// Subject Alternative Names
Collection<List<?>> sans = certificate.getSubjectAlternativeNames();
if (sans != null) {
List<String> sanList = new ArrayList<>();
for (List<?> san : sans) {
if (san.size() >= 2) {
sanList.add(san.get(1).toString());
}
}
details.put("subjectAlternativeNames", sanList);
}
} catch (Exception e) {
logger.error("Error extracting certificate details", e);
}
return details;
}
}

OIDC Integration

1. OIDC Token Provider
package com.example.fulcio.oidc;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class OidcTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(OidcTokenProvider.class);
@Value("${oidc.issuer:https://oauth2.sigstore.dev/auth}")
private String issuer;
@Value("${oidc.client.id:}")
private String clientId;
@Value("${oidc.client.secret:}")
private String clientSecret;
@Value("${oidc.redirect.uri:}")
private String redirectUri;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
public OidcTokenProvider() {
this.httpClient = new OkHttpClient();
this.objectMapper = new ObjectMapper();
}
public String getDeviceCodeToken() throws IOException {
// Request device code
FormBody formBody = new FormBody.Builder()
.add("client_id", clientId)
.add("scope", "openid email")
.build();
Request request = new Request.Builder()
.url(issuer + "/device/code")
.post(formBody)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed to get device code: " + response.code());
}
String responseBody = response.body().string();
Map<String, String> result = objectMapper.readValue(
responseBody, 
new com.fasterxml.jackson.core.type.TypeReference<Map<String, String>>() {}
);
String deviceCode = result.get("device_code");
String userCode = result.get("user_code");
String verificationUri = result.get("verification_uri");
logger.info("Please visit {} and enter code: {}", verificationUri, userCode);
// Poll for token
return pollForToken(deviceCode);
}
}
private String pollForToken(String deviceCode) throws IOException {
FormBody formBody = new FormBody.Builder()
.add("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
.add("device_code", deviceCode)
.add("client_id", clientId)
.build();
for (int i = 0; i < 30; i++) { // Try for 5 minutes
Request request = new Request.Builder()
.url(issuer + "/token")
.post(formBody)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() == 200) {
String responseBody = response.body().string();
Map<String, String> result = objectMapper.readValue(
responseBody, 
new com.fasterxml.jackson.core.type.TypeReference<Map<String, String>>() {}
);
return result.get("id_token");
} else if (response.code() == 400) {
// Still pending, wait and retry
Thread.sleep(10000);
} else {
throw new IOException("Failed to get token: " + response.code());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Token polling interrupted", e);
}
}
throw new IOException("Token request timed out");
}
public String createSelfSignedJwt(KeyPair keyPair, String email) throws Exception {
Instant now = Instant.now();
Instant expiry = now.plusSeconds(300); // 5 minutes
Map<String, Object> headers = new HashMap<>();
headers.put("alg", "ES256");
headers.put("typ", "JWT");
Algorithm algorithm = Algorithm.ECDSA256(
(ECPublicKey) keyPair.getPublic(), 
(ECPrivateKey) keyPair.getPrivate()
);
return JWT.create()
.withHeader(headers)
.withIssuer("self-signed")
.withSubject(email)
.withAudience(issuer)
.withIssuedAt(Date.from(now))
.withExpiresAt(Date.from(expiry))
.withClaim("email", email)
.sign(algorithm);
}
public DecodedJWT decodeAndVerifyToken(String token) {
return JWT.decode(token);
}
public Map<String, Object> extractTokenClaims(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaims();
}
public boolean validateToken(String token) {
try {
DecodedJWT jwt = decodeAndVerifyToken(token);
// Check expiration
if (jwt.getExpiresAt().before(new Date())) {
return false;
}
// Check issuer
if (!issuer.equals(jwt.getIssuer())) {
return false;
}
// Check audience
if (!jwt.getAudience().contains(clientId)) {
return false;
}
return true;
} catch (Exception e) {
logger.error("Token validation failed", e);
return false;
}
}
}

Code Signing Service

1. Fulcio Signing Service
package com.example.fulcio.signing;
import com.example.fulcio.certificate.CertificateUtils;
import com.example.fulcio.certificate.KeyManager;
import com.example.fulcio.client.FulcioClient;
import com.example.fulcio.model.*;
import com.example.fulcio.oidc.OidcTokenProvider;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
@Service
public class FulcioSigningService {
private static final Logger logger = LoggerFactory.getLogger(FulcioSigningService.class);
private final FulcioClient fulcioClient;
private final OidcTokenProvider oidcTokenProvider;
public FulcioSigningService(FulcioClient fulcioClient, 
OidcTokenProvider oidcTokenProvider) {
this.fulcioClient = fulcioClient;
this.oidcTokenProvider = oidcTokenProvider;
}
public CertificateBundle requestSigningCertificate(String email) throws Exception {
logger.info("Requesting signing certificate for: {}", email);
// Generate key pair
KeyPair keyPair = KeyManager.generateKeyPair();
logger.debug("Generated key pair with algorithm: {}", 
keyPair.getPublic().getAlgorithm());
// Get OIDC token (using device code flow)
String oidcToken = oidcTokenProvider.getDeviceCodeToken();
logger.debug("Obtained OIDC token");
// Prepare public key request
String base64PublicKey = Base64.toBase64String(keyPair.getPublic().getEncoded());
PublicKeyRequest publicKeyRequest = new PublicKeyRequest(
"ecdsa", 
base64PublicKey
);
// Request certificate from Fulcio
SigningCertificateResponse response = fulcioClient.requestSigningCertificate(
oidcToken, 
publicKeyRequest
);
logger.info("Successfully obtained signing certificate from Fulcio");
// Parse certificates
CertificateChain certificateChain = response.getSignedCertificate();
X509Certificate signingCert = CertificateUtils.parseCertificate(
certificateChain.getCertificate()
);
List<X509Certificate> chain = CertificateUtils.parseCertificateChain(
certificateChain.getChain()
);
// Create certificate bundle
CertificateBundle bundle = new CertificateBundle();
bundle.setKeyPair(keyPair);
bundle.setSigningCertificate(signingCert);
bundle.setCertificateChain(chain);
bundle.setSignedCertificateTimestamp(response.getSignedCertificateTimestamp());
bundle.setEmail(email);
return bundle;
}
public SigningResult signArtifact(byte[] artifact, CertificateBundle certificateBundle) 
throws Exception {
logger.info("Signing artifact with Fulcio certificate");
// Verify certificate is valid for code signing
if (!CertificateUtils.isCertificateValidForCodeSigning(
certificateBundle.getSigningCertificate())) {
throw new IllegalStateException("Certificate not valid for code signing");
}
// Create signature
String signature = KeyManager.signData(
certificateBundle.getKeyPair().getPrivate(), 
artifact
);
// Create signing result
SigningResult result = new SigningResult();
result.setArtifact(artifact);
result.setSignature(signature);
result.setCertificateBundle(certificateBundle);
result.setTimestamp(System.currentTimeMillis());
// Add certificate details
Map<String, Object> certDetails = CertificateUtils.extractCertificateDetails(
certificateBundle.getSigningCertificate()
);
result.setCertificateDetails(certDetails);
logger.debug("Artifact signed successfully");
return result;
}
public boolean verifySignature(SigningResult signingResult) throws Exception {
X509Certificate cert = signingResult.getCertificateBundle().getSigningCertificate();
byte[] artifact = signingResult.getArtifact();
String signature = signingResult.getSignature();
// Verify certificate chain
List<X509Certificate> chain = signingResult.getCertificateBundle().getCertificateChain();
RootCertificate rootCert = fulcioClient.getRootCertificate();
X509Certificate rootCertificate = CertificateUtils.parseCertificate(
rootCert.getCertificate()
);
if (!CertificateUtils.verifyCertificateChain(cert, chain, rootCertificate)) {
logger.error("Certificate chain verification failed");
return false;
}
// Verify certificate validity for code signing
if (!CertificateUtils.isCertificateValidForCodeSigning(cert)) {
logger.error("Certificate not valid for code signing");
return false;
}
// Verify signature
PublicKey publicKey = cert.getPublicKey();
boolean signatureValid = KeyManager.verifySignature(
publicKey, 
artifact, 
signature
);
if (!signatureValid) {
logger.error("Signature verification failed");
return false;
}
logger.debug("Signature verification successful");
return true;
}
public CertificateValidationResult validateCertificate(X509Certificate certificate) {
CertificateValidationResult result = new CertificateValidationResult();
result.setCertificate(certificate);
result.setTimestamp(System.currentTimeMillis());
try {
// Check basic validity
if (!CertificateUtils.isCertificateValidForCodeSigning(certificate)) {
result.setValid(false);
result.setReason("Certificate not valid for code signing");
return result;
}
// Verify with Fulcio
boolean chainValid = fulcioClient.verifyCertificateChain(certificate);
if (!chainValid) {
result.setValid(false);
result.setReason("Certificate chain verification failed");
return result;
}
// Extract details
Map<String, Object> details = CertificateUtils.extractCertificateDetails(certificate);
result.setCertificateDetails(details);
result.setValid(true);
result.setReason("Certificate is valid");
} catch (Exception e) {
result.setValid(false);
result.setReason("Validation error: " + e.getMessage());
}
return result;
}
}
2. Supporting Models
package com.example.fulcio.signing;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
public class CertificateBundle {
private KeyPair keyPair;
private X509Certificate signingCertificate;
private List<X509Certificate> certificateChain;
private String signedCertificateTimestamp;
private String email;
// Getters and setters
public KeyPair getKeyPair() { return keyPair; }
public void setKeyPair(KeyPair keyPair) { this.keyPair = keyPair; }
public X509Certificate getSigningCertificate() { return signingCertificate; }
public void setSigningCertificate(X509Certificate signingCertificate) { 
this.signingCertificate = signingCertificate; 
}
public List<X509Certificate> getCertificateChain() { return certificateChain; }
public void setCertificateChain(List<X509Certificate> certificateChain) { 
this.certificateChain = certificateChain; 
}
public String getSignedCertificateTimestamp() { return signedCertificateTimestamp; }
public void setSignedCertificateTimestamp(String signedCertificateTimestamp) { 
this.signedCertificateTimestamp = signedCertificateTimestamp; 
}
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
package com.example.fulcio.signing;
import java.util.Map;
public class SigningResult {
private byte[] artifact;
private String signature;
private CertificateBundle certificateBundle;
private long timestamp;
private Map<String, Object> certificateDetails;
// Getters and setters
public byte[] getArtifact() { return artifact; }
public void setArtifact(byte[] artifact) { this.artifact = artifact; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public CertificateBundle getCertificateBundle() { return certificateBundle; }
public void setCertificateBundle(CertificateBundle certificateBundle) { 
this.certificateBundle = certificateBundle; 
}
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public Map<String, Object> getCertificateDetails() { return certificateDetails; }
public void setCertificateDetails(Map<String, Object> certificateDetails) { 
this.certificateDetails = certificateDetails; 
}
}
package com.example.fulcio.signing;
import java.security.cert.X509Certificate;
import java.util.Map;
public class CertificateValidationResult {
private X509Certificate certificate;
private boolean valid;
private String reason;
private long timestamp;
private Map<String, Object> certificateDetails;
// Getters and setters
public X509Certificate getCertificate() { return certificate; }
public void setCertificate(X509Certificate certificate) { this.certificate = certificate; }
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public Map<String, Object> getCertificateDetails() { return certificateDetails; }
public void setCertificateDetails(Map<String, Object> certificateDetails) { 
this.certificateDetails = certificateDetails; 
}
}

REST API Controllers

1. Certificate Management Controller
package com.example.fulcio.controller;
import com.example.fulcio.certificate.CertificateUtils;
import com.example.fulcio.certificate.KeyManager;
import com.example.fulcio.client.FulcioClient;
import com.example.fulcio.model.FulcioException;
import com.example.fulcio.model.HealthStatus;
import com.example.fulcio.model.RootCertificate;
import com.example.fulcio.model.TrustBundle;
import com.example.fulcio.signing.CertificateBundle;
import com.example.fulcio.signing.FulcioSigningService;
import com.example.fulcio.signing.SigningResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/fulcio")
public class FulcioController {
private static final Logger logger = LoggerFactory.getLogger(FulcioController.class);
private final FulcioClient fulcioClient;
private final FulcioSigningService signingService;
public FulcioController(FulcioClient fulcioClient, 
FulcioSigningService signingService) {
this.fulcioClient = fulcioClient;
this.signingService = signingService;
}
@PostMapping("/certificate/request")
public ResponseEntity<CertificateResponse> requestCertificate(
@RequestBody CertificateRequest request) {
try {
logger.info("Certificate request for email: {}", request.getEmail());
CertificateBundle bundle = signingService.requestSigningCertificate(
request.getEmail()
);
String certPem = CertificateUtils.encodeCertificateToPem(
bundle.getSigningCertificate()
);
String chainPem = CertificateUtils.encodeCertificateChainToPem(
bundle.getCertificateChain()
);
CertificateResponse response = new CertificateResponse();
response.setCertificate(certPem);
response.setCertificateChain(chainPem);
response.setEmail(request.getEmail());
response.setSignedCertificateTimestamp(bundle.getSignedCertificateTimestamp());
response.setSuccess(true);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to request certificate", e);
return ResponseEntity.badRequest().body(
new CertificateResponse(false, "Failed: " + e.getMessage())
);
}
}
@PostMapping("/sign")
public ResponseEntity<SigningResponse> signArtifact(
@RequestBody SigningRequest request) {
try {
// Decode artifact from base64
byte[] artifact = Base64.getDecoder().decode(request.getArtifactBase64());
// Get or create certificate bundle
CertificateBundle bundle;
if (request.getCertificatePem() != null) {
// Use existing certificate
X509Certificate cert = CertificateUtils.parseCertificate(
request.getCertificatePem()
);
// In real implementation, you'd need the private key too
throw new UnsupportedOperationException(
"Using existing certificate not yet implemented");
} else {
// Request new certificate
bundle = signingService.requestSigningCertificate(request.getEmail());
}
// Sign artifact
SigningResult signingResult = signingService.signArtifact(artifact, bundle);
// Create response
SigningResponse response = new SigningResponse();
response.setSignature(signingResult.getSignature());
response.setCertificate(CertificateUtils.encodeCertificateToPem(
signingResult.getCertificateBundle().getSigningCertificate()
));
response.setCertificateChain(CertificateUtils.encodeCertificateChainToPem(
signingResult.getCertificateBundle().getCertificateChain()
));
response.setTimestamp(signingResult.getTimestamp());
response.setSuccess(true);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to sign artifact", e);
return ResponseEntity.badRequest().body(
new SigningResponse(false, "Failed: " + e.getMessage())
);
}
}
@PostMapping("/verify")
public ResponseEntity<VerificationResponse> verifySignature(
@RequestBody VerificationRequest request) {
try {
byte[] artifact = Base64.getDecoder().decode(request.getArtifactBase64());
X509Certificate cert = CertificateUtils.parseCertificate(request.getCertificatePem());
// TODO: Implement signature verification
// This would require recreating the SigningResult
VerificationResponse response = new VerificationResponse();
response.setValid(true); // Placeholder
response.setMessage("Verification not fully implemented");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to verify signature", e);
return ResponseEntity.badRequest().body(
new VerificationResponse(false, "Failed: " + e.getMessage())
);
}
}
@GetMapping("/root-certificate")
public ResponseEntity<RootCertificate> getRootCertificate() {
try {
RootCertificate rootCert = fulcioClient.getRootCertificate();
return ResponseEntity.ok(rootCert);
} catch (FulcioException e) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/trust-bundle")
public ResponseEntity<TrustBundle> getTrustBundle() {
try {
TrustBundle trustBundle = fulcioClient.getTrustBundle();
return ResponseEntity.ok(trustBundle);
} catch (FulcioException e) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/health")
public ResponseEntity<HealthStatus> getHealth() {
try {
HealthStatus health = fulcioClient.getHealth();
return ResponseEntity.ok(health);
} catch (FulcioException e) {
return ResponseEntity.ok(new HealthStatus(false, "Fulcio CA - Error: " + e.getMessage()));
}
}
@PostMapping("/key/generate")
public ResponseEntity<KeyPairResponse> generateKeyPair() {
try {
KeyPair keyPair = KeyManager.generateKeyPair();
KeyPairResponse response = new KeyPairResponse();
response.setPublicKey(KeyManager.encodePublicKeyToPem(keyPair.getPublic()));
response.setPrivateKey(KeyManager.encodePrivateKeyToPem(keyPair.getPrivate()));
response.setAlgorithm(keyPair.getPublic().getAlgorithm());
response.setSuccess(true);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to generate key pair", e);
return ResponseEntity.badRequest().body(
new KeyPairResponse(false, "Failed: " + e.getMessage())
);
}
}
}
// Request/Response DTOs
class CertificateRequest {
private String email;
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
class CertificateResponse {
private boolean success;
private String message;
private String certificate;
private String certificateChain;
private String email;
private String signedCertificateTimestamp;
public CertificateResponse() {}
public CertificateResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getCertificate() { return certificate; }
public void setCertificate(String certificate) { this.certificate = certificate; }
public String getCertificateChain() { return certificateChain; }
public void setCertificateChain(String certificateChain) { this.certificateChain = certificateChain; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getSignedCertificateTimestamp() { return signedCertificateTimestamp; }
public void setSignedCertificateTimestamp(String signedCertificateTimestamp) { 
this.signedCertificateTimestamp = signedCertificateTimestamp; 
}
}
class SigningRequest {
private String email;
private String artifactBase64;
private String certificatePem;
// Getters and setters
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getArtifactBase64() { return artifactBase64; }
public void setArtifactBase64(String artifactBase64) { this.artifactBase64 = artifactBase64; }
public String getCertificatePem() { return certificatePem; }
public void setCertificatePem(String certificatePem) { this.certificatePem = certificatePem; }
}
class SigningResponse {
private boolean success;
private String message;
private String signature;
private String certificate;
private String certificateChain;
private long timestamp;
public SigningResponse() {}
public SigningResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public String getCertificate() { return certificate; }
public void setCertificate(String certificate) { this.certificate = certificate; }
public String getCertificateChain() { return certificateChain; }
public void setCertificateChain(String certificateChain) { this.certificateChain = certificateChain; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
class VerificationRequest {
private String artifactBase64;
private String signature;
private String certificatePem;
// Getters and setters
public String getArtifactBase64() { return artifactBase64; }
public void setArtifactBase64(String artifactBase64) { this.artifactBase64 = artifactBase64; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public String getCertificatePem() { return certificatePem; }
public void setCertificatePem(String certificatePem) { this.certificatePem = certificatePem; }
}
class VerificationResponse {
private boolean valid;
private String message;
public VerificationResponse() {}
public VerificationResponse(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
// Getters and setters
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
class KeyPairResponse {
private boolean success;
private String message;
private String publicKey;
private String privateKey;
private String algorithm;
public KeyPairResponse() {}
public KeyPairResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getPublicKey() { return publicKey; }
public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
}

Container Image Signing

1. Cosign Integration
package com.example.fulcio.cosign;
import com.example.fulcio.signing.CertificateBundle;
import com.example.fulcio.signing.SigningResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Base64;
@Component
public class CosignSigner {
private static final Logger logger = LoggerFactory.getLogger(CosignSigner.class);
public CosignSignature signContainerImage(String imageRef, CertificateBundle certificateBundle) 
throws Exception {
logger.info("Signing container image: {}", imageRef);
// Create temporary directory for cosign
Path tempDir = Files.createTempDirectory("cosign");
try {
// Save certificate and key to files
Path keyFile = tempDir.resolve("cosign.key");
Path certFile = tempDir.resolve("cosign.crt");
Path chainFile = tempDir.resolve("cosign-chain.crt");
// Write private key
try (Writer writer = Files.newBufferedWriter(keyFile)) {
writer.write(com.example.fulcio.certificate.KeyManager.encodePrivateKeyToPem(
certificateBundle.getKeyPair().getPrivate()
));
}
// Write certificate
try (Writer writer = Files.newBufferedWriter(certFile)) {
writer.write(com.example.fulcio.certificate.CertificateUtils.encodeCertificateToPem(
certificateBundle.getSigningCertificate()
));
}
// Write certificate chain
try (Writer writer = Files.newBufferedWriter(chainFile)) {
writer.write(com.example.fulcio.certificate.CertificateUtils.encodeCertificateChainToPem(
certificateBundle.getCertificateChain()
));
}
// Execute cosign sign command
ProcessBuilder pb = new ProcessBuilder(
"cosign", "sign",
"--key", keyFile.toString(),
imageRef
);
pb.redirectErrorStream(true);
Process process = pb.start();
// Read output
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Cosign failed: " + output.toString());
}
logger.info("Container image signed successfully: {}", imageRef);
// Parse cosign output to extract signature
CosignSignature signature = new CosignSignature();
signature.setImageRef(imageRef);
signature.setSignature(output.toString()); // This would need proper parsing
signature.setCertificateBundle(certificateBundle);
return signature;
} finally {
// Clean up temporary directory
deleteDirectory(tempDir.toFile());
}
}
public boolean verifyContainerImage(String imageRef, String certificatePem) 
throws Exception {
logger.info("Verifying container image: {}", imageRef);
Path tempDir = Files.createTempDirectory("cosign-verify");
try {
// Save certificate to file
Path certFile = tempDir.resolve("verification.crt");
try (Writer writer = Files.newBufferedWriter(certFile)) {
writer.write(certificatePem);
}
// Execute cosign verify command
ProcessBuilder pb = new ProcessBuilder(
"cosign", "verify",
"--certificate", certFile.toString(),
imageRef
);
pb.redirectErrorStream(true);
Process process = pb.start();
// Read output
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
int exitCode = process.waitFor();
boolean verified = exitCode == 0;
if (verified) {
logger.info("Container image verified successfully: {}", imageRef);
} else {
logger.warn("Container image verification failed: {}", imageRef);
logger.warn("Cosign output: {}", output.toString());
}
return verified;
} finally {
// Clean up temporary directory
deleteDirectory(tempDir.toFile());
}
}
private void deleteDirectory(File directory) {
if (directory.exists()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
}
directory.delete();
}
}
}
package com.example.fulcio.cosign;
import com.example.fulcio.signing.CertificateBundle;
public class CosignSignature {
private String imageRef;
private String signature;
private CertificateBundle certificateBundle;
// Getters and setters
public String getImageRef() { return imageRef; }
public void setImageRef(String imageRef) { this.imageRef = imageRef; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public CertificateBundle getCertificateBundle() { return certificateBundle; }
public void setCertificateBundle(CertificateBundle certificateBundle) { 
this.certificateBundle = certificateBundle; 
}
}

Configuration

1. Application Properties
# application.yml
fulcio:
url: ${FULCIO_URL:https://fulcio.sigstore.dev}
timeout:
seconds: 30
oidc:
issuer: ${OIDC_ISSUER:https://oauth2.sigstore.dev/auth}
client:
id: ${OIDC_CLIENT_ID:}
secret: ${OIDC_CLIENT_SECRET:}
redirect:
uri: ${OIDC_REDIRECT_URI:}
key:
algorithm: ECDSA
curve: secp256r1
cosign:
binary:
path: ${COSIGN_PATH:/usr/local/bin/cosign}
logging:
level:
com.example.fulcio: INFO
2. Security Configuration
package com.example.fulcio.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/fulcio/**").permitAll() // Adjust based on requirements
.anyRequest().authenticated();
}
}

Best Practices

  1. Key Management: Always store private keys securely, preferably in hardware security modules (HSMs) or key management systems
  2. Certificate Rotation: Implement automatic certificate rotation for long-running services
  3. Audit Logging: Log all certificate requests and signing operations
  4. Access Control: Implement proper authorization for certificate requests
  5. Certificate Validation: Always validate certificates before use
  6. Error Handling: Implement comprehensive error handling and user feedback
// Example of secure key storage
public interface SecureKeyStorage {
void storePrivateKey(String keyId, PrivateKey key);
PrivateKey retrievePrivateKey(String keyId);
void deletePrivateKey(String keyId);
}

Conclusion

Fulcio integration in Java provides:

  • Secure code signing with short-lived certificates
  • OIDC-based authentication for certificate requests
  • Complete certificate lifecycle management
  • Container image signing integration with Cosign
  • Comprehensive verification of signatures and certificates
  • REST API for easy integration

By implementing this comprehensive Fulcio integration, you can establish a secure software supply chain with code signing capabilities that integrate seamlessly with modern container ecosystems and CI/CD pipelines.

Advanced Java Container Security, Sandboxing & Trusted Runtime Environments

https://macronepal.com/blog/sandboxing-java-applications-implementing-landlock-lsm-for-enhanced-container-security/
Explains using Linux Landlock LSM to sandbox Java applications by restricting file system and resource access without root privileges, improving application-level isolation and reducing attack surface.

https://macronepal.com/blog/gvisor-sandbox-integration-in-java-complete-guide/
Explains integrating gVisor with Java to provide a user-space kernel sandbox that intercepts system calls and isolates applications from the host operating system for stronger security.

https://macronepal.com/blog/selinux-for-java-mandatory-access-control-for-jvm-applications/
Explains how SELinux enforces Mandatory Access Control (MAC) policies on Java applications, strictly limiting what files, processes, and network resources the JVM can access.

https://macronepal.com/java/a-comprehensive-guide-to-intel-sgx-sdk-integration-in-java/
Explains Intel SGX integration in Java, allowing sensitive code and data to run inside secure hardware enclaves that remain protected even if the OS is compromised.

https://macronepal.com/blog/building-a-microvm-runtime-with-aws-firecracker-in-java-a-comprehensive-guide/
Explains using AWS Firecracker microVMs with Java to run workloads in lightweight virtual machines that provide strong isolation with near-container performance efficiency.

https://macronepal.com/blog/enforcing-mandatory-access-control-implementing-apparmor-for-java-applications/
Explains AppArmor security profiles for Java applications, enforcing rules that restrict file access, execution rights, and system-level permissions.

https://macronepal.com/blog/rootless-containers-in-java-secure-container-operations-without-root/
Explains running Java applications in rootless containers using Linux user namespaces so containers operate securely without requiring root privileges.

https://macronepal.com/blog/unlocking-container-security-harnessing-user-namespaces-in-java/
Explains Linux user namespaces, which isolate user and group IDs inside containers to improve privilege separation and enhance container security for Java workloads.

https://macronepal.com/blog/secure-bootstrapping-in-java-comprehensive-trust-establishment-framework/
Explains secure bootstrapping in Java, focusing on how systems establish trust during startup using secure key management, identity verification, and trusted configuration loading.

https://macronepal.com/blog/securing-java-applications-with-chainguard-wolfi-a-comprehensive-guide-2/
Explains using Chainguard/Wolfi minimal container images to secure Java applications by reducing unnecessary packages, minimizing vulnerabilities, and providing a hardened runtime environment.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper