cryptography

Implementing Digital Signatures with Ed25519

Ed25519 is a high-performance digital signature algorithm using the Edwards curve Curve25519. It provides 128-bit security with 64-byte signatures and 32-byte keys, offering significant advantages over RSA and ECDSA including deterministic signatures (no random nonce needed), resistance to side-channel attacks, and fast verification. This skill covers implementing Ed25519 for document signing, code signing, and API authentication.

authenticationcryptographydigital-signaturesed25519integrity
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Ed25519 is a high-performance digital signature algorithm using the Edwards curve Curve25519. It provides 128-bit security with 64-byte signatures and 32-byte keys, offering significant advantages over RSA and ECDSA including deterministic signatures (no random nonce needed), resistance to side-channel attacks, and fast verification. This skill covers implementing Ed25519 for document signing, code signing, and API authentication.

When to Use

  • When deploying or configuring implementing digital signatures with ed25519 capabilities in your environment
  • When establishing security controls aligned to compliance requirements
  • When building or improving security architecture for this domain
  • When conducting security assessments that require this implementation

Prerequisites

  • Familiarity with cryptography concepts and tools
  • Access to a test or lab environment for safe execution
  • Python 3.8+ with required dependencies installed
  • Appropriate authorization for any testing activities

Objectives

  • Generate Ed25519 key pairs for signing
  • Sign messages and files with Ed25519
  • Verify signatures against public keys
  • Implement multi-signature verification
  • Build a simple code signing system
  • Compare Ed25519 performance with RSA and ECDSA

Key Concepts

Ed25519 vs RSA vs ECDSA

Property Ed25519 RSA-3072 ECDSA P-256
Security 128-bit 128-bit 128-bit
Public key size 32 bytes 384 bytes 64 bytes
Signature size 64 bytes 384 bytes 64 bytes
Key generation ~50 us ~100 ms ~1 ms
Sign ~70 us ~5 ms ~200 us
Verify ~200 us ~200 us ~500 us
Deterministic Yes No (PSS) No (unless RFC 6979)

Key Properties

  • Deterministic: Same message + key always produces same signature
  • Collision-resistant: No separate hash function needed
  • Side-channel resistant: Constant-time implementation
  • Small keys: 32 bytes each (public and private)

Security Considerations

  • Ed25519 does not support key recovery from signatures
  • Verify the full message, not a hash (Ed25519 hashes internally)
  • Public keys must be validated before use (check for low-order points)
  • Private keys should be stored encrypted at rest
  • Ed25519 is not yet approved for all NIST use cases (Ed448 is preferred for federal)

Validation Criteria

  • Key pair generation produces valid Ed25519 keys
  • Signature verification succeeds for valid message
  • Signature verification fails for tampered message
  • Signature verification fails for wrong public key
  • Deterministic: same input produces same signature
  • File signing and verification works correctly
  • Performance meets or exceeds RSA-3072
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md1.7 KB

API Reference: Ed25519 Digital Signature Agent

Dependencies

Library Version Purpose
cryptography >=41.0 Ed25519 key generation, signing, verification

CLI Usage

# Generate keypair
python scripts/agent.py --generate-keys --output-dir /keys/
 
# Sign a file
python scripts/agent.py --sign release.tar.gz --private-key /keys/ed25519_private.pem
 
# Verify files
python scripts/agent.py --verify release.tar.gz --public-key /keys/ed25519_public.pem

Functions

generate_keypair(output_dir, key_name) -> dict

Ed25519PrivateKey.generate(), serializes with private_bytes(PEM, PKCS8, NoEncryption) and public_bytes(PEM, SubjectPublicKeyInfo).

sign_message(private_key_path, message) -> dict

Loads key via load_pem_private_key(), calls key.sign(message). Returns base64 and hex signature.

sign_file(private_key_path, file_path) -> dict

Signs file contents, writes .ed25519.sig JSON containing signature, hash, timestamp.

