Introduction to Container Image Signing
Container image signing is a critical security practice that ensures the integrity, authenticity, and provenance of container images. It allows users to verify that images haven't been tampered with and come from trusted sources.
Key Concepts and Standards
Signing Standards
- Notary v2: Modern container signing standard
- Cosign: Sigstore's signing tool
- Docker Content Trust: Docker's signing mechanism
- Simple Signing: Basic signature format
Container Signing with Cosign
Setting Up Cosign Dependencies
<!-- Maven Dependencies for Cosign --> <dependencies> <dependency> <groupId>dev.sigstore</groupId> <artifactId>sigstore-java</artifactId> <version>0.10.0</version> </dependency> <dependency> <groupId>com.google.crypto.tink</groupId> <artifactId>tink</artifactId> <version>1.11.0</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>1.76</version> </dependency> </dependencies>
Basic Cosign Signing Implementation
package com.container.signing; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class CosignSigner { private PrivateKey privateKey; private PublicKey publicKey; public CosignSigner() { generateKeyPair(); } public CosignSigner(String privateKeyPath, String publicKeyPath) throws Exception { loadKeyPair(privateKeyPath, publicKeyPath); } private void generateKeyPair() { try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); this.privateKey = pair.getPrivate(); this.publicKey = pair.getPublic(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Failed to generate key pair", e); } } private void loadKeyPair(String privateKeyPath, String publicKeyPath) throws Exception { // Load private key String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyPath))) .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyContent); PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); this.privateKey = keyFactory.generatePrivate(privateKeySpec); // Load public key String publicKeyContent = new String(Files.readAllBytes(Paths.get(publicKeyPath))) .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "") .replaceAll("\\s", ""); byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyContent); X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes); this.publicKey = keyFactory.generatePublic(publicKeySpec); } public String signImage(String imageDigest) throws Exception { Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(imageDigest.getBytes()); byte[] digitalSignature = signature.sign(); return Base64.getEncoder().encodeToString(digitalSignature); } public boolean verifySignature(String imageDigest, String signatureBase64) throws Exception { Signature signature = Signature.getInstance("SHA256withRSA"); signature.initVerify(publicKey); signature.update(imageDigest.getBytes()); byte[] digitalSignature = Base64.getDecoder().decode(signatureBase64); return signature.verify(digitalSignature); } public void saveKeyPair(String privateKeyPath, String publicKeyPath) throws Exception { // Save private key String privateKeyPem = "-----BEGIN PRIVATE KEY-----\n" + Base64.getMimeEncoder().encodeToString(privateKey.getEncoded()) + "\n-----END PRIVATE KEY-----"; Files.write(Paths.get(privateKeyPath), privateKeyPem.getBytes()); // Save public key String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" + Base64.getMimeEncoder().encodeToString(publicKey.getEncoded()) + "\n-----END PUBLIC KEY-----"; Files.write(Paths.get(publicKeyPath), publicKeyPem.getBytes()); } public static void main(String[] args) { try { CosignSigner signer = new CosignSigner(); // Example image digest String imageDigest = "sha256:abc123def456..."; // Sign the image String signature = signer.signImage(imageDigest); System.out.println("Image signature: " + signature); // Verify the signature boolean isValid = signer.verifySignature(imageDigest, signature); System.out.println("Signature valid: " + isValid); // Save keys for future use signer.saveKeyPair("cosign.key", "cosign.pub"); } catch (Exception e) { e.printStackTrace(); } } } Advanced Container Signing Service
Complete Signing Service Implementation
package com.container.signing; import java.time.Instant; import java.util.*; import java.security.*; import java.nio.file.*; import com.fasterxml.jackson.databind.ObjectMapper; public class ContainerSigningService { private final Map<String, SigningKey> signingKeys; private final ObjectMapper objectMapper; private final String keysDirectory; public ContainerSigningService(String keysDirectory) { this.signingKeys = new HashMap<>(); this.objectMapper = new ObjectMapper(); this.keysDirectory = keysDirectory; loadExistingKeys(); } public static class ImageReference { private String registry; private String repository; private String tag; private String digest; public ImageReference(String registry, String repository, String tag, String digest) { this.registry = registry; this.repository = repository; this.tag = tag; this.digest = digest; } // Getters and setters public String getRegistry() { return registry; } public String getRepository() { return repository; } public String getTag() { return tag; } public String getDigest() { return digest; } public String getFullReference() { return registry + "/" + repository + ":" + tag + "@" + digest; } } public static class SignatureMetadata { private String imageDigest; private String signature; private String keyId; private Instant timestamp; private String signer; private Map<String, String> annotations; public SignatureMetadata(String imageDigest, String signature, String keyId, String signer) { this.imageDigest = imageDigest; this.signature = signature; this.keyId = keyId; this.signer = signer; this.timestamp = Instant.now(); this.annotations = new HashMap<>(); } // Getters and setters public String getImageDigest() { return imageDigest; } public String getSignature() { return signature; } public String getKeyId() { return keyId; } public Instant getTimestamp() { return timestamp; } public String getSigner() { return signer; } public Map<String, String> getAnnotations() { return annotations; } public void addAnnotation(String key, String value) { this.annotations.put(key, value); } } public static class SigningKey { private String keyId; private PublicKey publicKey; private PrivateKey privateKey; private String algorithm; private Instant created; private String description; public SigningKey(String keyId, PublicKey publicKey, PrivateKey privateKey, String algorithm, String description) { this.keyId = keyId; this.publicKey = publicKey; this.privateKey = privateKey; this.algorithm = algorithm; this.description = description; this.created = Instant.now(); } // Getters public String getKeyId() { return keyId; } public PublicKey getPublicKey() { return publicKey; } public PrivateKey getPrivateKey() { return privateKey; } public String getAlgorithm() { return algorithm; } public Instant getCreated() { return created; } public String getDescription() { return description; } } public String generateKeyPair(String keyId, String description) throws Exception { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); SigningKey signingKey = new SigningKey(keyId, pair.getPublic(), pair.getPrivate(), "RSA", description); signingKeys.put(keyId, signingKey); saveKeyToDisk(signingKey); return keyId; } public SignatureMetadata signImage(ImageReference image, String keyId, String signer, Map<String, String> annotations) throws Exception { SigningKey signingKey = signingKeys.get(keyId); if (signingKey == null) { throw new IllegalArgumentException("Signing key not found: " + keyId); } // Create signature payload String payload = createSignaturePayload(image, annotations); // Sign the payload Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(signingKey.getPrivateKey()); signature.update(payload.getBytes()); byte[] digitalSignature = signature.sign(); String signatureBase64 = Base64.getEncoder().encodeToString(digitalSignature); // Create metadata SignatureMetadata metadata = new SignatureMetadata( image.getDigest(), signatureBase64, keyId, signer); metadata.getAnnotations().putAll(annotations); // Save signature saveSignature(metadata); return metadata; } public boolean verifySignature(ImageReference image, SignatureMetadata metadata) throws Exception { SigningKey signingKey = signingKeys.get(metadata.getKeyId()); if (signingKey == null) { throw new IllegalArgumentException("Signing key not found: " + metadata.getKeyId()); } // Recreate payload String payload = createSignaturePayload(image, metadata.getAnnotations()); // Verify signature Signature signature = Signature.getInstance("SHA256withRSA"); signature.initVerify(signingKey.getPublicKey()); signature.update(payload.getBytes()); byte[] digitalSignature = Base64.getDecoder().decode(metadata.getSignature()); return signature.verify(digitalSignature); } public List<SignatureMetadata> getImageSignatures(String imageDigest) throws Exception { Path signaturesDir = Paths.get(keysDirectory, "signatures"); List<SignatureMetadata> signatures = new ArrayList<>(); if (Files.exists(signaturesDir)) { Files.list(signaturesDir) .filter(path -> path.toString().contains(imageDigest)) .forEach(path -> { try { String content = new String(Files.readAllBytes(path)); SignatureMetadata metadata = objectMapper.readValue( content, SignatureMetadata.class); signatures.add(metadata); } catch (Exception e) { System.err.println("Error reading signature: " + e.getMessage()); } }); } return signatures; } private String createSignaturePayload(ImageReference image, Map<String, String> annotations) { Map<String, Object> payload = new LinkedHashMap<>(); payload.put("critical", Map.of( "identity", Map.of( "docker-reference", image.getRegistry() + "/" + image.getRepository() ), "image", Map.of( "docker-manifest-digest", image.getDigest() ), "type", "cosign container image signature" )); payload.put("optional", annotations); try { return objectMapper.writeValueAsString(payload); } catch (Exception e) { throw new RuntimeException("Failed to create signature payload", e); } } private void saveKeyToDisk(SigningKey signingKey) throws Exception { Path keyDir = Paths.get(keysDirectory, "keys"); Files.createDirectories(keyDir); // Save private key String privateKeyPem = "-----BEGIN PRIVATE KEY-----\n" + Base64.getMimeEncoder().encodeToString(signingKey.getPrivateKey().getEncoded()) + "\n-----END PRIVATE KEY-----"; Files.write(keyDir.resolve(signingKey.getKeyId() + ".key"), privateKeyPem.getBytes()); // Save public key String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" + Base64.getMimeEncoder().encodeToString(signingKey.getPublicKey().getEncoded()) + "\n-----END PUBLIC KEY-----"; Files.write(keyDir.resolve(signingKey.getKeyId() + ".pub"), publicKeyPem.getBytes()); // Save key metadata Map<String, Object> keyInfo = Map.of( "keyId", signingKey.getKeyId(), "algorithm", signingKey.getAlgorithm(), "created", signingKey.getCreated().toString(), "description", signingKey.getDescription() ); String keyInfoJson = objectMapper.writeValueAsString(keyInfo); Files.write(keyDir.resolve(signingKey.getKeyId() + ".json"), keyInfoJson.getBytes()); } private void saveSignature(SignatureMetadata metadata) throws Exception { Path signaturesDir = Paths.get(keysDirectory, "signatures"); Files.createDirectories(signaturesDir); String signatureJson = objectMapper.writeValueAsString(metadata); String filename = String.format("%s-%s.json", metadata.getImageDigest().replace(":", "-"), UUID.randomUUID().toString().substring(0, 8)); Files.write(signaturesDir.resolve(filename), signatureJson.getBytes()); } private void loadExistingKeys() { try { Path keyDir = Paths.get(keysDirectory, "keys"); if (!Files.exists(keyDir)) return; Files.list(keyDir) .filter(path -> path.toString().endsWith(".json")) .forEach(path -> { try { String content = new String(Files.readAllBytes(path)); Map<?, ?> keyInfo = objectMapper.readValue(content, Map.class); String keyId = (String) keyInfo.get("keyId"); // Load keys (simplified - in practice, you'd load the actual keys) System.out.println("Loaded key: " + keyId); } catch (Exception e) { System.err.println("Error loading key: " + e.getMessage()); } }); } catch (Exception e) { System.err.println("Error loading existing keys: " + e.getMessage()); } } public static void main(String[] args) { try { ContainerSigningService service = new ContainerSigningService("./signing-keys"); // Generate a signing key String keyId = service.generateKeyPair("prod-key-1", "Production signing key for container images"); System.out.println("Generated key: " + keyId); // Create image reference ImageReference image = new ImageReference( "docker.io", "myorg/myapp", "v1.0", "sha256:abc123def4567890123456789abcdef1234567890abcdef1234567890abcdef" ); // Sign the image Map<String, String> annotations = new HashMap<>(); annotations.put("build-timestamp", Instant.now().toString()); annotations.put("build-url", "https://ci.example.com/build/123"); annotations.put("vcs-ref", "abc123def"); SignatureMetadata signature = service.signImage( image, keyId, "build-bot", annotations); System.out.println("Image signed: " + signature.getSignature()); // Verify signature boolean isValid = service.verifySignature(image, signature); System.out.println("Signature verification: " + isValid); // List signatures for image List<SignatureMetadata> signatures = service.getImageSignatures(image.getDigest()); System.out.println("Found " + signatures.size() + " signatures for image"); } catch (Exception e) { e.printStackTrace(); } } } Integration with Container Registries
Docker Registry v2 Integration
package com.container.signing.registry; import okhttp3.*; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; import java.nio.charset.StandardCharsets; import java.util.Base64; public class DockerRegistryClient { private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final String registryUrl; private final String username; private final String password; public DockerRegistryClient(String registryUrl, String username, String password) { this.httpClient = new OkHttpClient(); this.objectMapper = new ObjectMapper(); this.registryUrl = registryUrl; this.username = username; this.password = password; } public void attachSignature(String imageName, String imageDigest, String signature, String keyId) throws Exception { // Get manifest String manifest = getManifest(imageName, imageDigest); // Create signature manifest String signatureManifest = createSignatureManifest( imageName, imageDigest, signature, keyId); // Upload signature uploadSignature(imageName, signatureManifest); } public List<String> getSignatures(String imageName, String imageDigest) throws Exception { String url = String.format("%s/v2/%s/manifests/%s", registryUrl, imageName, imageDigest); Request request = new Request.Builder() .url(url) .header("Authorization", getAuthHeader()) .header("Accept", "application/vnd.docker.distribution.manifest.v2+json") .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Failed to get manifest: " + response.code()); } // Parse response to find signatures // This is a simplified version - actual implementation would // need to handle registry-specific signature storage return Collections.emptyList(); } } private String getManifest(String imageName, String imageDigest) throws Exception { String url = String.format("%s/v2/%s/manifests/%s", registryUrl, imageName, imageDigest); Request request = new Request.Builder() .url(url) .header("Authorization", getAuthHeader()) .header("Accept", "application/vnd.docker.distribution.manifest.v2+json") .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Failed to get manifest: " + response.code()); } return response.body().string(); } } private String createSignatureManifest(String imageName, String imageDigest, String signature, String keyId) throws Exception { Map<String, Object> signatureManifest = new HashMap<>(); signatureManifest.put("schemaVersion", 2); signatureManifest.put("mediaType", "application/vnd.docker.distribution.manifest.v2+json"); Map<String, Object> config = new HashMap<>(); config.put("mediaType", "application/vnd.docker.container.image.v1+json"); config.put("size", 0); config.put("digest", "sha256:" + Base64.getEncoder().encodeToString("signature-config".getBytes()).substring(0, 64)); signatureManifest.put("config", config); // Add signature as a layer Map<String, Object> signatureLayer = new HashMap<>(); signatureLayer.put("mediaType", "application/vnd.docker.image.rootfs.diff.tar.gzip"); signatureLayer.put("size", signature.getBytes(StandardCharsets.UTF_8).length); signatureLayer.put("digest", "sha256:" + Base64.getEncoder().encodeToString(signature.getBytes()).substring(0, 64)); signatureManifest.put("layers", Collections.singletonList(signatureLayer)); return objectMapper.writeValueAsString(signatureManifest); } private void uploadSignature(String imageName, String signatureManifest) throws Exception { String url = String.format("%s/v2/%s/manifests/sha256-%s", registryUrl, imageName, UUID.randomUUID().toString()); RequestBody body = RequestBody.create( signatureManifest, MediaType.parse("application/vnd.docker.distribution.manifest.v2+json") ); Request request = new Request.Builder() .url(url) .header("Authorization", getAuthHeader()) .put(body) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Failed to upload signature: " + response.code()); } } } private String getAuthHeader() { String credentials = username + ":" + password; String encoded = Base64.getEncoder().encodeToString(credentials.getBytes()); return "Basic " + encoded; } } Kubernetes Integration for Signature Verification
Admission Controller for Signature Validation
package com.container.signing.kubernetes; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.Configuration; import io.kubernetes.client.openapi.apis.AdmissionregistrationV1Api; import io.kubernetes.client.openapi.models.*; import io.kubernetes.client.util.Config; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; public class SignatureValidationWebhook { private final ContainerSigningService signingService; private final ObjectMapper objectMapper; public SignatureValidationWebhook(ContainerSigningService signingService) { this.signingService = signingService; this.objectMapper = new ObjectMapper(); } public V1AdmissionReview validatePod(V1AdmissionReview admissionRequest) { V1AdmissionReview response = new V1AdmissionReview(); response.apiVersion("admission.k8s.io/v1"); response.kind("AdmissionReview"); V1AdmissionResponse admissionResponse = new V1AdmissionResponse(); admissionResponse.uid(admissionRequest.getRequest().getUid()); try { String requestJson = objectMapper.writeValueAsString(admissionRequest.getRequest().getObject()); V1Pod pod = objectMapper.readValue(requestJson, V1Pod.class); // Validate all containers in the pod List<String> validationErrors = validatePodContainers(pod); if (validationErrors.isEmpty()) { admissionResponse.allowed(true); } else { admissionResponse.allowed(false); admissionResponse.status(new V1Status().message( String.join("; ", validationErrors))); } } catch (Exception e) { admissionResponse.allowed(false); admissionResponse.status(new V1Status().message( "Error validating pod: " + e.getMessage())); } response.setResponse(admissionResponse); return response; } private List<String> validatePodContainers(V1Pod pod) { List<String> errors = new ArrayList<>(); if (pod.getSpec() != null) { // Check init containers if (pod.getSpec().getInitContainers() != null) { pod.getSpec().getInitContainers().forEach(container -> validateContainer(container, "initContainer", errors)); } // Check regular containers if (pod.getSpec().getContainers() != null) { pod.getSpec().getContainers().forEach(container -> validateContainer(container, "container", errors)); } } return errors; } private void validateContainer(V1Container container, String containerType, List<String> errors) { String image = container.getImage(); try { // Parse image reference ContainerSigningService.ImageReference imageRef = parseImageReference(image); // Check if image is signed List<ContainerSigningService.SignatureMetadata> signatures = signingService.getImageSignatures(imageRef.getDigest()); if (signatures.isEmpty()) { errors.add(String.format( "%s '%s': No valid signatures found for image %s", containerType, container.getName(), image)); return; } // Verify at least one valid signature boolean hasValidSignature = signatures.stream().anyMatch(signature -> { try { return signingService.verifySignature(imageRef, signature); } catch (Exception e) { return false; } }); if (!hasValidSignature) { errors.add(String.format( "%s '%s': No valid signature found for image %s", containerType, container.getName(), image)); } } catch (Exception e) { errors.add(String.format( "%s '%s': Error validating image %s - %s", containerType, container.getName(), image, e.getMessage())); } } private ContainerSigningService.ImageReference parseImageReference(String image) { // Simplified parsing - in practice, use a proper image reference parser String[] parts = image.split("@"); String namePart = parts[0]; String digest = parts.length > 1 ? parts[1] : null; String[] nameParts = namePart.split("/"); String registry = nameParts.length > 2 ? nameParts[0] : "docker.io"; String repository = String.join("/", Arrays.copyOfRange(nameParts, nameParts.length > 2 ? 1 : 0, nameParts.length)); String[] repoTagParts = repository.split(":"); String repo = repoTagParts[0]; String tag = repoTagParts.length > 1 ? repoTagParts[1] : "latest"; return new ContainerSigningService.ImageReference(registry, repo, tag, digest); } public static void main(String[] args) throws Exception { // Initialize signing service ContainerSigningService signingService = new ContainerSigningService("./signing-keys"); // Initialize webhook SignatureValidationWebhook webhook = new SignatureValidationWebhook(signingService); // Setup Kubernetes client ApiClient client = Config.defaultClient(); Configuration.setDefaultApiClient(client); // This would typically run as an HTTP server in a real implementation System.out.println("Signature validation webhook initialized"); } } Security Best Practices
Key Management and Rotation
package com.container.signing.security; import java.time.Instant; import java.time.Duration; import java.util.*; import java.security.*; import java.nio.file.*; public class KeyManager { private final Path keysDirectory; private final Duration keyRotationInterval; private final Map<String, KeyInfo> activeKeys; public KeyManager(String keysDirectory, Duration keyRotationInterval) { this.keysDirectory = Paths.get(keysDirectory); this.keyRotationInterval = keyRotationInterval; this.activeKeys = new HashMap<>(); loadKeys(); } public static class KeyInfo { private String keyId; private Instant created; private Instant expires; private KeyPair keyPair; private KeyStatus status; private String description; public enum KeyStatus { ACTIVE, EXPIRED, REVOKED, COMPROMISED } public KeyInfo(String keyId, KeyPair keyPair, String description) { this.keyId = keyId; this.keyPair = keyPair; this.description = description; this.created = Instant.now(); this.expires = created.plus(Duration.ofDays(365)); // 1 year default this.status = KeyStatus.ACTIVE; } // Getters and setters public boolean isExpired() { return Instant.now().isAfter(expires); } public boolean isValid() { return status == KeyStatus.ACTIVE && !isExpired(); } } public String generateNewKey(String description) throws Exception { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair keyPair = keyGen.generateKeyPair(); String keyId = "key-" + UUID.randomUUID().toString().substring(0, 8); KeyInfo keyInfo = new KeyInfo(keyId, keyPair, description); activeKeys.put(keyId, keyInfo); saveKey(keyInfo); return keyId; } public void rotateKeys() throws Exception { Instant now = Instant.now(); List<String> keysToRotate = new ArrayList<>(); // Find keys that need rotation for (KeyInfo keyInfo : activeKeys.values()) { if (keyInfo.isExpired() || Duration.between(keyInfo.created, now).compareTo(keyRotationInterval) > 0) { keysToRotate.add(keyInfo.keyId); } } // Generate new keys for rotation for (String oldKeyId : keysToRotate) { KeyInfo oldKey = activeKeys.get(oldKeyId); oldKey.status = KeyInfo.KeyStatus.EXPIRED; String newKeyId = generateNewKey("Rotated from " + oldKeyId); System.out.println("Rotated key: " + oldKeyId + " -> " + newKeyId); } } public void revokeKey(String keyId, String reason) { KeyInfo keyInfo = activeKeys.get(keyId); if (keyInfo != null) { keyInfo.status = KeyInfo.KeyStatus.REVOKED; System.out.println("Revoked key: " + keyId + " - " + reason); } } public PublicKey getPublicKey(String keyId) { KeyInfo keyInfo = activeKeys.get(keyId); return keyInfo != null && keyInfo.isValid() ? keyInfo.keyPair.getPublic() : null; } public PrivateKey getPrivateKey(String keyId) { KeyInfo keyInfo = activeKeys.get(keyId); return keyInfo != null && keyInfo.isValid() ? keyInfo.keyPair.getPrivate() : null; } public List<String> getValidKeyIds() { return activeKeys.values().stream() .filter(KeyInfo::isValid) .map(key -> key.keyId) .toList(); } private void saveKey(KeyInfo keyInfo) throws Exception { Files.createDirectories(keysDirectory); Map<String, Object> keyData = new HashMap<>(); keyData.put("keyId", keyInfo.keyId); keyData.put("created", keyInfo.created.toString()); keyData.put("expires", keyInfo.expires.toString()); keyData.put("status", keyInfo.status.toString()); keyData.put("description", keyInfo.description); keyData.put("publicKey", Base64.getEncoder().encodeToString(keyInfo.keyPair.getPublic().getEncoded())); keyData.put("privateKey", Base64.getEncoder().encodeToString(keyInfo.keyPair.getPrivate().getEncoded())); // In production, you would encrypt the private key String keyDataJson = new ObjectMapper().writeValueAsString(keyData); Files.write(keysDirectory.resolve(keyInfo.keyId + ".json"), keyDataJson.getBytes()); } private void loadKeys() { try { if (!Files.exists(keysDirectory)) return; Files.list(keysDirectory) .filter(path -> path.toString().endsWith(".json")) .forEach(path -> { try { String content = new String(Files.readAllBytes(path)); Map<?, ?> keyData = new ObjectMapper().readValue(content, Map.class); // Load key data (simplified) String keyId = (String) keyData.get("keyId"); System.out.println("Loaded key: " + keyId); } catch (Exception e) { System.err.println("Error loading key from " + path + ": " + e.getMessage()); } }); } catch (Exception e) { System.err.println("Error loading keys: " + e.getMessage()); } } } Testing Container Image Signing
Unit Tests for Signing Service
package com.container.signing.test; import com.container.signing.ContainerSigningService; import org.junit.jupiter.api.*; import java.util.*; import static org.junit.jupiter.api.Assertions.*; public class ContainerSigningServiceTest { private ContainerSigningService signingService; @BeforeEach void setUp() throws Exception { signingService = new ContainerSigningService("./test-keys"); } @Test void testGenerateKeyPair() throws Exception { String keyId = signingService.generateKeyPair("test-key", "Test key"); assertNotNull(keyId); assertTrue(keyId.startsWith("test-key")); } @Test void testSignAndVerifyImage() throws Exception { // Generate key String keyId = signingService.generateKeyPair("verify-key", "Verification test key"); // Create test image reference ContainerSigningService.ImageReference image = new ContainerSigningService.ImageReference( "docker.io", "testorg/testimage", "v1.0", "sha256:test123456789012345678901234567890123456789012345678901234567890" ); // Sign image Map<String, String> annotations = new HashMap<>(); annotations.put("test", "true"); annotations.put("timestamp", new Date().toString()); ContainerSigningService.SignatureMetadata signature = signingService.signImage(image, keyId, "test-user", annotations); assertNotNull(signature); assertNotNull(signature.getSignature()); // Verify signature boolean isValid = signingService.verifySignature(image, signature); assertTrue(isValid); } @Test void testTamperedImageRejection() throws Exception { // Generate key String keyId = signingService.generateKeyPair("tamper-key", "Tamper test key"); // Create and sign image ContainerSigningService.ImageReference originalImage = new ContainerSigningService.ImageReference( "docker.io", "testorg/tampertest", "v1.0", "sha256:original12345678901234567890123456789012345678901234567890123456" ); ContainerSigningService.SignatureMetadata signature = signingService.signImage(originalImage, keyId, "test-user", new HashMap<>()); // Try to verify with tampered image ContainerSigningService.ImageReference tamperedImage = new ContainerSigningService.ImageReference( "docker.io", "testorg/tampertest", "v1.0", "sha256:tampered1234567890123456789012345678901234567890123456789012345" ); boolean isValid = signingService.verifySignature(tamperedImage, signature); assertFalse(isValid); } @AfterEach void tearDown() throws Exception { // Clean up test files // Files.walk(Paths.get("./test-keys")).forEach(path -> path.toFile().delete()); } } Conclusion
Container image signing is essential for securing your containerized applications. This comprehensive implementation covers:
- Basic Signing: RSA-based signature generation and verification
- Key Management: Secure key generation, storage, and rotation
- Registry Integration: Storing and retrieving signatures from container registries
- Kubernetes Integration: Validating signatures in admission controllers
- Security Best Practices: Proper key management and security considerations
Key Security Considerations:
- Store private keys securely (HSM, cloud KMS, etc.)
- Implement proper key rotation policies
- Use strong cryptographic algorithms
- Validate signatures at multiple points in your pipeline
- Monitor and audit signing activities
This implementation provides a solid foundation for building secure container image signing into your Java-based container workflows.