#!/usr/bin/env python3 """Code signing verification agent using cryptography library for Ed25519/RSA signature operations.""" import argparse import hashlib import json import logging import os import subprocess import sys from datetime import datetime from typing import List try: from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization from cryptography.exceptions import InvalidSignature except ImportError: sys.exit("cryptography required: pip install cryptography") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) def compute_file_hash(file_path: str, algorithm: str = "sha256") -> str: """Compute hash digest of a file.""" h = hashlib.new(algorithm) with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): h.update(chunk) return h.hexdigest() def generate_ed25519_keypair(output_dir: str) -> dict: """Generate Ed25519 signing keypair.""" private_key = Ed25519PrivateKey.generate() private_bytes = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) public_bytes = private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) priv_path = os.path.join(output_dir, "signing_key.pem") pub_path = os.path.join(output_dir, "signing_key.pub") with open(priv_path, "wb") as f: f.write(private_bytes) with open(pub_path, "wb") as f: f.write(public_bytes) return {"private_key": priv_path, "public_key": pub_path, "algorithm": "Ed25519"} def sign_artifact(file_path: str, private_key_path: str) -> dict: """Sign a file artifact using Ed25519.""" with open(private_key_path, "rb") as f: private_key = serialization.load_pem_private_key(f.read(), password=None) with open(file_path, "rb") as f: data = f.read() signature = private_key.sign(data) sig_path = file_path + ".sig" with open(sig_path, "wb") as f: f.write(signature) return { "file": file_path, "signature_file": sig_path, "hash_sha256": hashlib.sha256(data).hexdigest(), "algorithm": "Ed25519", } def verify_signature(file_path: str, signature_path: str, public_key_path: str) -> dict: """Verify an Ed25519 signature against a file.""" with open(public_key_path, "rb") as f: public_key = serialization.load_pem_public_key(f.read()) with open(file_path, "rb") as f: data = f.read() with open(signature_path, "rb") as f: signature = f.read() try: public_key.verify(signature, data) return {"file": file_path, "valid": True, "algorithm": "Ed25519"} except InvalidSignature: return {"file": file_path, "valid": False, "error": "Invalid signature"} def verify_cosign_signature(image: str) -> dict: """Verify container image signature using cosign CLI.""" try: result = subprocess.run( ["cosign", "verify", image], capture_output=True, text=True, timeout=30) return {"image": image, "verified": result.returncode == 0, "output": result.stdout[:500]} except FileNotFoundError: return {"image": image, "error": "cosign not installed"} def batch_verify(artifacts: List[dict], public_key_path: str) -> List[dict]: """Verify signatures for multiple artifacts.""" results = [] for art in artifacts: result = verify_signature(art["file"], art["signature"], public_key_path) results.append(result) return results def generate_report(artifacts: List[str], public_key_path: str) -> dict: """Generate code signing verification report.""" report = {"analysis_date": datetime.utcnow().isoformat(), "verifications": []} for art_path in artifacts: sig_path = art_path + ".sig" if os.path.isfile(sig_path): result = verify_signature(art_path, sig_path, public_key_path) else: result = {"file": art_path, "valid": False, "error": "No signature file found"} result["hash_sha256"] = compute_file_hash(art_path) report["verifications"].append(result) valid = sum(1 for v in report["verifications"] if v.get("valid")) report["summary"] = { "total": len(report["verifications"]), "valid": valid, "invalid": len(report["verifications"]) - valid, } return report def main(): parser = argparse.ArgumentParser(description="Code Signing Verification Agent") parser.add_argument("--artifacts", nargs="+", help="Files to verify") parser.add_argument("--public-key", help="Path to public key PEM") parser.add_argument("--generate-keys", action="store_true", help="Generate new keypair") parser.add_argument("--sign", help="File to sign (requires --private-key)") parser.add_argument("--private-key", help="Path to private key PEM") parser.add_argument("--output-dir", default=".") parser.add_argument("--output", default="signing_report.json") args = parser.parse_args() os.makedirs(args.output_dir, exist_ok=True) if args.generate_keys: keys = generate_ed25519_keypair(args.output_dir) print(json.dumps(keys, indent=2)) return if args.sign and args.private_key: result = sign_artifact(args.sign, args.private_key) print(json.dumps(result, indent=2)) return if args.artifacts and args.public_key: report = generate_report(args.artifacts, args.public_key) out_path = os.path.join(args.output_dir, args.output) with open(out_path, "w") as f: json.dump(report, f, indent=2) print(json.dumps(report["summary"], indent=2)) if __name__ == "__main__": main()