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