Files
T

320 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Ed25519 Digital Signature Tool
Implements Ed25519 key generation, signing, verification, and a
simple code signing system.
Requirements:
pip install cryptography
Usage:
python process.py generate --output ./keys
python process.py sign --key ./keys/private.pem --input document.pdf
python process.py verify --key ./keys/public.pem --input document.pdf --signature document.pdf.sig
python process.py code-sign --key ./keys/private.pem --artifact ./build/app.zip
python process.py benchmark
"""
import os
import sys
import json
import time
import hashlib
import argparse
import logging
import datetime
import base64
from pathlib import Path
from typing import Dict, Optional, Tuple
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives import serialization
from cryptography.exceptions import InvalidSignature
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def generate_ed25519_keypair(
output_dir: str, passphrase: Optional[str] = None
) -> Dict:
"""Generate an Ed25519 key pair."""
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
if passphrase:
enc = serialization.BestAvailableEncryption(passphrase.encode())
else:
enc = serialization.NoEncryption()
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=enc,
)
(output_path / "private.pem").write_bytes(private_pem)
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
(output_path / "public.pem").write_bytes(public_pem)
# Compute fingerprint
public_raw = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
fingerprint = hashlib.sha256(public_raw).hexdigest()
metadata = {
"algorithm": "Ed25519",
"public_key_hex": public_raw.hex(),
"fingerprint_sha256": fingerprint,
"created_at": datetime.datetime.utcnow().isoformat() + "Z",
"private_key_path": str(output_path / "private.pem"),
"public_key_path": str(output_path / "public.pem"),
}
(output_path / "key_metadata.json").write_text(json.dumps(metadata, indent=2))
logger.info(f"Ed25519 key pair generated in {output_dir}")
logger.info(f"Fingerprint: {fingerprint}")
return metadata
def load_private_key(path: str, passphrase: Optional[str] = None) -> Ed25519PrivateKey:
"""Load Ed25519 private key from PEM file."""
data = Path(path).read_bytes()
pwd = passphrase.encode() if passphrase else None
key = serialization.load_pem_private_key(data, password=pwd)
if not isinstance(key, Ed25519PrivateKey):
raise TypeError("Key is not Ed25519")
return key
def load_public_key(path: str) -> Ed25519PublicKey:
"""Load Ed25519 public key from PEM file."""
data = Path(path).read_bytes()
key = serialization.load_pem_public_key(data)
if not isinstance(key, Ed25519PublicKey):
raise TypeError("Key is not Ed25519")
return key
def sign_data(data: bytes, private_key: Ed25519PrivateKey) -> bytes:
"""Sign data with Ed25519."""
return private_key.sign(data)
def verify_data(data: bytes, signature: bytes, public_key: Ed25519PublicKey) -> bool:
"""Verify Ed25519 signature."""
try:
public_key.verify(signature, data)
return True
except InvalidSignature:
return False
def sign_file(key_path: str, input_path: str, passphrase: Optional[str] = None) -> Dict:
"""Sign a file and save the signature."""
private_key = load_private_key(key_path, passphrase)
data = Path(input_path).read_bytes()
signature = sign_data(data, private_key)
sig_path = input_path + ".sig"
Path(sig_path).write_bytes(signature)
# Also save base64 signature for text-friendly contexts
sig_b64_path = input_path + ".sig.b64"
Path(sig_b64_path).write_text(base64.b64encode(signature).decode())
file_hash = hashlib.sha256(data).hexdigest()
logger.info(f"Signed {input_path} ({len(data)} bytes)")
return {
"file": input_path,
"signature_file": sig_path,
"signature_b64_file": sig_b64_path,
"signature_hex": signature.hex(),
"file_sha256": file_hash,
"algorithm": "Ed25519",
}
def verify_file(key_path: str, input_path: str, sig_path: str) -> Dict:
"""Verify a file's Ed25519 signature."""
public_key = load_public_key(key_path)
data = Path(input_path).read_bytes()
signature = Path(sig_path).read_bytes()
# Handle base64 encoded signatures
if len(signature) != 64:
try:
signature = base64.b64decode(signature)
except Exception:
pass
valid = verify_data(data, signature, public_key)
logger.info(f"Verification: {'VALID' if valid else 'INVALID'}")
return {
"file": input_path,
"valid": valid,
"file_sha256": hashlib.sha256(data).hexdigest(),
"algorithm": "Ed25519",
}
def code_sign(key_path: str, artifact_path: str, passphrase: Optional[str] = None) -> Dict:
"""Create a code signing manifest for an artifact."""
private_key = load_private_key(key_path, passphrase)
data = Path(artifact_path).read_bytes()
public_raw = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
manifest = {
"artifact": Path(artifact_path).name,
"size": len(data),
"sha256": hashlib.sha256(data).hexdigest(),
"sha512": hashlib.sha512(data).hexdigest(),
"signer_public_key": public_raw.hex(),
"signer_fingerprint": hashlib.sha256(public_raw).hexdigest(),
"signed_at": datetime.datetime.utcnow().isoformat() + "Z",
"algorithm": "Ed25519",
}
manifest_json = json.dumps(manifest, indent=2, sort_keys=True).encode()
signature = sign_data(manifest_json, private_key)
signed_manifest = {
**manifest,
"signature": base64.b64encode(signature).decode(),
}
manifest_path = artifact_path + ".manifest.json"
Path(manifest_path).write_text(json.dumps(signed_manifest, indent=2))
logger.info(f"Code signed: {artifact_path}")
return signed_manifest
def verify_code_signature(manifest_path: str, artifact_path: str) -> Dict:
"""Verify a code signing manifest."""
signed_manifest = json.loads(Path(manifest_path).read_text())
signature = base64.b64decode(signed_manifest["signature"])
public_raw = bytes.fromhex(signed_manifest["signer_public_key"])
public_key = Ed25519PublicKey.from_public_bytes(public_raw)
manifest_copy = {k: v for k, v in signed_manifest.items() if k != "signature"}
manifest_json = json.dumps(manifest_copy, indent=2, sort_keys=True).encode()
sig_valid = verify_data(manifest_json, signature, public_key)
data = Path(artifact_path).read_bytes()
hash_valid = hashlib.sha256(data).hexdigest() == signed_manifest["sha256"]
return {
"artifact": artifact_path,
"signature_valid": sig_valid,
"hash_valid": hash_valid,
"overall_valid": sig_valid and hash_valid,
"signer_fingerprint": signed_manifest["signer_fingerprint"],
}
def benchmark():
"""Benchmark Ed25519 operations."""
print("=== Ed25519 Benchmark ===\n")
# Key generation
count = 1000
start = time.time()
for _ in range(count):
Ed25519PrivateKey.generate()
elapsed = time.time() - start
print(f"Key generation: {count / elapsed:.0f} keys/s ({elapsed / count * 1e6:.1f} us/key)")
# Signing
key = Ed25519PrivateKey.generate()
message = b"Benchmark message for Ed25519 signing performance test." * 10
count = 5000
start = time.time()
for _ in range(count):
key.sign(message)
elapsed = time.time() - start
print(f"Signing: {count / elapsed:.0f} sigs/s ({elapsed / count * 1e6:.1f} us/sig)")
# Verification
public_key = key.public_key()
signature = key.sign(message)
count = 2000
start = time.time()
for _ in range(count):
public_key.verify(signature, message)
elapsed = time.time() - start
print(f"Verification: {count / elapsed:.0f} verifs/s ({elapsed / count * 1e6:.1f} us/verify)")
def main():
parser = argparse.ArgumentParser(description="Ed25519 Digital Signature Tool")
subparsers = parser.add_subparsers(dest="command")
gen = subparsers.add_parser("generate", help="Generate Ed25519 key pair")
gen.add_argument("--output", "-o", default="./keys", help="Output directory")
gen.add_argument("--passphrase", "-p", help="Passphrase for private key")
sig = subparsers.add_parser("sign", help="Sign a file")
sig.add_argument("--key", required=True, help="Private key path")
sig.add_argument("--input", "-i", required=True, help="File to sign")
sig.add_argument("--passphrase", "-p", help="Key passphrase")
ver = subparsers.add_parser("verify", help="Verify signature")
ver.add_argument("--key", required=True, help="Public key path")
ver.add_argument("--input", "-i", required=True, help="File to verify")
ver.add_argument("--signature", "-s", required=True, help="Signature file")
cs = subparsers.add_parser("code-sign", help="Code sign an artifact")
cs.add_argument("--key", required=True, help="Private key path")
cs.add_argument("--artifact", required=True, help="Artifact to sign")
cs.add_argument("--passphrase", "-p", help="Key passphrase")
csv = subparsers.add_parser("code-verify", help="Verify code signature")
csv.add_argument("--manifest", required=True, help="Manifest file path")
csv.add_argument("--artifact", required=True, help="Artifact file path")
subparsers.add_parser("benchmark", help="Benchmark Ed25519 performance")
args = parser.parse_args()
if args.command == "generate":
result = generate_ed25519_keypair(args.output, args.passphrase)
print(json.dumps(result, indent=2))
elif args.command == "sign":
result = sign_file(args.key, args.input, args.passphrase)
print(json.dumps(result, indent=2))
elif args.command == "verify":
result = verify_file(args.key, args.input, args.signature)
print(json.dumps(result, indent=2))
elif args.command == "code-sign":
result = code_sign(args.key, args.artifact, args.passphrase)
print(json.dumps(result, indent=2))
elif args.command == "code-verify":
result = verify_code_signature(args.manifest, args.artifact)
print(json.dumps(result, indent=2))
elif args.command == "benchmark":
benchmark()
else:
parser.print_help()
if __name__ == "__main__":
main()