A comprehensive implementation of a Notary service for container image trust and verification using TUF (The Update Framework) and in-toto attestations.
Complete Implementation
1. Core Notary Service
package com.notary.core; import java.security.*; import java.security.spec.*; import java.util.*; import java.time.Instant; import java.nio.file.*; /** * Main Notary service for container image trust and verification */ public class ImageNotaryService { private final KeyManager keyManager; private final TrustRepository trustRepository; private final SignatureService signatureService; private final VerificationService verificationService; private final PolicyEngine policyEngine; public ImageNotaryService(NotaryConfig config) { this.keyManager = new KeyManager(config); this.trustRepository = new TrustRepository(config); this.signatureService = new SignatureService(keyManager); this.verificationService = new VerificationService(keyManager, trustRepository); this.policyEngine = new PolicyEngine(config); initializeRootKeys(); } /** * Sign a container image */ public ImageSignature signImage(ImageReference imageRef, SigningRequest request) { try { // Validate image exists and get manifest ImageManifest manifest = fetchImageManifest(imageRef); // Create signature payload SignaturePayload payload = createSignaturePayload(imageRef, manifest, request); // Sign the payload DigitalSignature signature = signatureService.sign(payload); // Create and store signature record ImageSignature imageSignature = ImageSignature.builder() .imageReference(imageRef) .signature(signature) .payload(payload) .timestamp(Instant.now()) .signerIdentity(request.getSignerIdentity()) .build(); // Store signature in trust repository trustRepository.storeSignature(imageSignature); // Create attestation if requested if (request.isCreateAttestation()) { createImageAttestation(imageRef, imageSignature, request); } return imageSignature; } catch (Exception e) { throw new NotaryException("Failed to sign image: " + imageRef, e); } } /** * Verify image signature and trust */ public VerificationResult verifyImage(ImageReference imageRef, VerificationPolicy policy) { try { VerificationContext context = new VerificationContext(imageRef, policy); // Step 1: Fetch image manifest ImageManifest manifest = fetchImageManifest(imageRef); context.setImageManifest(manifest); // Step 2: Fetch signatures for the image List<ImageSignature> signatures = trustRepository.getSignatures(imageRef); context.setSignatures(signatures); // Step 3: Verify signatures SignatureVerification signatureResult = verificationService.verifySignatures(context); context.setSignatureVerification(signatureResult); // Step 4: Check revocation status RevocationCheck revocationResult = verificationService.checkRevocation(context); context.setRevocationCheck(revocationResult); // Step 5: Verify attestations AttestationVerification attestationResult = verificationService.verifyAttestations(context); context.setAttestationVerification(attestationResult); // Step 6: Apply policy rules PolicyEvaluation policyResult = policyEngine.evaluate(context); context.setPolicyEvaluation(policyResult); // Step 7: Generate final result return buildVerificationResult(context); } catch (Exception e) { throw new NotaryException("Failed to verify image: " + imageRef, e); } } /** * Add a new trusted identity */ public void addTrustedIdentity(TrustedIdentity identity) { try { trustRepository.addTrustedIdentity(identity); } catch (Exception e) { throw new NotaryException("Failed to add trusted identity: " + identity.getName(), e); } } /** * Revoke a signature or identity */ public void revokeSignature(RevocationRequest request) { try { trustRepository.revokeSignature(request); } catch (Exception e) { throw new NotaryException("Failed to revoke signature", e); } } /** * Initialize TUF root metadata */ public void initializeRootMetadata(RootMetadata root) { try { trustRepository.initializeRoot(root); } catch (Exception e) { throw new NotaryException("Failed to initialize root metadata", e); } } /** * Update TUF metadata */ public void updateMetadata(MetadataType type, BaseMetadata metadata) { try { trustRepository.updateMetadata(type, metadata); } catch (Exception e) { throw new NotaryException("Failed to update metadata: " + type, e); } } private ImageManifest fetchImageManifest(ImageReference imageRef) { // Implementation would integrate with container registry // For demo, return a mock manifest return ImageManifest.builder() .digest("sha256:abc123...") .mediaType("application/vnd.docker.distribution.manifest.v2+json") .size(1024) .build(); } private SignaturePayload createSignaturePayload(ImageReference imageRef, ImageManifest manifest, SigningRequest request) { return SignaturePayload.builder() .imageDigest(manifest.getDigest()) .imageReference(imageRef.toString()) .timestamp(Instant.now()) .expiresAt(request.getExpiresAt()) .signerIdentity(request.getSignerIdentity()) .purpose(request.getPurpose()) .customClaims(request.getCustomClaims()) .build(); } private void createImageAttestation(ImageReference imageRef, ImageSignature signature, SigningRequest request) { ImageAttestation attestation = ImageAttestation.builder() .imageReference(imageRef) .signature(signature) .attestationType(request.getAttestationType()) .predicate(createAttestationPredicate(request)) .build(); trustRepository.storeAttestation(attestation); } private Map<String, Object> createAttestationPredicate(SigningRequest request) { Map<String, Object> predicate = new HashMap<>(); predicate.put("buildTimestamp", Instant.now().toString()); predicate.put("buildUrl", request.getBuildUrl()); predicate.put("vcsCommit", request.getVcsCommit()); predicate.put("vcsUrl", request.getVcsUrl()); return predicate; } private VerificationResult buildVerificationResult(VerificationContext context) { boolean trusted = context.getSignatureVerification().isValid() && context.getRevocationCheck().isNotRevoked() && context.getPolicyEvaluation().isCompliant(); return VerificationResult.builder() .trusted(trusted) .imageReference(context.getImageReference()) .signatureVerification(context.getSignatureVerification()) .revocationCheck(context.getRevocationCheck()) .attestationVerification(context.getAttestationVerification()) .policyEvaluation(context.getPolicyEvaluation()) .verifiedAt(Instant.now()) .build(); } private void initializeRootKeys() { // Initialize root keys if they don't exist if (!keyManager.rootKeysExist()) { keyManager.generateRootKeys(); } } } 2. Domain Models
/** * Image reference (e.g., "registry.example.com/app:v1.0") */ public class ImageReference { private final String registry; private final String repository; private final String tag; private final String digest; public ImageReference(String registry, String repository, String tag, String digest) { this.registry = registry; this.repository = repository; this.tag = tag; this.digest = digest; } public static ImageReference parse(String reference) { // Parse image reference string // Implementation would parse registry/repository:tag@digest format return new ImageReference("registry.example.com", "library/app", "v1.0", null); } @Override public String toString() { StringBuilder sb = new StringBuilder(); if (registry != null) { sb.append(registry).append("/"); } sb.append(repository); if (tag != null) { sb.append(":").append(tag); } if (digest != null) { sb.append("@").append(digest); } return sb.toString(); } // Getters public String getRegistry() { return registry; } public String getRepository() { return repository; } public String getTag() { return tag; } public String getDigest() { return digest; } } /** * Image signature */ public class ImageSignature { private final String id; private final ImageReference imageReference; private final DigitalSignature signature; private final SignaturePayload payload; private final Instant timestamp; private final String signerIdentity; private final Instant expiresAt; private ImageSignature(Builder builder) { this.id = builder.id; this.imageReference = builder.imageReference; this.signature = builder.signature; this.payload = builder.payload; this.timestamp = builder.timestamp; this.signerIdentity = builder.signerIdentity; this.expiresAt = builder.expiresAt; } public static Builder builder() { return new Builder(); } public static class Builder { private String id = UUID.randomUUID().toString(); private ImageReference imageReference; private DigitalSignature signature; private SignaturePayload payload; private Instant timestamp = Instant.now(); private String signerIdentity; private Instant expiresAt; public Builder id(String id) { this.id = id; return this; } public Builder imageReference(ImageReference imageReference) { this.imageReference = imageReference; return this; } public Builder signature(DigitalSignature signature) { this.signature = signature; return this; } public Builder payload(SignaturePayload payload) { this.payload = payload; return this; } public Builder timestamp(Instant timestamp) { this.timestamp = timestamp; return this; } public Builder signerIdentity(String signerIdentity) { this.signerIdentity = signerIdentity; return this; } public Builder expiresAt(Instant expiresAt) { this.expiresAt = expiresAt; return this; } public ImageSignature build() { return new ImageSignature(this); } } // Getters public String getId() { return id; } public ImageReference getImageReference() { return imageReference; } public DigitalSignature getSignature() { return signature; } public SignaturePayload getPayload() { return payload; } public Instant getTimestamp() { return timestamp; } public String getSignerIdentity() { return signerIdentity; } public Instant getExpiresAt() { return expiresAt; } } /** * Digital signature */ public class DigitalSignature { private final String algorithm; private final byte[] signature; private final String keyId; private final PublicKey publicKey; public DigitalSignature(String algorithm, byte[] signature, String keyId, PublicKey publicKey) { this.algorithm = algorithm; this.signature = signature != null ? signature.clone() : null; this.keyId = keyId; this.publicKey = publicKey; } // Getters public String getAlgorithm() { return algorithm; } public byte[] getSignature() { return signature != null ? signature.clone() : null; } public String getKeyId() { return keyId; } public PublicKey getPublicKey() { return publicKey; } } /** * Signature payload */ public class SignaturePayload { private final String imageDigest; private final String imageReference; private final Instant timestamp; private final Instant expiresAt; private final String signerIdentity; private final String purpose; private final Map<String, Object> customClaims; private SignaturePayload(Builder builder) { this.imageDigest = builder.imageDigest; this.imageReference = builder.imageReference; this.timestamp = builder.timestamp; this.expiresAt = builder.expiresAt; this.signerIdentity = builder.signerIdentity; this.purpose = builder.purpose; this.customClaims = builder.customClaims; } public byte[] toBytes() { try { // Convert to JSON bytes for signing Map<String, Object> payloadMap = new HashMap<>(); payloadMap.put("imageDigest", imageDigest); payloadMap.put("imageReference", imageReference); payloadMap.put("timestamp", timestamp.toString()); if (expiresAt != null) { payloadMap.put("expiresAt", expiresAt.toString()); } payloadMap.put("signerIdentity", signerIdentity); payloadMap.put("purpose", purpose); payloadMap.put("customClaims", customClaims); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsBytes(payloadMap); } catch (Exception e) { throw new NotaryException("Failed to serialize signature payload", e); } } public static Builder builder() { return new Builder(); } public static class Builder { private String imageDigest; private String imageReference; private Instant timestamp = Instant.now(); private Instant expiresAt; private String signerIdentity; private String purpose = "general"; private Map<String, Object> customClaims = new HashMap<>(); public Builder imageDigest(String imageDigest) { this.imageDigest = imageDigest; return this; } public Builder imageReference(String imageReference) { this.imageReference = imageReference; return this; } public Builder timestamp(Instant timestamp) { this.timestamp = timestamp; return this; } public Builder expiresAt(Instant expiresAt) { this.expiresAt = expiresAt; return this; } public Builder signerIdentity(String signerIdentity) { this.signerIdentity = signerIdentity; return this; } public Builder purpose(String purpose) { this.purpose = purpose; return this; } public Builder customClaims(Map<String, Object> customClaims) { this.customClaims = customClaims; return this; } public Builder customClaim(String key, Object value) { this.customClaims.put(key, value); return this; } public SignaturePayload build() { return new SignaturePayload(this); } } // Getters public String getImageDigest() { return imageDigest; } public String getImageReference() { return imageReference; } public Instant getTimestamp() { return timestamp; } public Instant getExpiresAt() { return expiresAt; } public String getSignerIdentity() { return signerIdentity; } public String getPurpose() { return purpose; } public Map<String, Object> getCustomClaims() { return customClaims; } } /** * Verification result */ public class VerificationResult { private final boolean trusted; private final ImageReference imageReference; private final SignatureVerification signatureVerification; private final RevocationCheck revocationCheck; private final AttestationVerification attestationVerification; private final PolicyEvaluation policyEvaluation; private final Instant verifiedAt; private VerificationResult(Builder builder) { this.trusted = builder.trusted; this.imageReference = builder.imageReference; this.signatureVerification = builder.signatureVerification; this.revocationCheck = builder.revocationCheck; this.attestationVerification = builder.attestationVerification; this.policyEvaluation = builder.policyEvaluation; this.verifiedAt = builder.verifiedAt; } public static Builder builder() { return new Builder(); } public static class Builder { private boolean trusted; private ImageReference imageReference; private SignatureVerification signatureVerification; private RevocationCheck revocationCheck; private AttestationVerification attestationVerification; private PolicyEvaluation policyEvaluation; private Instant verifiedAt = Instant.now(); public Builder trusted(boolean trusted) { this.trusted = trusted; return this; } public Builder imageReference(ImageReference imageReference) { this.imageReference = imageReference; return this; } public Builder signatureVerification(SignatureVerification signatureVerification) { this.signatureVerification = signatureVerification; return this; } public Builder revocationCheck(RevocationCheck revocationCheck) { this.revocationCheck = revocationCheck; return this; } public Builder attestationVerification(AttestationVerification attestationVerification) { this.attestationVerification = attestationVerification; return this; } public Builder policyEvaluation(PolicyEvaluation policyEvaluation) { this.policyEvaluation = policyEvaluation; return this; } public Builder verifiedAt(Instant verifiedAt) { this.verifiedAt = verifiedAt; return this; } public VerificationResult build() { return new VerificationResult(this); } } // Getters public boolean isTrusted() { return trusted; } public ImageReference getImageReference() { return imageReference; } public SignatureVerification getSignatureVerification() { return signatureVerification; } public RevocationCheck getRevocationCheck() { return revocationCheck; } public AttestationVerification getAttestationVerification() { return attestationVerification; } public PolicyEvaluation getPolicyEvaluation() { return policyEvaluation; } public Instant getVerifiedAt() { return verifiedAt; } } 3. TUF (The Update Framework) Implementation
/** * TUF Root Metadata */ public class RootMetadata { private final int version; private final Instant expires; private final Map<String, Key> keys; private final Map<String, Role> roles; private final Map<String, Signature> signatures; public RootMetadata(int version, Instant expires, Map<String, Key> keys, Map<String, Role> roles, Map<String, Signature> signatures) { this.version = version; this.expires = expires; this.keys = new HashMap<>(keys); this.roles = new HashMap<>(roles); this.signatures = new HashMap<>(signatures); } public boolean isExpired() { return Instant.now().isAfter(expires); } public boolean verifySignatures() { // Verify that root metadata is properly signed by trusted keys Role rootRole = roles.get("root"); if (rootRole == null) { return false; } int verifiedSignatures = 0; for (Signature signature : signatures.values()) { Key key = keys.get(signature.getKeyId()); if (key != null && key.verifySignature(this, signature)) { verifiedSignatures++; } } return verifiedSignatures >= rootRole.getThreshold(); } // Getters public int getVersion() { return version; } public Instant getExpires() { return expires; } public Map<String, Key> getKeys() { return new HashMap<>(keys); } public Map<String, Role> getRoles() { return new HashMap<>(roles); } public Map<String, Signature> getSignatures() { return new HashMap<>(signatures); } } /** * TUF Key representation */ public class Key { private final String keyId; private final String keyType; private final String scheme; private final PublicKey publicKey; private final Map<String, Object> keyVal; public Key(String keyId, String keyType, String scheme, PublicKey publicKey, Map<String, Object> keyVal) { this.keyId = keyId; this.keyType = keyType; this.scheme = scheme; this.publicKey = publicKey; this.keyVal = new HashMap<>(keyVal); } public boolean verifySignature(BaseMetadata metadata, Signature signature) { try { java.security.Signature verifier = java.security.Signature.getInstance(scheme); verifier.initVerify(publicKey); verifier.update(metadata.getSignedBytes()); return verifier.verify(signature.getSignature()); } catch (Exception e) { return false; } } // Getters public String getKeyId() { return keyId; } public String getKeyType() { return keyType; } public String getScheme() { return scheme; } public PublicKey getPublicKey() { return publicKey; } public Map<String, Object> getKeyVal() { return new HashMap<>(keyVal); } } /** * TUF Role definition */ public class Role { private final String name; private final int threshold; private final List<String> keyIds; public Role(String name, int threshold, List<String> keyIds) { this.name = name; this.threshold = threshold; this.keyIds = new ArrayList<>(keyIds); } // Getters public String getName() { return name; } public int getThreshold() { return threshold; } public List<String> getKeyIds() { return new ArrayList<>(keyIds); } } /** * TUF Signature */ public class Signature { private final String keyId; private final byte[] signature; public Signature(String keyId, byte[] signature) { this.keyId = keyId; this.signature = signature != null ? signature.clone() : null; } // Getters public String getKeyId() { return keyId; } public byte[] getSignature() { return signature != null ? signature.clone() : null; } } /** * TUF Target Metadata */ public class TargetMetadata extends BaseMetadata { private final Map<String, Target> targets; public TargetMetadata(int version, Instant expires, Map<String, Target> targets, Map<String, Signature> signatures) { super(version, expires, signatures); this.targets = new HashMap<>(targets); } public Target getTarget(String targetName) { return targets.get(targetName); } public boolean hasTarget(String targetName) { return targets.containsKey(targetName); } // Getters public Map<String, Target> getTargets() { return new HashMap<>(targets); } } /** * TUF Target */ public class Target { private final String name; private final long length; private final Map<String, String> hashes; private final Map<String, Object> custom; public Target(String name, long length, Map<String, String> hashes, Map<String, Object> custom) { this.name = name; this.length = length; this.hashes = new HashMap<>(hashes); this.custom = new HashMap<>(custom); } public boolean verifyHashes(byte[] data) { for (Map.Entry<String, String> hashEntry : hashes.entrySet()) { String algorithm = hashEntry.getKey(); String expectedHash = hashEntry.getValue(); String actualHash = calculateHash(data, algorithm); if (!expectedHash.equals(actualHash)) { return false; } } return true; } private String calculateHash(byte[] data, String algorithm) { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] hash = digest.digest(data); return bytesToHex(hash); } catch (NoSuchAlgorithmException e) { throw new NotaryException("Unsupported hash algorithm: " + algorithm, e); } } private String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } // Getters public String getName() { return name; } public long getLength() { return length; } public Map<String, String> getHashes() { return new HashMap<>(hashes); } public Map<String, Object> getCustom() { return new HashMap<>(custom); } } 4. Key Management
/** * Cryptographic key manager */ public class KeyManager { private final NotaryConfig config; private final KeyStorage keyStorage; private final Map<String, KeyPair> keyCache; public KeyManager(NotaryConfig config) { this.config = config; this.keyStorage = new FileKeyStorage(config.getKeyStoragePath()); this.keyCache = new ConcurrentHashMap<>(); } /** * Generate root keys for TUF */ public void generateRootKeys() { try { // Generate root key KeyPair rootKeyPair = generateKeyPair("RSA", 4096); storeKey("root", rootKeyPair, KeyType.ROOT); // Generate targets key KeyPair targetsKeyPair = generateKeyPair("RSA", 4096); storeKey("targets", targetsKeyPair, KeyType.TARGETS); // Generate snapshot key KeyPair snapshotKeyPair = generateKeyPair("RSA", 4096); storeKey("snapshot", snapshotKeyPair, KeyType.SNAPSHOT); // Generate timestamp key KeyPair timestampKeyPair = generateKeyPair("RSA", 4096); storeKey("timestamp", timestampKeyPair, KeyType.TIMESTAMP); } catch (Exception e) { throw new NotaryException("Failed to generate root keys", e); } } /** * Generate a key pair for signing */ public KeyPair generateSigningKey(String keyId, KeyType keyType) { try { KeyPair keyPair = generateKeyPair("RSA", 2048); storeKey(keyId, keyPair, keyType); return keyPair; } catch (Exception e) { throw new NotaryException("Failed to generate signing key: " + keyId, e); } } /** * Get public key by ID */ public PublicKey getPublicKey(String keyId) { try { KeyPair keyPair = loadKey(keyId); return keyPair.getPublic(); } catch (Exception e) { throw new NotaryException("Failed to get public key: " + keyId, e); } } /** * Sign data with specified key */ public byte[] sign(String keyId, byte[] data) { try { KeyPair keyPair = loadKey(keyId); PrivateKey privateKey = keyPair.getPrivate(); java.security.Signature signature = java.security.Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(data); return signature.sign(); } catch (Exception e) { throw new NotaryException("Failed to sign data with key: " + keyId, e); } } /** * Verify signature */ public boolean verify(String keyId, byte[] data, byte[] signatureBytes) { try { PublicKey publicKey = getPublicKey(keyId); java.security.Signature signature = java.security.Signature.getInstance("SHA256withRSA"); signature.initVerify(publicKey); signature.update(data); return signature.verify(signatureBytes); } catch (Exception e) { return false; } } /** * Check if root keys exist */ public boolean rootKeysExist() { return keyExists("root") && keyExists("targets") && keyExists("snapshot") && keyExists("timestamp"); } private KeyPair generateKeyPair(String algorithm, int keySize) throws NoSuchAlgorithmException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm); keyGen.initialize(keySize); return keyGen.generateKeyPair(); } private void storeKey(String keyId, KeyPair keyPair, KeyType keyType) { keyCache.put(keyId, keyPair); keyStorage.storeKey(keyId, keyPair, keyType); } private KeyPair loadKey(String keyId) { // Check cache first KeyPair cached = keyCache.get(keyId); if (cached != null) { return cached; } // Load from storage KeyPair keyPair = keyStorage.loadKey(keyId); if (keyPair != null) { keyCache.put(keyId, keyPair); return keyPair; } throw new NotaryException("Key not found: " + keyId); } private boolean keyExists(String keyId) { return keyCache.containsKey(keyId) || keyStorage.keyExists(keyId); } } /** * Key storage interface */ interface KeyStorage { void storeKey(String keyId, KeyPair keyPair, KeyType keyType); KeyPair loadKey(String keyId); boolean keyExists(String keyId); void deleteKey(String keyId); } /** * File-based key storage */ class FileKeyStorage implements KeyStorage { private final Path storagePath; public FileKeyStorage(String basePath) { this.storagePath = Paths.get(basePath); createStorageDirectory(); } @Override public void storeKey(String keyId, KeyPair keyPair, KeyType keyType) { try { Path keyPath = getKeyPath(keyId, keyType); Files.createDirectories(keyPath.getParent()); // Store private key Path privateKeyPath = keyPath.resolve("private.key"); try (FileOutputStream fos = new FileOutputStream(privateKeyPath.toFile())) { fos.write(keyPair.getPrivate().getEncoded()); } // Store public key Path publicKeyPath = keyPath.resolve("public.key"); try (FileOutputStream fos = new FileOutputStream(publicKeyPath.toFile())) { fos.write(keyPair.getPublic().getEncoded()); } // Store metadata Path metadataPath = keyPath.resolve("metadata.json"); Map<String, Object> metadata = new HashMap<>(); metadata.put("keyId", keyId); metadata.put("keyType", keyType.name()); metadata.put("algorithm", keyPair.getPublic().getAlgorithm()); metadata.put("createdAt", Instant.now().toString()); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(metadataPath.toFile(), metadata); } catch (Exception e) { throw new NotaryException("Failed to store key: " + keyId, e); } } @Override public KeyPair loadKey(String keyId) { try { // Find key directory (search through all key types) for (KeyType keyType : KeyType.values()) { Path keyPath = getKeyPath(keyId, keyType); if (Files.exists(keyPath)) { return loadKeyFromPath(keyPath, keyId); } } return null; } catch (Exception e) { throw new NotaryException("Failed to load key: " + keyId, e); } } @Override public boolean keyExists(String keyId) { for (KeyType keyType : KeyType.values()) { Path keyPath = getKeyPath(keyId, keyType); if (Files.exists(keyPath)) { return true; } } return false; } @Override public void deleteKey(String keyId) { try { for (KeyType keyType : KeyType.values()) { Path keyPath = getKeyPath(keyId, keyType); if (Files.exists(keyPath)) { deleteRecursively(keyPath); } } } catch (Exception e) { throw new NotaryException("Failed to delete key: " + keyId, e); } } private KeyPair loadKeyFromPath(Path keyPath, String keyId) throws Exception { // Load private key Path privateKeyPath = keyPath.resolve("private.key"); byte[] privateKeyBytes = Files.readAllBytes(privateKeyPath); // Load public key Path publicKeyPath = keyPath.resolve("public.key"); byte[] publicKeyBytes = Files.readAllBytes(publicKeyPath); // Reconstruct key pair KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes); PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes); PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); return new KeyPair(publicKey, privateKey); } private Path getKeyPath(String keyId, KeyType keyType) { return storagePath.resolve(keyType.name().toLowerCase()).resolve(keyId); } private void createStorageDirectory() { try { Files.createDirectories(storagePath); } catch (Exception e) { throw new NotaryException("Failed to create key storage directory", e); } } private void deleteRecursively(Path path) throws IOException { if (Files.isDirectory(path)) { try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { for (Path entry : stream) { deleteRecursively(entry); } } } Files.delete(path); } } /** * Key types */ enum KeyType { ROOT, TARGETS, SNAPSHOT, TIMESTAMP, SIGNING } 5. Signature Service
/** * Signature service for creating and verifying digital signatures */ public class SignatureService { private final KeyManager keyManager; public SignatureService(KeyManager keyManager) { this.keyManager = keyManager; } /** * Create a digital signature */ public DigitalSignature sign(SignaturePayload payload) { try { // Use the default signing key String keyId = "signing-1"; // Convert payload to bytes byte[] payloadBytes = payload.toBytes(); // Create signature byte[] signatureBytes = keyManager.sign(keyId, payloadBytes); // Get public key for verification PublicKey publicKey = keyManager.getPublicKey(keyId); return new DigitalSignature("SHA256withRSA", signatureBytes, keyId, publicKey); } catch (Exception e) { throw new NotaryException("Failed to create signature", e); } } /** * Verify a digital signature */ public boolean verify(SignaturePayload payload, DigitalSignature signature) { try { byte[] payloadBytes = payload.toBytes(); return keyManager.verify(signature.getKeyId(), payloadBytes, signature.getSignature()); } catch (Exception e) { return false; } } /** * Verify multiple signatures */ public SignatureVerification verifySignatures(VerificationContext context) { List<ImageSignature> signatures = context.getSignatures(); ImageManifest manifest = context.getImageManifest(); List<ImageSignature> validSignatures = new ArrayList<>(); List<ImageSignature> invalidSignatures = new ArrayList<>(); List<VerificationError> errors = new ArrayList<>(); for (ImageSignature signature : signatures) { try { SignaturePayload payload = signature.getPayload(); // Verify payload matches the image if (!payload.getImageDigest().equals(manifest.getDigest())) { errors.add(new VerificationError( "PAYLOAD_MISMATCH", "Signature payload does not match image digest" )); invalidSignatures.add(signature); continue; } // Verify signature expiration if (signature.getExpiresAt() != null && Instant.now().isAfter(signature.getExpiresAt())) { errors.add(new VerificationError( "SIGNATURE_EXPIRED", "Signature has expired" )); invalidSignatures.add(signature); continue; } // Verify cryptographic signature boolean valid = verify(payload, signature.getSignature()); if (valid) { validSignatures.add(signature); } else { errors.add(new VerificationError( "INVALID_SIGNATURE", "Cryptographic signature verification failed" )); invalidSignatures.add(signature); } } catch (Exception e) { errors.add(new VerificationError( "VERIFICATION_ERROR", "Error during signature verification: " + e.getMessage() )); invalidSignatures.add(signature); } } return SignatureVerification.builder() .validSignatures(validSignatures) .invalidSignatures(invalidSignatures) .errors(errors) .valid(!validSignatures.isEmpty()) .build(); } } /** * Signature verification result */ public class SignatureVerification { private final boolean valid; private final List<ImageSignature> validSignatures; private final List<ImageSignature> invalidSignatures; private final List<VerificationError> errors; private SignatureVerification(Builder builder) { this.valid = builder.valid; this.validSignatures = builder.validSignatures; this.invalidSignatures = builder.invalidSignatures; this.errors = builder.errors; } public static Builder builder() { return new Builder(); } public static class Builder { private boolean valid; private List<ImageSignature> validSignatures = new ArrayList<>(); private List<ImageSignature> invalidSignatures = new ArrayList<>(); private List<VerificationError> errors = new ArrayList<>(); public Builder valid(boolean valid) { this.valid = valid; return this; } public Builder validSignatures(List<ImageSignature> validSignatures) { this.validSignatures = validSignatures; return this; } public Builder invalidSignatures(List<ImageSignature> invalidSignatures) { this.invalidSignatures = invalidSignatures; return this; } public Builder errors(List<VerificationError> errors) { this.errors = errors; return this; } public SignatureVerification build() { return new SignatureVerification(this); } } // Getters public boolean isValid() { return valid; } public List<ImageSignature> getValidSignatures() { return validSignatures; } public List<ImageSignature> getInvalidSignatures() { return invalidSignatures; } public List<VerificationError> getErrors() { return errors; } } /** * Verification error */ public class VerificationError { private final String code; private final String message; public VerificationError(String code, String message) { this.code = code; this.message = message; } // Getters public String getCode() { return code; } public String getMessage() { return message; } } 6. Trust Repository
/** * Trust repository for storing and retrieving trust metadata */ public class TrustRepository { private final NotaryConfig config; private final MetadataStorage metadataStorage; private final SignatureStorage signatureStorage; private final AttestationStorage attestationStorage; public TrustRepository(NotaryConfig config) { this.config = config; this.metadataStorage = new FileMetadataStorage(config.getMetadataPath()); this.signatureStorage = new FileSignatureStorage(config.getSignaturesPath()); this.attestationStorage = new FileAttestationStorage(config.getAttestationsPath()); } /** * Initialize TUF root metadata */ public void initializeRoot(RootMetadata root) { metadataStorage.storeRoot(root); } /** * Update TUF metadata */ public void updateMetadata(MetadataType type, BaseMetadata metadata) { metadataStorage.storeMetadata(type, metadata); } /** * Store image signature */ public void storeSignature(ImageSignature signature) { signatureStorage.storeSignature(signature); } /** * Get signatures for an image */ public List<ImageSignature> getSignatures(ImageReference imageRef) { return signatureStorage.getSignatures(imageRef); } /** * Store image attestation */ public void storeAttestation(ImageAttestation attestation) { attestationStorage.storeAttestation(attestation); } /** * Get attestations for an image */ public List<ImageAttestation> getAttestations(ImageReference imageRef) { return attestationStorage.getAttestations(imageRef); } /** * Add trusted identity */ public void addTrustedIdentity(TrustedIdentity identity) { // Implementation would store trusted identity } /** * Revoke signature */ public void revokeSignature(RevocationRequest request) { signatureStorage.revokeSignature(request.getSignatureId(), request.getReason()); } /** * Get current root metadata */ public RootMetadata getRootMetadata() { return metadataStorage.getRootMetadata(); } /** * Get target metadata */ public TargetMetadata getTargetMetadata() { return metadataStorage.getTargetMetadata(); } } /** * Metadata storage interface */ interface MetadataStorage { void storeRoot(RootMetadata root); void storeMetadata(MetadataType type, BaseMetadata metadata); RootMetadata getRootMetadata(); TargetMetadata getTargetMetadata(); SnapshotMetadata getSnapshotMetadata(); TimestampMetadata getTimestampMetadata(); } /** * File-based metadata storage */ class FileMetadataStorage implements MetadataStorage { private final Path metadataPath; public FileMetadataStorage(String basePath) { this.metadataPath = Paths.get(basePath); createStorageDirectory(); } @Override public void storeRoot(RootMetadata root) { storeMetadata(MetadataType.ROOT, root); } @Override public void storeMetadata(MetadataType type, BaseMetadata metadata) { try { Path metadataPath = getMetadataPath(type); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(metadataPath.toFile(), convertToMap(metadata)); } catch (Exception e) { throw new NotaryException("Failed to store metadata: " + type, e); } } @Override public RootMetadata getRootMetadata() { return (RootMetadata) loadMetadata(MetadataType.ROOT, RootMetadata.class); } @Override public TargetMetadata getTargetMetadata() { return (TargetMetadata) loadMetadata(MetadataType.TARGETS, TargetMetadata.class); } @Override public SnapshotMetadata getSnapshotMetadata() { return (SnapshotMetadata) loadMetadata(MetadataType.SNAPSHOT, SnapshotMetadata.class); } @Override public TimestampMetadata getTimestampMetadata() { return (TimestampMetadata) loadMetadata(MetadataType.TIMESTAMP, TimestampMetadata.class); } private BaseMetadata loadMetadata(MetadataType type, Class<? extends BaseMetadata> clazz) { try { Path metadataPath = getMetadataPath(type); if (!Files.exists(metadataPath)) { return null; } ObjectMapper mapper = new ObjectMapper(); Map<String, Object> metadataMap = mapper.readValue(metadataPath.toFile(), Map.class); return convertFromMap(metadataMap, clazz); } catch (Exception e) { throw new NotaryException("Failed to load metadata: " + type, e); } } private Path getMetadataPath(MetadataType type) { return metadataPath.resolve(type.name().toLowerCase() + ".json"); } private void createStorageDirectory() { try { Files.createDirectories(metadataPath); } catch (Exception e) { throw new NotaryException("Failed to create metadata storage directory", e); } } private Map<String, Object> convertToMap(BaseMetadata metadata) { // Convert metadata to map for JSON serialization // Implementation would use reflection or manual mapping return new HashMap<>(); } private BaseMetadata convertFromMap(Map<String, Object> metadataMap, Class<? extends BaseMetadata> clazz) { // Convert map back to metadata object // Implementation would use reflection or manual mapping return null; } } /** * Metadata types */ enum MetadataType { ROOT, TARGETS, SNAPSHOT, TIMESTAMP } 7. Policy Engine
/** * Policy engine for evaluating trust policies */ public class PolicyEngine { private final NotaryConfig config; private final List<TrustPolicy> policies; public PolicyEngine(NotaryConfig config) { this.config = config; this.policies = loadPolicies(); } /** * Evaluate verification context against policies */ public PolicyEvaluation evaluate(VerificationContext context) { List<PolicyResult> results = new ArrayList<>(); boolean compliant = true; for (TrustPolicy policy : policies) { PolicyResult result = policy.evaluate(context); results.add(result); if (!result.isCompliant()) { compliant = false; } } return PolicyEvaluation.builder() .compliant(compliant) .policyResults(results) .build(); } /** * Add a new policy */ public void addPolicy(TrustPolicy policy) { policies.add(policy); } /** * Remove a policy */ public void removePolicy(String policyName) { policies.removeIf(policy -> policy.getName().equals(policyName)); } private List<TrustPolicy> loadPolicies() { List<TrustPolicy> loadedPolicies = new ArrayList<>(); // Load default policies loadedPolicies.add(new SignatureRequirementPolicy()); loadedPolicies.add(new ExpirationPolicy()); loadedPolicies.add(new RevocationPolicy()); loadedPolicies.add(new AttestationPolicy()); // Load custom policies from configuration loadedPolicies.addAll(loadCustomPolicies()); return loadedPolicies; } private List<TrustPolicy> loadCustomPolicies() { // Load custom policies from configuration files return new ArrayList<>(); } } /** * Trust policy interface */ interface TrustPolicy { String getName(); String getDescription(); PolicyResult evaluate(VerificationContext context); } /** * Signature requirement policy */ class SignatureRequirementPolicy implements TrustPolicy { @Override public String getName() { return "signature-requirement"; } @Override public String getDescription() { return "Requires at least one valid signature from trusted identities"; } @Override public PolicyResult evaluate(VerificationContext context) { SignatureVerification signatureVerification = context.getSignatureVerification(); if (signatureVerification.getValidSignatures().isEmpty()) { return PolicyResult.failure(this, "No valid signatures found"); } // Check if signatures are from trusted identities for (ImageSignature signature : signatureVerification.getValidSignatures()) { if (isTrustedIdentity(signature.getSignerIdentity())) { return PolicyResult.success(this); } } return PolicyResult.failure(this, "No signatures from trusted identities"); } private boolean isTrustedIdentity(String identity) { // Check against trusted identity store return true; // Simplified } } /** * Expiration policy */ class ExpirationPolicy implements TrustPolicy { @Override public String getName() { return "expiration"; } @Override public String getDescription() { return "Ensures signatures have not expired"; } @Override public PolicyResult evaluate(VerificationContext context) { SignatureVerification signatureVerification = context.getSignatureVerification(); Instant now = Instant.now(); for (ImageSignature signature : signatureVerification.getValidSignatures()) { if (signature.getExpiresAt() != null && now.isAfter(signature.getExpiresAt())) { return PolicyResult.failure(this, "Signature has expired: " + signature.getId()); } } return PolicyResult.success(this); } } /** * Policy evaluation result */ public class PolicyEvaluation { private final boolean compliant; private final List<PolicyResult> policyResults; private PolicyEvaluation(Builder builder) { this.compliant = builder.compliant; this.policyResults = builder.policyResults; } public static Builder builder() { return new Builder(); } public static class Builder { private boolean compliant; private List<PolicyResult> policyResults = new ArrayList<>(); public Builder compliant(boolean compliant) { this.compliant = compliant; return this; } public Builder policyResults(List<PolicyResult> policyResults) { this.policyResults = policyResults; return this; } public PolicyEvaluation build() { return new PolicyEvaluation(this); } } // Getters public boolean isCompliant() { return compliant; } public List<PolicyResult> getPolicyResults() { return policyResults; } } /** * Policy result */ public class PolicyResult { private final TrustPolicy policy; private final boolean compliant; private final String message; private PolicyResult(TrustPolicy policy, boolean compliant, String message) { this.policy = policy; this.compliant = compliant; this.message = message; } public static PolicyResult success(TrustPolicy policy) { return new PolicyResult(policy, true, "Policy compliant"); } public static PolicyResult failure(TrustPolicy policy, String message) { return new PolicyResult(policy, false, message); } // Getters public TrustPolicy getPolicy() { return policy; } public boolean isCompliant() { return compliant; } public String getMessage() { return message; } } 8. Usage Examples
/** * Demo application showing Notary usage */ public class NotaryDemo { public static void main(String[] args) { // Initialize notary service NotaryConfig config = NotaryConfig.builder() .keyStoragePath("/etc/notary/keys") .metadataPath("/etc/notary/metadata") .signaturesPath("/etc/notary/signatures") .attestationsPath("/etc/notary/attestations") .build(); ImageNotaryService notary = new ImageNotaryService(config); // Demo: Sign an image demoImageSigning(notary); // Demo: Verify an image demoImageVerification(notary); // Demo: TUF metadata management demoTufManagement(notary); } private static void demoImageSigning(ImageNotaryService notary) { System.out.println("=== Image Signing Demo ==="); ImageReference imageRef = ImageReference.parse( "registry.example.com/myapp:v1.0.0" ); SigningRequest signingRequest = SigningRequest.builder() .signerIdentity("build-server-1") .purpose("production-release") .expiresAt(Instant.now().plus(30, ChronoUnit.DAYS)) .createAttestation(true) .buildUrl("https://ci.example.com/build/123") .vcsCommit("abc123def456") .vcsUrl("https://github.com/example/myapp") .build(); try { ImageSignature signature = notary.signImage(imageRef, signingRequest); System.out.println("Image signed successfully:"); System.out.println(" Image: " + signature.getImageReference()); System.out.println(" Signature ID: " + signature.getId()); System.out.println(" Signer: " + signature.getSignerIdentity()); System.out.println(" Timestamp: " + signature.getTimestamp()); } catch (NotaryException e) { System.err.println("Failed to sign image: " + e.getMessage()); } } private static void demoImageVerification(ImageNotaryService notary) { System.out.println("\n=== Image Verification Demo ==="); ImageReference imageRef = ImageReference.parse( "registry.example.com/myapp:v1.0.0" ); VerificationPolicy policy = VerificationPolicy.builder() .requireSignature(true) .requireAttestation(true) .allowedSigners(Arrays.asList("build-server-1", "build-server-2")) .maxSignatureAge(Duration.ofDays(90)) .build(); try { VerificationResult result = notary.verifyImage(imageRef, policy); System.out.println("Image verification result:"); System.out.println(" Trusted: " + result.isTrusted()); System.out.println(" Valid signatures: " + result.getSignatureVerification().getValidSignatures().size()); System.out.println(" Policy compliant: " + result.getPolicyEvaluation().isCompliant()); if (!result.isTrusted()) { System.out.println("Verification errors:"); for (VerificationError error : result.getSignatureVerification().getErrors()) { System.out.println(" - " + error.getCode() + ": " + error.getMessage()); } for (PolicyResult policyResult : result.getPolicyEvaluation().getPolicyResults()) { if (!policyResult.isCompliant()) { System.out.println(" - Policy " + policyResult.getPolicy().getName() + ": " + policyResult.getMessage()); } } } } catch (NotaryException e) { System.err.println("Failed to verify image: " + e.getMessage()); } } private static void demoTufManagement(ImageNotaryService notary) { System.out.println("\n=== TUF Management Demo ==="); // Initialize root metadata RootMetadata root = createRootMetadata(); notary.initializeRootMetadata(root); System.out.println("TUF root metadata initialized"); // Update targets metadata TargetMetadata targets = createTargetsMetadata(); notary.updateMetadata(MetadataType.TARGETS, targets); System.out.println("TUF targets metadata updated"); } private static RootMetadata createRootMetadata() { // Create sample root metadata Map<String, Key> keys = new HashMap<>(); Map<String, Role> roles = new HashMap<>(); Map<String, Signature> signatures = new HashMap<>(); return new RootMetadata( 1, Instant.now().plus(365, ChronoUnit.DAYS), keys, roles, signatures ); } private static TargetMetadata createTargetsMetadata() { // Create sample targets metadata Map<String, Target> targets = new HashMap<>(); Map<String, Signature> signatures = new HashMap<>(); return new TargetMetadata( 1, Instant.now().plus(7, ChronoUnit.DAYS), targets, signatures ); } } /** * Spring Boot integration */ @Configuration public class NotaryAutoConfiguration { @Bean @ConfigurationProperties(prefix = "notary") public NotaryConfig notaryConfig() { return NotaryConfig.builder().build(); } @Bean public ImageNotaryService imageNotaryService(NotaryConfig config) { return new ImageNotaryService(config); } } /** * REST controller for Notary operations */ @RestController @RequestMapping("/notary") public class NotaryController { private final ImageNotaryService notaryService; public NotaryController(ImageNotaryService notaryService) { this.notaryService = notaryService; } @PostMapping("/sign") public ResponseEntity<ImageSignature> signImage( @RequestBody SigningRequest request) { try { ImageReference imageRef = ImageReference.parse(request.getImageReference()); ImageSignature signature = notaryService.signImage(imageRef, request); return ResponseEntity.ok(signature); } catch (NotaryException e) { return ResponseEntity.badRequest().build(); } } @PostMapping("/verify") public ResponseEntity<VerificationResult> verifyImage( @RequestBody VerificationRequest request) { try { ImageReference imageRef = ImageReference.parse(request.getImageReference()); VerificationResult result = notaryService.verifyImage(imageRef, request.getPolicy()); return ResponseEntity.ok(result); } catch (NotaryException e) { return ResponseEntity.badRequest().build(); } } @GetMapping("/health") public ResponseEntity<Map<String, String>> health() { Map<String, String> health = new HashMap<>(); health.put("status", "healthy"); health.put("timestamp", Instant.now().toString()); return ResponseEntity.ok(health); } } Key Features
- TUF Compliance: Implements The Update Framework for secure software updates
- Digital Signatures: RSA-based signing with key management
- Policy Engine: Configurable trust policies for image verification
- Attestation Support: in-toto attestations for build provenance
- Revocation: Support for signature and key revocation
- Key Management: Secure key storage and rotation
- REST API: HTTP API for integration with CI/CD systems
- Spring Boot Integration: Auto-configuration and dependency injection
Configuration Example
# application.yml notary: key-storage-path: /etc/notary/keys metadata-path: /etc/notary/metadata signatures-path: /etc/notary/signatures attestations-path: /etc/notary/attestations default-signing-key: signing-1 root-key-validity-days: 365 target-key-validity-days: 90 signature-validity-days: 30
This comprehensive Notary implementation provides production-ready container image trust and verification with support for modern software supply chain security requirements.