verify_message(public_key_path, message, signature_b64) -> dict

Calls key.verify(signature, message). Catches InvalidSignature.

verify_file(public_key_path, file_path, sig_path) -> dict

Verifies file against .ed25519.sig JSON, checks hash match.

cryptography API

Method Purpose
Ed25519PrivateKey.generate() Generate 32-byte private key
private_key.sign(data) Create 64-byte signature
public_key.verify(signature, data) Verify signature
load_pem_private_key(data, password) Load PEM key

Output Schema

{
  "verifications": [{"file": "release.tar.gz", "valid": true}],
  "valid": 3, "invalid": 0
}
standards.md1.3 KB

Standards and References - Digital Signatures with Ed25519

Primary Standards

RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA)

RFC 8709 - Ed25519 and Ed448 Public Key Algorithms for SSH

NIST FIPS 186-5 - Digital Signature Standard

RFC 7748 - Elliptic Curves for Security

Python Libraries

cryptography (pyca/cryptography)

PyNaCl (libsodium)

Related

Daniel J. Bernstein et al. - High-speed high-security signatures

workflows.md1.3 KB

Workflows - Digital Signatures with Ed25519

Workflow 1: Key Generation and Storage

[Generate Ed25519 Key Pair]
(32-byte private seed -> 32-byte public key)
      |
