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.