Cosign is a popular tool from the Sigstore project that enables keyless signing and verification of container images and other artifacts. It simplifies digital signing by leveraging Fulcio for certificate issuance and Rekor for transparency log storage.
Understanding Cosign and Sigstore Ecosystem
Sigstore Components:
- Cosign: Handles signing, verification, and key management
- Fulcio: Root CA that issues short-lived certificates for keyless signing
- Rekor: Transparency log that records signed artifacts metadata
- OIDC: OpenID Connect for identity verification in keyless mode
Key Benefits of Cosign
- Keyless Signing: No long-term key management required
- Transparency: All signatures recorded in public ledger
- Standard Compliance: Uses X.509 certificates and standard PKIX
- Multi-format Support: Containers, SBOMs, binaries, and more
Java Dependencies Setup
<dependencies> <!-- HTTP client for Sigstore APIs --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> <!-- JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency> <!-- Cryptography libraries --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>1.76</version> </dependency> <!-- JWT for OIDC --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> <!-- Base64 encoding --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.16.0</version> </dependency> </dependencies>
Core Cosign Client Implementation
package com.example.cosign;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
import org.bouncycastle.util.encoders.Base64;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
public class CosignClient {
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private final String fulcioUrl;
private final String rekorUrl;
// Sigstore production endpoints
private static final String DEFAULT_FULCIO_URL = "https://fulcio.sigstore.dev";
private static final String DEFAULT_REKOR_URL = "https://rekor.sigstore.dev";
public CosignClient() {
this(DEFAULT_FULCIO_URL, DEFAULT_REKOR_URL);
}
public CosignClient(String fulcioUrl, String rekorUrl) {
this.httpClient = new OkHttpClient();
this.objectMapper = new ObjectMapper();
this.fulcioUrl = fulcioUrl;
this.rekorUrl = rekorUrl;
}
public static class SigningOptions {
private boolean keyless = true;
private String privateKeyPath;
private String oidcToken;
private String oidcIssuer = "https://oauth2.sigstore.dev/auth";
private boolean uploadToRekor = true;
// Getters and setters
public boolean isKeyless() { return keyless; }
public void setKeyless(boolean keyless) { this.keyless = keyless; }
public String getPrivateKeyPath() { return privateKeyPath; }
public void setPrivateKeyPath(String privateKeyPath) { this.privateKeyPath = privateKeyPath; }
public String getOidcToken() { return oidcToken; }
public void setOidcToken(String oidcToken) { this.oidcToken = oidcToken; }
public String getOidcIssuer() { return oidcIssuer; }
public void setOidcIssuer(String oidcIssuer) { this.oidcIssuer = oidcIssuer; }
public boolean isUploadToRekor() { return uploadToRekor; }
public void setUploadToRekor(boolean uploadToRekor) { this.uploadToRekor = uploadToRekor; }
}
public static class SignatureResult {
private final boolean success;
private final String signature;
private final String certificate;
private final String rekorEntry;
private final String error;
public SignatureResult(boolean success, String signature, String certificate, String rekorEntry) {
this.success = success;
this.signature = signature;
this.certificate = certificate;
this.rekorEntry = rekorEntry;
this.error = null;
}
public SignatureResult(String error) {
this.success = false;
this.signature = null;
this.certificate = null;
this.rekorEntry = null;
this.error = error;
}
// Getters
public boolean isSuccess() { return success; }
public String getSignature() { return signature; }
public String getCertificate() { return certificate; }
public String getRekorEntry() { return rekorEntry; }
public String getError() { return error; }
}
public static class VerificationResult {
private final boolean verified;
private final String digest;
private final String signer;
private final String rekorEntry;
private final String error;
public VerificationResult(boolean verified, String digest, String signer, String rekorEntry) {
this.verified = verified;
this.digest = digest;
this.signer = signer;
this.rekorEntry = rekorEntry;
this.error = null;
}
public VerificationResult(String error) {
this.verified = false;
this.digest = null;
this.signer = null;
this.rekorEntry = null;
this.error = error;
}
// Getters
public boolean isVerified() { return verified; }
public String getDigest() { return digest; }
public String getSigner() { return signer; }
public String getRekorEntry() { return rekorEntry; }
public String getError() { return error; }
}
/**
* Sign a digest using keyless signing
*/
public SignatureResult signDigestKeyless(byte[] digest, SigningOptions options) throws IOException {
try {
// Generate ephemeral key pair
KeyPair keyPair = generateKeyPair();
// Prepare signing certificate request
String certRequest = createCertificateRequest(keyPair.getPublic(), options);
// Get signing certificate from Fulcio
String certificate = getFulcioCertificate(certRequest, options.getOidcToken());
// Sign the digest
byte[] signature = signData(digest, keyPair.getPrivate());
String base64Signature = Base64.toBase64String(signature);
// Upload to Rekor if requested
String rekorEntry = null;
if (options.isUploadToRekor()) {
rekorEntry = uploadToRekor(digest, base64Signature, certificate, keyPair.getPublic());
}
return new SignatureResult(true, base64Signature, certificate, rekorEntry);
} catch (Exception e) {
return new SignatureResult("Keyless signing failed: " + e.getMessage());
}
}
/**
* Sign a digest using existing private key
*/
public SignatureResult signDigestWithKey(byte[] digest, PrivateKey privateKey,
String certificate) throws IOException {
try {
// Sign the digest
byte[] signature = signData(digest, privateKey);
String base64Signature = Base64.toBase64String(signature);
// Upload to Rekor
String rekorEntry = uploadToRekor(digest, base64Signature, certificate, null);
return new SignatureResult(true, base64Signature, certificate, rekorEntry);
} catch (Exception e) {
return new SignatureResult("Key-based signing failed: " + e.getMessage());
}
}
/**
* Verify a signature
*/
public VerificationResult verifySignature(byte[] digest, String signature,
String certificate, String rekorEntry) {
try {
// Verify certificate chain
if (!verifyCertificate(certificate)) {
return new VerificationResult("Certificate verification failed");
}
// Extract public key from certificate
PublicKey publicKey = extractPublicKeyFromCertificate(certificate);
// Verify signature
if (!verifyData(digest, Base64.decode(signature), publicKey)) {
return new VerificationResult("Signature verification failed");
}
// Verify Rekor entry if provided
if (rekorEntry != null) {
if (!verifyRekorEntry(rekorEntry, digest, signature, certificate)) {
return new VerificationResult("Rekor entry verification failed");
}
}
// Extract signer information from certificate
String signer = extractSignerFromCertificate(certificate);
return new VerificationResult(true,
bytesToHex(digest), signer, rekorEntry);
} catch (Exception e) {
return new VerificationResult("Verification failed: " + e.getMessage());
}
}
/**
* Generate a key pair for traditional signing
*/
public KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
keyGen.initialize(256);
return keyGen.generateKeyPair();
}
private String createCertificateRequest(PublicKey publicKey, SigningOptions options) {
// Create a simple certificate request
// In production, you'd use proper X.509 certificate request generation
Map<String, Object> request = new HashMap<>();
request.put("publicKey", Base64.toBase64String(publicKey.getEncoded()));
request.put("signedEmailAddress", options.getOidcToken());
try {
return objectMapper.writeValueAsString(request);
} catch (Exception e) {
throw new RuntimeException("Failed to create certificate request", e);
}
}
private String getFulcioCertificate(String certRequest, String oidcToken) throws IOException {
RequestBody body = RequestBody.create(
certRequest, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(fulcioUrl + "/api/v1/signingCert")
.post(body)
.header("Authorization", "Bearer " + oidcToken)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Fulcio request failed: " + response.message());
}
String responseBody = response.body().string();
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
// Extract certificate chain
@SuppressWarnings("unchecked")
Map<String, Object> signedCertificate = (Map<String, Object>) responseMap.get("signedCertificate");
return (String) signedCertificate.get("certChain");
}
}
private byte[] signData(byte[] data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
}
private boolean verifyData(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
Signature verifier = Signature.getInstance("SHA256withECDSA");
verifier.initVerify(publicKey);
verifier.update(data);
return verifier.verify(signature);
}
private String uploadToRekor(byte[] digest, String signature, String certificate, PublicKey publicKey)
throws IOException {
Map<String, Object> rekorEntry = new HashMap<>();
Map<String, Object> spec = new HashMap<>();
spec.put("signature", signature);
spec.put("publicKey", publicKey != null ?
Map.of("content", Base64.toBase64String(publicKey.getEncoded())) :
Map.of("certificate", certificate));
spec.put("data", Map.of("hash", Map.of("algorithm", "sha256", "value", bytesToHex(digest))));
rekorEntry.put("kind", "hashedrekord");
rekorEntry.put("apiVersion", "0.0.1");
rekorEntry.put("spec", spec);
String jsonEntry = objectMapper.writeValueAsString(rekorEntry);
RequestBody body = RequestBody.create(jsonEntry, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(rekorUrl + "/api/v1/log/entries")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Rekor upload failed: " + response.message());
}
String responseBody = response.body().string();
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
// Return the first entry UUID
return responseMap.keySet().iterator().next();
}
}
private boolean verifyCertificate(String certificate) {
// Implement certificate chain verification
// This would verify against Sigstore's root CA
return true; // Simplified for example
}
private PublicKey extractPublicKeyFromCertificate(String certificate) throws Exception {
// Implement certificate parsing and public key extraction
// This is a simplified version
java.security.cert.CertificateFactory cf =
java.security.cert.CertificateFactory.getInstance("X.509");
java.io.ByteArrayInputStream bis =
new java.io.ByteArrayInputStream(java.util.Base64.getDecoder().decode(certificate));
java.security.cert.Certificate cert = cf.generateCertificate(bis);
return cert.getPublicKey();
}
private String extractSignerFromCertificate(String certificate) {
// Extract subject from certificate
// Simplified implementation
return "unknown-signer";
}
private boolean verifyRekorEntry(String entryUuid, byte[] digest, String signature, String certificate) {
try {
// Fetch entry from Rekor and verify
Request request = new Request.Builder()
.url(rekorUrl + "/api/v1/log/entries/" + entryUuid)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
return response.isSuccessful();
}
} catch (Exception e) {
return false;
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
Container Image Signing Service
package com.example.cosign;
import org.springframework.stereotype.Service;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
@Service
public class ContainerImageSigner {
private final CosignClient cosignClient;
public ContainerImageSigner(CosignClient cosignClient) {
this.cosignClient = cosignClient;
}
public static class ImageSignRequest {
private String imageReference;
private String digest;
private CosignClient.SigningOptions signingOptions;
private Map<String, String> annotations;
// Getters and setters
public String getImageReference() { return imageReference; }
public void setImageReference(String imageReference) { this.imageReference = imageReference; }
public String getDigest() { return digest; }
public void setDigest(String digest) { this.digest = digest; }
public CosignClient.SigningOptions getSigningOptions() { return signingOptions; }
public void setSigningOptions(CosignClient.SigningOptions signingOptions) { this.signingOptions = signingOptions; }
public Map<String, String> getAnnotations() { return annotations; }
public void setAnnotations(Map<String, String> annotations) { this.annotations = annotations; }
}
public static class ImageVerifyRequest {
private String imageReference;
private String digest;
private String signature;
private String certificate;
private String rekorEntry;
private String publicKey;
// Getters and setters
public String getImageReference() { return imageReference; }
public void setImageReference(String imageReference) { this.imageReference = imageReference; }
public String getDigest() { return digest; }
public void setDigest(String digest) { this.digest = digest; }
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 getRekorEntry() { return rekorEntry; }
public void setRekorEntry(String rekorEntry) { this.rekorEntry = rekorEntry; }
public String getPublicKey() { return publicKey; }
public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
}
/**
* Sign a container image
*/
public CosignClient.SignatureResult signImage(ImageSignRequest request) {
try {
// Convert digest to bytes
byte[] digestBytes = hexToBytes(request.getDigest());
CosignClient.SigningOptions options = request.getSigningOptions();
if (options == null) {
options = new CosignClient.SigningOptions();
}
if (options.isKeyless()) {
return cosignClient.signDigestKeyless(digestBytes, options);
} else {
// For key-based signing, you'd load the private key
// PrivateKey privateKey = loadPrivateKey(options.getPrivateKeyPath());
// return cosignClient.signDigestWithKey(digestBytes, privateKey, certificate);
throw new UnsupportedOperationException("Key-based signing not implemented in this example");
}
} catch (Exception e) {
return new CosignClient.SignatureResult("Image signing failed: " + e.getMessage());
}
}
/**
* Verify a container image signature
*/
public CosignClient.VerificationResult verifyImage(ImageVerifyRequest request) {
try {
byte[] digestBytes = hexToBytes(request.getDigest());
return cosignClient.verifySignature(
digestBytes,
request.getSignature(),
request.getCertificate(),
request.getRekorEntry()
);
} catch (Exception e) {
return new CosignClient.VerificationResult("Image verification failed: " + e.getMessage());
}
}
/**
* Generate key pair for traditional signing
*/
public Map<String, String> generateKeyPair() {
try {
KeyPair keyPair = cosignClient.generateKeyPair();
Map<String, String> result = new HashMap<>();
result.put("privateKey", java.util.Base64.getEncoder()
.encodeToString(keyPair.getPrivate().getEncoded()));
result.put("publicKey", java.util.Base64.getEncoder()
.encodeToString(keyPair.getPublic().getEncoded()));
return result;
} catch (Exception e) {
throw new RuntimeException("Key pair generation failed", e);
}
}
private byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
}
Spring Boot REST API
package com.example.cosign;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@SpringBootApplication
public class CosignApplication {
public static void main(String[] args) {
SpringApplication.run(CosignApplication.class, args);
}
@Bean
public CosignClient cosignClient() {
return new CosignClient();
}
}
@RestController
@RequestMapping("/api/cosign")
public class CosignController {
private final ContainerImageSigner imageSigner;
public CosignController(ContainerImageSigner imageSigner) {
this.imageSigner = imageSigner;
}
@PostMapping("/sign")
public Map<String, Object> signImage(@RequestBody ContainerImageSigner.ImageSignRequest request) {
CosignClient.SignatureResult result = imageSigner.signImage(request);
if (result.isSuccess()) {
return Map.of(
"success", true,
"signature", result.getSignature(),
"certificate", result.getCertificate(),
"rekorEntry", result.getRekorEntry()
);
} else {
return Map.of(
"success", false,
"error", result.getError()
);
}
}
@PostMapping("/verify")
public Map<String, Object> verifyImage(@RequestBody ContainerImageSigner.ImageVerifyRequest request) {
CosignClient.VerificationResult result = imageSigner.verifyImage(request);
if (result.isVerified()) {
return Map.of(
"verified", true,
"digest", result.getDigest(),
"signer", result.getSigner(),
"rekorEntry", result.getRekorEntry()
);
} else {
return Map.of(
"verified", false,
"error", result.getError()
);
}
}
@PostMapping("/keys/generate")
public Map<String, Object> generateKeys() {
try {
Map<String, String> keyPair = imageSigner.generateKeyPair();
return Map.of(
"success", true,
"privateKey", keyPair.get("privateKey"),
"publicKey", keyPair.get("publicKey")
);
} catch (Exception e) {
return Map.of(
"success", false,
"error", e.getMessage()
);
}
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "healthy");
}
}
CI/CD Integration Example
package com.example.cosign;
import java.util.Map;
public class CICDIntegration {
private final ContainerImageSigner imageSigner;
public CICDIntegration(ContainerImageSigner imageSigner) {
this.imageSigner = imageSigner;
}
/**
* Sign image during CI/CD pipeline
*/
public boolean signInPipeline(String imageRef, String digest, String oidcToken) {
ContainerImageSigner.ImageSignRequest request = new ContainerImageSigner.ImageSignRequest();
request.setImageReference(imageRef);
request.setDigest(digest);
CosignClient.SigningOptions options = new CosignClient.SigningOptions();
options.setKeyless(true);
options.setOidcToken(oidcToken);
options.setUploadToRekor(true);
request.setSigningOptions(options);
CosignClient.SignatureResult result = imageSigner.signImage(request);
if (result.isSuccess()) {
System.out.println("Image signed successfully: " + imageRef);
System.out.println("Rekor entry: " + result.getRekorEntry());
return true;
} else {
System.err.println("Failed to sign image: " + result.getError());
return false;
}
}
/**
* Verify image during deployment
*/
public boolean verifyInDeployment(String imageRef, String digest,
String signature, String certificate) {
ContainerImageSigner.ImageVerifyRequest request = new ContainerImageSigner.ImageVerifyRequest();
request.setImageReference(imageRef);
request.setDigest(digest);
request.setSignature(signature);
request.setCertificate(certificate);
CosignClient.VerificationResult result = imageSigner.verifyImage(request);
if (result.isVerified()) {
System.out.println("Image verified successfully: " + imageRef);
System.out.println("Signer: " + result.getSigner());
return true;
} else {
System.err.println("Image verification failed: " + result.getError());
return false;
}
}
}
Best Practices for Production
- OIDC Integration: Integrate with GitHub Actions, Google Cloud IAM, or other OIDC providers
- Key Management: For key-based signing, use HSMs or cloud KMS
- Error Handling: Implement robust error handling for network failures
- Retry Logic: Add retry mechanisms for Fulcio and Rekor requests
- Monitoring: Monitor signing and verification operations
- Security: Secure OIDC tokens and private keys
- Compliance: Maintain audit trails of all signing operations
Conclusion
Implementing Cosign for Sigstore signing in Java provides a robust solution for securing your software supply chain. The keyless signing approach eliminates the complexity of key management while maintaining strong security guarantees through transparency logs and short-lived certificates.
This Java implementation offers:
- Keyless Signing: No long-term key management required
- Transparency: All signatures recorded in Rekor
- Flexible Integration: Easy integration with CI/CD pipelines
- Standards Compliance: Uses standard cryptographic primitives
- Extensible Architecture: Support for multiple signing strategies
By integrating Cosign into your Java applications, you can significantly enhance the security of your container images and other artifacts, providing cryptographic proof of origin and integrity throughout your software supply chain.