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.