Introduction to Cosign Image Signing
Cosign is a tool developed by the Sigstore project for signing and verifying container images and other artifacts. It uses keyless signing with OpenID Connect (OIDC) or traditional key-based approaches, providing a secure way to verify the provenance of container images.
System Architecture Overview
Cosign Signing Process ├── Key-based Signing │ ├ - Generate Key Pair │ ├ - Sign Image │ └ - Verify Signature ├── Keyless Signing (OIDC) │ ├ - OIDC Token Acquisition │ ├ - Fulcio Certificate │ └ - Rekor Transparency Log ├── Artifact Types │ ├ - Container Images (OCI) │ ├ - SBOMs (Software Bill of Materials) │ ├ - Binary Files │ └ - Java Archives (JAR) └── Verification ├ - Signature Check ├ - Certificate Validation └ - Rekor Entry Verification
Core Implementation
1. Maven Dependencies
<properties>
<cosign.version>2.1.1</cosign.version>
<sigstore.version>0.8.0</sigstore.version>
<bouncycastle.version>1.75</bouncycastle.version>
<jib.version>3.3.2</jib.version>
</properties>
<dependencies>
<!-- Cosign Java Client -->
<dependency>
<groupId>dev.sigstore</groupId>
<artifactId>sigstore-java</artifactId>
<version>${sigstore.version}</version>
</dependency>
<!-- Cosign REST Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- JGit for Git operations -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.6.0.202305301015-r</version>
</dependency>
<!-- Docker Registry Client -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.3.0</version>
</dependency>
<!-- OCI Registry Client -->
<dependency>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-core</artifactId>
<version>${jib.version}</version>
</dependency>
<!-- Cryptography -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Base64 & Utilities -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
2. Core Cosign Service
import dev.sigstore.KeylessSignature;
import dev.sigstore.KeylessSigner;
import dev.sigstore.bundle.Bundle;
import dev.sigstore.encryption.signers.Signers;
import dev.sigstore.rekor.client.RekorClient;
import dev.sigstore.trustroot.SigstoreTrustedRoot;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Service
public class CosignService {
static {
Security.addProvider(new BouncyCastleProvider());
}
private final RekorClient rekorClient;
private final KeyPair keyPair;
private final SigstoreTrustedRoot trustedRoot;
public CosignService() throws Exception {
// Initialize Rekor client (transparency log)
this.rekorClient = RekorClient.builder()
.setServerUrl("https://rekor.sigstore.dev")
.build();
// Initialize trusted root
this.trustedRoot = SigstoreTrustedRoot.from(
getClass().getResourceAsStream("/trustroot.json")
);
// Generate or load key pair
this.keyPair = loadOrGenerateKeyPair();
}
/**
* Key-based Image Signing
*/
public String signImage(String imageReference, Map<String, String> annotations) throws Exception {
// Generate signature payload
String payload = generateSignaturePayload(imageReference, annotations);
byte[] payloadBytes = payload.getBytes();
// Sign the payload
byte[] signature = signPayload(payloadBytes);
// Upload to Rekor transparency log
String rekorEntry = uploadToRekor(payloadBytes, signature);
// Create and push signature to registry
pushSignatureToRegistry(imageReference, signature, annotations);
return rekorEntry;
}
/**
* Keyless Signing with OIDC
*/
public Bundle keylessSignImage(String imageReference,
Map<String, String> annotations,
String oidcToken) throws Exception {
KeylessSigner signer = KeylessSigner.builder()
.sigstorePublicDefaults()
.oidcToken(oidcToken)
.build();
// Generate signature
byte[] payload = generateSignaturePayload(imageReference, annotations).getBytes();
KeylessSignature keylessSignature = signer.sign(payload);
// Create bundle
Bundle bundle = Bundle.builder()
.withMessage(payload)
.withSignatures(keylessSignature)
.build();
// Push signature
pushBundleToRegistry(imageReference, bundle);
return bundle;
}
/**
* Verify Image Signature
*/
public boolean verifyImage(String imageReference) throws Exception {
// Pull signature from registry
byte[] signature = pullSignatureFromRegistry(imageReference);
// Pull certificate (if keyless)
byte[] certificate = pullCertificateFromRegistry(imageReference);
// Verify against Rekor
boolean rekorVerified = verifyRekorEntry(imageReference, signature);
// Cryptographic verification
boolean cryptoVerified = verifyCryptographic(signature, certificate, imageReference);
return rekorVerified && cryptoVerified;
}
/**
* Generate Key Pair
*/
public KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
return keyGen.generateKeyPair();
}
/**
* Save Key Pair to Files
*/
public void saveKeyPair(KeyPair keyPair, Path privateKeyPath, Path publicKeyPath) throws Exception {
// Save private key
Files.write(privateKeyPath, keyPair.getPrivate().getEncoded());
// Save public key
Files.write(publicKeyPath, keyPair.getPublic().getEncoded());
}
/**
* Load Key Pair from Files
*/
public KeyPair loadKeyPair(Path privateKeyPath, Path publicKeyPath) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// Load private key
byte[] privateKeyBytes = Files.readAllBytes(privateKeyPath);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
// Load public key
byte[] publicKeyBytes = Files.readAllBytes(publicKeyPath);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
return new KeyPair(publicKey, privateKey);
}
/**
* Sign SBOM (Software Bill of Materials)
*/
public String signSBOM(String imageReference, String sbomContent) throws Exception {
Map<String, String> annotations = new HashMap<>();
annotations.put("sbom.type", "spdx");
annotations.put("sbom.format", "json");
String payload = generateSBOMSignaturePayload(imageReference, sbomContent);
byte[] signature = signPayload(payload.getBytes());
// Upload SBOM to registry
pushSBOMToRegistry(imageReference, sbomContent, signature);
return Base64.getEncoder().encodeToString(signature);
}
/**
* Verify Multiple Signatures
*/
public VerificationResult verifyAllSignatures(String imageReference) throws Exception {
VerificationResult result = new VerificationResult();
result.setImageReference(imageReference);
// Check for cosign signature
result.setCosignSignatureVerified(verifyCosignSignature(imageReference));
// Check for SBOM signature
result.setSbomVerified(verifySBOMSignature(imageReference));
// Check for attestations
result.setAttestationsVerified(verifyAttestations(imageReference));
// Check transparency log
result.setTransparencyLogVerified(verifyTransparencyLog(imageReference));
return result;
}
private KeyPair loadOrGenerateKeyPair() throws Exception {
Path privateKeyPath = Path.of("cosign.key");
Path publicKeyPath = Path.of("cosign.pub");
if (Files.exists(privateKeyPath) && Files.exists(publicKeyPath)) {
return loadKeyPair(privateKeyPath, publicKeyPath);
} else {
KeyPair newKeyPair = generateKeyPair();
saveKeyPair(newKeyPair, privateKeyPath, publicKeyPath);
return newKeyPair;
}
}
private byte[] signPayload(byte[] payload) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(keyPair.getPrivate());
signature.update(payload);
return signature.sign();
}
private String generateSignaturePayload(String imageReference, Map<String, String> annotations) {
StringBuilder payload = new StringBuilder();
payload.append("image:").append(imageReference).append("\n");
if (annotations != null) {
annotations.forEach((key, value) -> {
payload.append(key).append(":").append(value).append("\n");
});
}
return payload.toString();
}
private String uploadToRekor(byte[] payload, byte[] signature) throws Exception {
// Implementation for uploading to Rekor transparency log
// This would use the RekorClient to create an entry
return "rekor-entry-id";
}
private void pushSignatureToRegistry(String imageReference,
byte[] signature,
Map<String, String> annotations) {
// Implementation for pushing signature to container registry
}
private byte[] pullSignatureFromRegistry(String imageReference) {
// Implementation for pulling signature from registry
return new byte[0];
}
private boolean verifyRekorEntry(String imageReference, byte[] signature) {
// Implementation for verifying Rekor entry
return true;
}
private boolean verifyCryptographic(byte[] signature, byte[] certificate, String imageReference) {
// Implementation for cryptographic verification
return true;
}
// Additional helper methods...
}
3. Container Registry Integration
import com.google.cloud.tools.jib.api.*;
import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath;
import org.springframework.stereotype.Service;
import java.nio.file.Path;
import java.util.Map;
@Service
public class ContainerRegistryService {
/**
* Push Image with Signature
*/
public void pushImageWithSignature(Path dockerfilePath,
String targetImage,
byte[] signature,
Map<String, String> annotations) throws Exception {
// Build and push container image
Containerizer containerizer = Containerizer.to(RegistryImage.named(targetImage)
.addCredential("username", "password"));
Jib.from("base-image")
.addLayer(dockerfilePath.getParent(), AbsoluteUnixPath.get("/"))
.containerize(containerizer);
// Create and push signature as separate layer
pushSignatureLayer(targetImage, signature, annotations);
}
/**
* Attach Signature to Existing Image
*/
public void attachSignatureToImage(String imageReference,
byte[] signature,
Map<String, String> annotations) throws Exception {
// Pull the image manifest
ImageReference imageRef = ImageReference.parse(imageReference);
// Create signature manifest
DescriptorDigest signatureDigest = createSignatureManifest(signature, annotations);
// Attach signature to image
attachSignatureToManifest(imageRef, signatureDigest);
}
/**
* Verify Image in Registry
*/
public boolean verifyImageInRegistry(String imageReference) throws Exception {
ImageReference imageRef = ImageReference.parse(imageReference);
// Pull image manifest
ManifestDescriptor manifest = pullManifest(imageRef);
// Check for signature attachment
boolean hasSignature = checkForSignature(manifest);
if (!hasSignature) {
return false;
}
// Pull and verify signature
byte[] signature = pullSignature(manifest);
return verifySignatureDigest(signature, manifest.getDigest());
}
private DescriptorDigest createSignatureManifest(byte[] signature,
Map<String, String> annotations) {
// Create a manifest for the signature layer
// This would include annotations and the signature content
return DescriptorDigest.fromHash("sha256:abc123");
}
private void attachSignatureToManifest(ImageReference imageRef,
DescriptorDigest signatureDigest) {
// Attach signature to image manifest as a referrer
}
private ManifestDescriptor pullManifest(ImageReference imageRef) throws Exception {
// Pull manifest from registry
return null;
}
private boolean checkForSignature(ManifestDescriptor manifest) {
// Check if manifest has signature referrers
return false;
}
private byte[] pullSignature(ManifestDescriptor manifest) {
// Pull signature from registry
return new byte[0];
}
private boolean verifySignatureDigest(byte[] signature, DescriptorDigest expectedDigest) {
// Verify signature matches expected digest
return true;
}
}
4. OIDC Integration for Keyless Signing
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class OIDCService {
private final RestTemplate restTemplate;
public OIDCService() {
this.restTemplate = new RestTemplate();
}
/**
* Get OIDC Token from GitHub Actions
*/
public String getGitHubActionsToken() {
// In GitHub Actions, the token is available via environment variable
String token = System.getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN");
String url = System.getenv("ACTIONS_ID_TOKEN_REQUEST_URL");
if (token == null || url == null) {
throw new IllegalStateException("Not running in GitHub Actions");
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
Map.class
);
return (String) response.getBody().get("value");
}
/**
* Get OIDC Token from Google Cloud
*/
public String getGoogleCloudToken() throws Exception {
// Use Google Cloud metadata server
String metadataUrl = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=sigstore";
HttpHeaders headers = new HttpHeaders();
headers.set("Metadata-Flavor", "Google");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
metadataUrl,
HttpMethod.GET,
entity,
String.class
);
return response.getBody();
}
/**
* Exchange OIDC Token for Fulcio Certificate
*/
public String getFulcioCertificate(String oidcToken) {
String fulcioUrl = "https://fulcio.sigstore.dev/api/v1/signingCert";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(oidcToken);
Map<String, Object> requestBody = Map.of(
"publicKey", Map.of(
"algorithm", "ECDSA",
"content", "base64-encoded-public-key"
),
"signedEmailAddress", "base64-signed-email"
);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.exchange(
fulcioUrl,
HttpMethod.POST,
entity,
Map.class
);
return (String) response.getBody().get("certificate");
}
/**
* Validate OIDC Token
*/
public boolean validateToken(String token, String issuer, String audience) {
// Implement JWT validation
// Check issuer, audience, expiration, etc.
return true;
}
/**
* Extract Claims from OIDC Token
*/
public Map<String, Object> extractClaims(String token) {
// Parse JWT and extract claims
// This would use a JWT library like jjwt
return Map.of();
}
}
5. Rekor Transparency Log Integration
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
@Service
public class RekorService {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final String rekorUrl = "https://rekor.sigstore.dev";
public RekorService() {
this.restTemplate = new RestTemplate();
this.objectMapper = new ObjectMapper();
}
/**
* Create Rekor Entry for Signature
*/
public String createEntry(String signature,
String certificate,
String artifactHash) throws Exception {
String entryUrl = rekorUrl + "/api/v1/log/entries";
Map<String, Object> requestBody = Map.of(
"apiVersion", "0.0.1",
"spec", Map.of(
"signature", Map.of(
"format", "x509",
"content", signature,
"publicKey", Map.of(
"content", certificate
)
),
"data", Map.of(
"hash", Map.of(
"algorithm", "sha256",
"value", artifactHash
)
)
)
);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.exchange(
entryUrl,
HttpMethod.POST,
entity,
Map.class
);
Map<String, Object> responseBody = response.getBody();
return (String) responseBody.get("uuid");
}
/**
* Get Rekor Entry by UUID
*/
public Map<String, Object> getEntry(String uuid) {
String entryUrl = rekorUrl + "/api/v1/log/entries/" + uuid;
ResponseEntity<Map> response = restTemplate.getForEntity(entryUrl, Map.class);
return response.getBody();
}
/**
* Verify Rekor Entry
*/
public boolean verifyEntry(String uuid, String expectedHash) throws Exception {
Map<String, Object> entry = getEntry(uuid);
// Extract hash from entry
Map<String, Object> body = (Map<String, Object>) entry.get("body");
Map<String, Object> spec = (Map<String, Object>) body.get("spec");
Map<String, Object> data = (Map<String, Object>) spec.get("data");
Map<String, Object> hash = (Map<String, Object>) data.get("hash");
String actualHash = (String) hash.get("value");
// Compare with expected hash
return expectedHash.equals(actualHash);
}
/**
* Search Rekor Entries
*/
public Map<String, Object> searchEntries(String hash,
String email,
String publicKey) throws Exception {
String searchUrl = rekorUrl + "/api/v1/index/retrieve";
Map<String, Object> requestBody = Map.of(
"hash", hash,
"email", email,
"publicKey", publicKey
);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.exchange(
searchUrl,
HttpMethod.POST,
entity,
Map.class
);
return response.getBody();
}
/**
* Get Rekor Public Key
*/
public String getPublicKey() {
String publicKeyUrl = rekorUrl + "/api/v1/log/publicKey";
return restTemplate.getForObject(publicKeyUrl, String.class);
}
}
6. REST API Controllers
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@RestController
@RequestMapping("/api/cosign")
public class CosignController {
private final CosignService cosignService;
private final OIDCService oidcService;
private final ContainerRegistryService registryService;
public CosignController(CosignService cosignService,
OIDCService oidcService,
ContainerRegistryService registryService) {
this.cosignService = cosignService;
this.oidcService = oidcService;
this.registryService = registryService;
}
@PostMapping("/sign/key-based")
public ResponseEntity<SignResponse> signImageKeyBased(
@RequestBody SignRequest request) {
try {
String rekorEntry = cosignService.signImage(
request.getImageReference(),
request.getAnnotations()
);
return ResponseEntity.ok(new SignResponse(
"Image signed successfully",
rekorEntry
));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new SignResponse("Signing failed: " + e.getMessage(), null));
}
}
@PostMapping("/sign/keyless")
public ResponseEntity<SignResponse> signImageKeyless(
@RequestBody KeylessSignRequest request) {
try {
// Get OIDC token based on provider
String oidcToken;
switch (request.getIdentityProvider()) {
case "github":
oidcToken = oidcService.getGitHubActionsToken();
break;
case "google":
oidcToken = oidcService.getGoogleCloudToken();
break;
default:
oidcToken = request.getOidcToken();
}
Bundle bundle = cosignService.keylessSignImage(
request.getImageReference(),
request.getAnnotations(),
oidcToken
);
return ResponseEntity.ok(new SignResponse(
"Image signed keylessly",
bundle.getSignatures().get(0).getCertPath().toString()
));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new SignResponse("Keyless signing failed: " + e.getMessage(), null));
}
}
@PostMapping("/verify")
public ResponseEntity<VerifyResponse> verifyImage(
@RequestBody VerifyRequest request) {
try {
boolean verified = cosignService.verifyImage(request.getImageReference());
if (verified) {
return ResponseEntity.ok(new VerifyResponse(
"Signature verified successfully",
true
));
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new VerifyResponse(
"Signature verification failed",
false
));
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new VerifyResponse(
"Verification error: " + e.getMessage(),
false
));
}
}
@PostMapping("/sbom/sign")
public ResponseEntity<SignResponse> signSBOM(
@RequestParam String imageReference,
@RequestParam MultipartFile sbomFile) {
try {
String sbomContent = new String(sbomFile.getBytes());
String signature = cosignService.signSBOM(imageReference, sbomContent);
return ResponseEntity.ok(new SignResponse(
"SBOM signed successfully",
signature
));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new SignResponse("SBOM signing failed: " + e.getMessage(), null));
}
}
@GetMapping("/keys/generate")
public ResponseEntity<KeyPairResponse> generateKeys() {
try {
KeyPair keyPair = cosignService.generateKeyPair();
return ResponseEntity.ok(new KeyPairResponse(
Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()),
Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded())
));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new KeyPairResponse(null, null));
}
}
@PostMapping("/attestations/create")
public ResponseEntity<AttestationResponse> createAttestation(
@RequestBody AttestationRequest request) {
try {
// Create in-toto attestation
String attestation = createAttestationDocument(
request.getImageReference(),
request.getPredicateType(),
request.getPredicate()
);
// Sign attestation
String signature = signAttestation(attestation);
return ResponseEntity.ok(new AttestationResponse(
"Attestation created and signed",
attestation,
signature
));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new AttestationResponse(
"Attestation creation failed: " + e.getMessage(),
null,
null
));
}
}
// Request/Response classes
public static class SignRequest {
private String imageReference;
private Map<String, String> annotations;
// getters and setters
}
public static class KeylessSignRequest {
private String imageReference;
private Map<String, String> annotations;
private String identityProvider;
private String oidcToken;
// getters and setters
}
public static class SignResponse {
private String message;
private String reference;
// constructor, getters and setters
}
public static class VerifyRequest {
private String imageReference;
// getters and setters
}
public static class VerifyResponse {
private String message;
private boolean verified;
// constructor, getters and setters
}
public static class KeyPairResponse {
private String publicKey;
private String privateKey;
// constructor, getters and setters
}
private String createAttestationDocument(String imageReference,
String predicateType,
Map<String, Object> predicate) {
// Create in-toto attestation document
return "";
}
private String signAttestation(String attestation) {
// Sign attestation document
return "";
}
}
7. GitHub Actions Integration
# .github/workflows/build-and-sign.yaml
name: Build, Sign and Push
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
id-token: write # Needed for keyless signing
contents: read
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Maven
run: mvn clean package
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
- name: Set up Cosign
uses: sigstore/cosign-installer@main
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Keyless Sign with Cosign
run: |
cosign sign --yes \
myapp:${{ github.sha }} \
--registry-username ${{ secrets.DOCKER_USERNAME }} \
--registry-password ${{ secrets.DOCKER_TOKEN }}
- name: Generate SBOM
run: |
syft myapp:${{ github.sha }} --scope all-layers -o spdx-json > sbom.json
- name: Sign SBOM
run: |
cosign attach sbom --yes \
--sbom sbom.json \
myapp:${{ github.sha }}
8. Verification in CI/CD Pipeline
import org.springframework.stereotype.Service;
@Service
public class CICDVerificationService {
private final CosignService cosignService;
private final RekorService rekorService;
public CICDVerificationService(CosignService cosignService,
RekorService rekorService) {
this.cosignService = cosignService;
this.rekorService = rekorService;
}
/**
* Verify Image Before Deployment
*/
public boolean verifyImageForDeployment(String imageReference,
VerificationPolicy policy) throws Exception {
VerificationResult result = cosignService.verifyAllSignatures(imageReference);
// Apply policy checks
if (!result.isCosignSignatureVerified() && policy.isSignatureRequired()) {
throw new SecurityException("Signature verification failed");
}
if (!result.isSbomVerified() && policy.isSbomRequired()) {
throw new SecurityException("SBOM verification failed");
}
if (!result.isAttestationsVerified() && policy.areAttestationsRequired()) {
throw new SecurityException("Attestations verification failed");
}
// Check for known vulnerabilities
if (policy.isVulnerabilityScanRequired()) {
boolean hasVulnerabilities = checkVulnerabilities(imageReference);
if (hasVulnerabilities && policy.isBlockOnVulnerabilities()) {
throw new SecurityException("Image contains vulnerabilities");
}
}
return true;
}
/**
* Verify Supply Chain
*/
public SupplyChainVerification verifySupplyChain(String imageReference) throws Exception {
SupplyChainVerification verification = new SupplyChainVerification();
// Verify build provenance
verification.setBuildProvenanceVerified(
verifyBuildProvenance(imageReference)
);
// Verify source code
verification.setSourceVerified(
verifySourceCode(imageReference)
);
// Verify dependencies
verification.setDependenciesVerified(
verifyDependencies(imageReference)
);
// Verify deployment environment
verification.setEnvironmentVerified(
verifyDeploymentEnvironment(imageReference)
);
return verification;
}
private boolean verifyBuildProvenance(String imageReference) {
// Verify SLSA provenance
return true;
}
private boolean verifySourceCode(String imageReference) {
// Verify source matches built artifact
return true;
}
private boolean verifyDependencies(String imageReference) {
// Verify dependencies are from trusted sources
return true;
}
private boolean verifyDeploymentEnvironment(String imageReference) {
// Verify deployment environment compliance
return true;
}
private boolean checkVulnerabilities(String imageReference) {
// Integrate with vulnerability scanner like Trivy
return false;
}
public static class VerificationPolicy {
private boolean signatureRequired = true;
private boolean sbomRequired = true;
private boolean attestationsRequired = false;
private boolean vulnerabilityScanRequired = true;
private boolean blockOnVulnerabilities = true;
private String[] allowedSigners;
private String[] trustedRegistries;
// getters and setters
}
public static class SupplyChainVerification {
private boolean buildProvenanceVerified;
private boolean sourceVerified;
private boolean dependenciesVerified;
private boolean environmentVerified;
// getters and setters
}
}
9. Security Configuration
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/cosign/verify").permitAll()
.antMatchers("/api/cosign/sign/**").hasRole("SIGNER")
.antMatchers("/api/cosign/keys/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}
10. Monitoring and Auditing
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class CosignAuditService {
private static final Logger logger = LoggerFactory.getLogger(CosignAuditService.class);
private final ConcurrentHashMap<String, AuditEntry> auditLog = new ConcurrentHashMap<>();
public void logSigningEvent(String imageReference,
String signer,
String method,
boolean success) {
AuditEntry entry = new AuditEntry(
Instant.now(),
"SIGN",
imageReference,
signer,
method,
success
);
String id = generateId();
auditLog.put(id, entry);
logger.info("Signing event: {} - {} - {} - {}",
imageReference, signer, method, success ? "SUCCESS" : "FAILURE");
}
public void logVerificationEvent(String imageReference,
String verifier,
boolean success,
String details) {
AuditEntry entry = new AuditEntry(
Instant.now(),
"VERIFY",
imageReference,
verifier,
details,
success
);
String id = generateId();
auditLog.put(id, entry);
logger.info("Verification event: {} - {} - {} - {}",
imageReference, verifier, success ? "PASS" : "FAIL", details);
}
public AuditReport generateReport(Instant from, Instant to) {
AuditReport report = new AuditReport();
auditLog.forEach((id, entry) -> {
if (!entry.getTimestamp().isBefore(from) &&
!entry.getTimestamp().isAfter(to)) {
report.addEntry(entry);
}
});
return report;
}
private String generateId() {
return Instant.now().toString() + "-" + System.currentTimeMillis();
}
public static class AuditEntry {
private final Instant timestamp;
private final String operation;
private final String imageReference;
private final String actor;
private final String details;
private final boolean success;
// constructor, getters
}
public static class AuditReport {
private final List<AuditEntry> entries = new ArrayList<>();
private int totalSignings = 0;
private int totalVerifications = 0;
private int failedSignings = 0;
private int failedVerifications = 0;
// methods to add entries and calculate statistics
}
}
Best Practices
1. Key Management
// Store keys securely, never commit to version control
public class SecureKeyStorage {
public void storeKeyInVault(String keyId, byte[] keyMaterial) {
// Use HashiCorp Vault, AWS KMS, or Azure Key Vault
}
public byte[] retrieveKeyFromVault(String keyId) {
// Retrieve key from secure storage
return new byte[0];
}
}
2. Policy Enforcement
// Enforce signing policies
public class SigningPolicyEnforcer {
public boolean canSign(String imageReference, String signer) {
// Check if signer is authorized for this image
// Check if image is from allowed registry
// Check rate limits
return true;
}
}
3. Error Handling
public class CosignException extends RuntimeException {
private final ErrorCode errorCode;
public CosignException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public enum ErrorCode {
SIGNATURE_INVALID,
CERTIFICATE_EXPIRED,
REKOR_UNAVAILABLE,
KEY_NOT_FOUND
}
}
Conclusion
This comprehensive Cosign Image Signing implementation provides:
- Key-based signing with RSA/ECDSA keys
- Keyless signing with OIDC integration
- SBOM signing and verification
- Container registry integration
- Rekor transparency log operations
- CI/CD pipeline integration
- Comprehensive audit logging
- Security policy enforcement
Key benefits:
- Supply chain security for container images
- Non-repudiation through cryptographic signatures
- Transparency via Rekor public log
- Automation-friendly for CI/CD pipelines
- Vendor-neutral implementation
This setup ensures that your container images are securely signed and verified throughout the software supply chain, providing confidence in the integrity and provenance of deployed artifacts.
Advanced Java Supply Chain Security, Kubernetes Hardening & Runtime Threat Detection
Sigstore Rekor in Java – https://macronepal.com/blog/sigstore-rekor-in-java/
Explains integrating Sigstore Rekor into Java systems to create a transparent, tamper-proof log of software signatures and metadata for verifying supply chain integrity.
Securing Java Applications with Chainguard Wolfi – https://macronepal.com/blog/securing-java-applications-with-chainguard-wolfi-a-comprehensive-guide/
Explains using Chainguard Wolfi minimal container images to reduce vulnerabilities and secure Java applications with hardened, lightweight runtime environments.
Cosign Image Signing in Java Complete Guide – https://macronepal.com/blog/cosign-image-signing-in-java-complete-guide/
Explains how to digitally sign container images using Cosign in Java-based workflows to ensure authenticity and prevent unauthorized modifications.
Secure Supply Chain Enforcement Kyverno Image Verification for Java Containers – https://macronepal.com/blog/secure-supply-chain-enforcement-kyverno-image-verification-for-java-containers/
Explains enforcing Kubernetes policies with Kyverno to verify container image signatures and ensure only trusted Java container images are deployed.
Pod Security Admission in Java Securing Kubernetes Deployments for JVM Applications – https://macronepal.com/blog/pod-security-admission-in-java-securing-kubernetes-deployments-for-jvm-applications/
Explains Kubernetes Pod Security Admission policies that enforce security rules like restricted privileges and safe configurations for Java workloads.
Securing Java Applications at Runtime Kubernetes Security Context – https://macronepal.com/blog/securing-java-applications-at-runtime-a-guide-to-kubernetes-security-context/
Explains how Kubernetes security contexts control runtime permissions, user IDs, and access rights for Java containers to improve isolation.
Process Anomaly Detection in Java Behavioral Monitoring – https://macronepal.com/blog/process-anomaly-detection-in-java-comprehensive-behavioral-monitoring-2/
Explains detecting abnormal runtime behavior in Java applications to identify potential security threats using process monitoring techniques.
Achieving Security Excellence CIS Benchmark Compliance for Java Applications – https://macronepal.com/blog/achieving-security-excellence-implementing-cis-benchmark-compliance-for-java-applications/
Explains applying CIS security benchmarks to Java environments to standardize hardening and improve overall system security posture.
Process Anomaly Detection in Java Behavioral Monitoring – https://macronepal.com/blog/process-anomaly-detection-in-java-comprehensive-behavioral-monitoring/
Explains behavioral monitoring of Java processes to detect anomalies and improve runtime security through continuous observation and analysis.