[Serialize Private Key (PKCS#8 PEM)]
[Serialize Public Key (SubjectPublicKeyInfo PEM)]
      |
[Encrypt Private Key with Passphrase]
      |
[Store with Metadata]
(key_id, fingerprint, creation_date)

Workflow 2: Sign Document

[Document to Sign]
      |
[Load Private Key (decrypt passphrase)]
      |
[Ed25519 Sign]
(deterministic: SHA-512 internal hash)
      |
[Output: 64-byte Signature]
      |
[Create Signature File]
(signature + public key reference + metadata)

Workflow 3: Verify Signature

[Document + Signature + Public Key]
      |
[Load Public Key]
      |
[Ed25519 Verify]
      |
[Valid?]
  YES -> Accept document as authentic
  NO  -> Reject (tampering detected)

Workflow 4: Code Signing System

[Build Artifact] (binary, package, container)
      |
[Hash Artifact] (SHA-256)
      |
[Create Signing Manifest]
(artifact_name, hash, timestamp, signer_id)
      |
[Sign Manifest with Ed25519]
      |
[Distribute: Artifact + Manifest + Signature + Public Key]
      |
[Recipient Verifies]:
  1. Verify signature on manifest
  2. Hash artifact and compare to manifest
  3. Check signer identity against trust store

Scripts 2

agent.py5.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Ed25519 digital signature agent using the cryptography library."""

import argparse
import base64
import hashlib
import json
import logging
import os
import sys
from datetime import datetime
from typing import List

try:
    from cryptography.hazmat.primitives.asymmetric.ed25519 import (
        Ed25519PrivateKey, Ed25519PublicKey)
    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 generate_keypair(output_dir: str, key_name: str = "ed25519") -> dict:
    """Generate Ed25519 keypair and save to PEM files."""
    private_key = Ed25519PrivateKey.generate()
    priv_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption())
    pub_pem = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo)
    priv_path = os.path.join(output_dir, f"{key_name}_private.pem")
    pub_path = os.path.join(output_dir, f"{key_name}_public.pem")
    with open(priv_path, "wb") as f:
        f.write(priv_pem)
    with open(pub_path, "wb") as f:
        f.write(pub_pem)
    logger.info("Keypair saved: %s, %s", priv_path, pub_path)
    return {"private_key_path": priv_path, "public_key_path": pub_path}


def load_private_key(path: str) -> Ed25519PrivateKey:
    """Load Ed25519 private key from PEM file."""
    with open(path, "rb") as f:
        return serialization.load_pem_private_key(f.read(), password=None)


def load_public_key(path: str) -> Ed25519PublicKey:
    """Load Ed25519 public key from PEM file."""
    with open(path, "rb") as f:
        return serialization.load_pem_public_key(f.read())


def sign_message(private_key_path: str, message: bytes) -> dict:
    """Sign a message with Ed25519 private key."""
    key = load_private_key(private_key_path)
    signature = key.sign(message)
    return {
        "signature_b64": base64.b64encode(signature).decode(),
        "signature_hex": signature.hex(),
        "message_hash": hashlib.sha256(message).hexdigest(),
        "signature_bytes": len(signature),
    }


def sign_file(private_key_path: str, file_path: str) -> dict:
    """Sign a file with Ed25519 and write signature to .sig file."""
    with open(file_path, "rb") as f:
        data = f.read()
    result = sign_message(private_key_path, data)
    sig_path = file_path + ".ed25519.sig"
    with open(sig_path, "w") as f:
        json.dump({"signature": result["signature_b64"],
                    "file_hash": result["message_hash"],
                    "algorithm": "Ed25519",
                    "signed_at": datetime.utcnow().isoformat()}, f, indent=2)
    result["signature_file"] = sig_path
    return result


def verify_message(public_key_path: str, message: bytes, signature_b64: str) -> dict:
    """Verify an Ed25519 signature on a message."""
    key = load_public_key(public_key_path)
    signature = base64.b64decode(signature_b64)
    try:
        key.verify(signature, message)
        return {"valid": True, "algorithm": "Ed25519"}
    except InvalidSignature:
        return {"valid": False, "error": "Signature verification failed"}


def verify_file(public_key_path: str, file_path: str, sig_path: str) -> dict:
    """Verify a file's Ed25519 signature."""
    with open(file_path, "rb") as f:
        data = f.read()
    with open(sig_path) as f:
        sig_data = json.load(f)
    result = verify_message(public_key_path, data, sig_data["signature"])
    result["file"] = file_path
    result["file_hash"] = hashlib.sha256(data).hexdigest()
    result["hash_matches"] = result["file_hash"] == sig_data.get("file_hash", "")
    return result


def batch_verify(public_key_path: str, files: List[str]) -> List[dict]:
    """Verify signatures for multiple files."""
    results = []
    for file_path in files:
        sig_path = file_path + ".ed25519.sig"
        if os.path.isfile(sig_path):
            results.append(verify_file(public_key_path, file_path, sig_path))
        else:
            results.append({"file": file_path, "valid": False, "error": "No signature file"})
    return results


def main():
    parser = argparse.ArgumentParser(description="Ed25519 Digital Signature Agent")
    parser.add_argument("--generate-keys", action="store_true")
    parser.add_argument("--sign", help="File to sign")
    parser.add_argument("--verify", nargs="+", help="Files to verify")
    parser.add_argument("--private-key", help="Private key PEM path")
    parser.add_argument("--public-key", help="Public key PEM path")
    parser.add_argument("--output-dir", default=".")
    parser.add_argument("--output", default="signature_report.json")
    args = parser.parse_args()

    os.makedirs(args.output_dir, exist_ok=True)

    if args.generate_keys:
        result = generate_keypair(args.output_dir)
        print(json.dumps(result, indent=2))
    elif args.sign and args.private_key:
        result = sign_file(args.private_key, args.sign)
        print(json.dumps(result, indent=2))
    elif args.verify and args.public_key:
        results = batch_verify(args.public_key, args.verify)
        report = {"verifications": results,
                  "valid": sum(1 for r in results if r.get("valid")),
                  "invalid": sum(1 for r in results if not r.get("valid"))}
        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, indent=2))


if __name__ == "__main__":
    main()
process.py10.9 KB
Display-only source. This catalog never executes bundled scripts.
#!/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()

Assets 1

template.mdtext/markdown · 0.9 KB
Keep exploring