cryptography

Implementing RSA Key Pair Management

RSA (Rivest-Shamir-Adleman) is the most widely deployed asymmetric cryptographic algorithm, used for digital signatures, key exchange, and encryption. This skill covers generating, storing, rotating, and managing RSA key pairs following NIST SP 800-57 key management guidelines, including key serialization formats (PEM, DER, PKCS#8), passphrase protection, and key strength validation.

asymmetric-encryptioncryptographykey-managementpkirsa
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

RSA (Rivest-Shamir-Adleman) is the most widely deployed asymmetric cryptographic algorithm, used for digital signatures, key exchange, and encryption. This skill covers generating, storing, rotating, and managing RSA key pairs following NIST SP 800-57 key management guidelines, including key serialization formats (PEM, DER, PKCS#8), passphrase protection, and key strength validation.

When to Use

  • When deploying or configuring implementing rsa key pair management 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 RSA key pairs with appropriate key sizes (2048, 3072, 4096 bits)
  • Serialize keys in PEM and DER formats with PKCS#8
  • Protect private keys with strong passphrase encryption
  • Implement key rotation with versioning
  • Extract public key components and fingerprints
  • Validate key strength and detect weak keys
  • Sign and verify data using RSA-PSS

Key Concepts

RSA Key Sizes and Security Strength

Key Size (bits) Security Strength (bits) Recommended Until
2048 112 2030
3072 128 Beyond 2030
4096 ~140 Beyond 2030

RSA Padding Schemes

Scheme Use Case Standard
OAEP Encryption PKCS#1 v2.2 (RFC 8017)
PSS Signatures PKCS#1 v2.2 (RFC 8017)
PKCS#1 v1.5 Legacy only Deprecated for new systems

Key Storage Formats

  • PEM: Base64-encoded with headers, human-readable
  • DER: Binary ASN.1 encoding, compact
  • PKCS#8: Standard for private key encapsulation
  • PKCS#12/PFX: Bundled key + certificate, password-protected

Security Considerations

  • Minimum 3072-bit keys for new deployments (NIST recommendation)
  • Always protect private keys with AES-256-CBC passphrase encryption
  • Use RSA-PSS for signatures (not PKCS#1 v1.5)
  • Use RSA-OAEP for encryption (not PKCS#1 v1.5)
  • Store private keys with restrictive file permissions (0600)
  • Implement key rotation at least annually

Validation Criteria

  • Key generation produces valid RSA key pair
  • Public key can be extracted from private key
  • Private key is protected with passphrase
  • RSA-PSS signature verification succeeds
  • Tampered signature verification fails
  • Key fingerprint is computed correctly
  • Key rotation maintains old key access for verification
Source materials

References and resources

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

References 3

api-reference.md5.7 KB

API Reference: RSA Key Pair Lifecycle Management

Libraries Used

Library Purpose
cryptography RSA key generation, signing, verification, serialization
os Secure random bytes, file permissions
datetime Certificate validity periods and key rotation schedules
json Export key metadata and audit reports

Installation

pip install cryptography

Key Generation

Generate RSA Key Pair

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
 
def generate_rsa_keypair(key_size=4096):
    """Generate an RSA key pair. Use 2048 minimum, 4096 recommended."""
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=key_size,
    )
    return private_key

Serialize Private Key (PEM, encrypted)

def save_private_key(private_key, filepath, passphrase):
    pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.BestAvailableEncryption(
            passphrase.encode()
        ),
    )
    with open(filepath, "wb") as f:
        f.write(pem)
    os.chmod(filepath, 0o600)  # Restrict permissions

Serialize Public Key

def save_public_key(private_key, filepath):
    public_key = private_key.public_key()
    pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    with open(filepath, "wb") as f:
        f.write(pem)

Load Existing Key

def load_private_key(filepath, passphrase=None):
    with open(filepath, "rb") as f:
        private_key = serialization.load_pem_private_key(
            f.read(),
            password=passphrase.encode() if passphrase else None,
        )
    return private_key
 
def load_public_key(filepath):
    with open(filepath, "rb") as f:
        public_key = serialization.load_pem_public_key(f.read())
    return public_key

Signing and Verification

Sign Data

def sign_data(private_key, data):
    signature = private_key.sign(
        data,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH,
        ),
        hashes.SHA256(),
    )
    return signature

Verify Signature

from cryptography.exceptions import InvalidSignature
 
def verify_signature(public_key, data, signature):
    try:
        public_key.verify(
            signature,
            data,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH,
            ),
            hashes.SHA256(),
        )
        return True
    except InvalidSignature:
        return False

Encryption and Decryption

Encrypt with RSA-OAEP

def encrypt_data(public_key, plaintext):
    ciphertext = public_key.encrypt(
        plaintext,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )
    return ciphertext

Decrypt with RSA-OAEP

def decrypt_data(private_key, ciphertext):
    plaintext = private_key.decrypt(
        ciphertext,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )
    return plaintext

Key Audit and Rotation

Inspect Key Properties

def audit_key(filepath, passphrase=None):
    key = load_private_key(filepath, passphrase)
    pub = key.public_key()
    numbers = pub.public_numbers()
    return {
        "key_size": key.key_size,
        "compliant": key.key_size >= 2048,
        "recommended": key.key_size >= 4096,
        "public_exponent": numbers.e,
        "modulus_bits": numbers.n.bit_length(),
        "format": "PKCS8-PEM",
        "encrypted": passphrase is not None,
    }

Check Key Strength

def check_key_strength(key_path, passphrase=None):
    key = load_private_key(key_path, passphrase)
    findings = []
    if key.key_size < 2048:
        findings.append({
            "issue": f"Key size {key.key_size} bits is below minimum (2048)",
            "severity": "critical",
        })
    elif key.key_size < 4096:
        findings.append({
            "issue": f"Key size {key.key_size} bits — 4096 recommended",
            "severity": "low",
        })
    return {"key_size": key.key_size, "findings": findings}

Self-Signed Certificate Generation

from cryptography import x509
from cryptography.x509.oid import NameOID
from datetime import datetime, timedelta, timezone
 
def create_self_signed_cert(private_key, common_name, days_valid=365):
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, common_name),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Security Audit"),
    ])
    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.now(timezone.utc))
        .not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid))
        .sign(private_key, hashes.SHA256())
    )
    return cert

Output Format

{
  "key_path": "/etc/pki/private/server.key",
  "key_size": 4096,
  "public_exponent": 65537,
  "compliant": true,
  "encrypted": true,
  "certificate": {
    "common_name": "server.example.com",
    "not_before": "2025-01-15T00:00:00Z",
    "not_after": "2026-01-15T00:00:00Z",
    "serial_number": "ABC123..."
  },
  "findings": []
}
standards.md1.8 KB

Standards and References - RSA Key Pair Management

Primary Standards

NIST FIPS 186-5 - Digital Signature Standard (DSS)

RFC 8017 - PKCS #1: RSA Cryptography Specifications Version 2.2

RFC 5958 - Asymmetric Key Packages (PKCS#8 v2)

RFC 7468 - Textual Encodings of PKIX, PKCS, and CMS Structures

NIST SP 800-57 Part 1 Rev. 5 - Key Management

NIST SP 800-131A Rev. 2 - Transitioning Cryptographic Algorithms

Python Library References

cryptography (pyca/cryptography)

workflows.md1.5 KB

Workflows - RSA Key Pair Management

Workflow 1: Key Pair Generation

[Select Key Size] (3072 or 4096 bits)
      |
[Generate RSA Key Pair]
(public_exponent=65537)
      |
[Serialize Private Key]
(PEM/PKCS#8 with AES-256-CBC passphrase)
      |
[Extract and Serialize Public Key]
(PEM/SubjectPublicKeyInfo)
      |
[Compute Key Fingerprint]
(SHA-256 of DER-encoded public key)
      |
[Store Keys with Metadata]
(key_id, creation_date, algorithm, size)

Workflow 2: Digital Signature (RSA-PSS)

[Document/Data to Sign]
      |
[Hash Data] (SHA-256)
      |
[Load Private Key] (decrypt with passphrase)
      |
[RSA-PSS Sign]
(padding=PSS, mgf=MGF1(SHA256), salt_length=PSS.MAX_LENGTH)
      |
[Output Signature] (DER or Base64)

Workflow 3: Signature Verification

[Document + Signature + Public Key]
      |
[Load Public Key]
      |
[RSA-PSS Verify]
(same padding parameters as signing)
      |
[Valid?]
  YES --> Accept
  NO  --> Reject (data or signature tampered)

Workflow 4: Key Rotation

[Current Key Pair (version N)]
      |
[Generate New Key Pair (version N+1)]
      |
[Update Active Key Reference]
      |
[Archive Old Key Pair]
(mark as "decrypt/verify only")
      |
[After Grace Period: Destroy Old Private Key]
(keep public key for verification)

Workflow 5: RSA Encryption (OAEP)

[Plaintext] (max size depends on key and padding)
      |
[Load Recipient's Public Key]
      |
[RSA-OAEP Encrypt]
(padding=OAEP, mgf=MGF1(SHA256), algorithm=SHA256)
      |
[Ciphertext]

Scripts 2

agent.py9.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""RSA key pair lifecycle management agent.

Generates, audits, rotates, and manages RSA key pairs using the
cryptography library. Supports key generation with configurable sizes,
PEM export with encryption, public key extraction, key strength
auditing, and expiration tracking.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone

try:
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import rsa, padding
    from cryptography.hazmat.backends import default_backend
    from cryptography.x509 import load_pem_x509_certificate
    HAS_CRYPTO = True
except ImportError:
    HAS_CRYPTO = False


def generate_key_pair(key_size=4096, passphrase=None):
    """Generate an RSA key pair."""
    if not HAS_CRYPTO:
        print("[!] 'cryptography' required: pip install cryptography", file=sys.stderr)
        sys.exit(1)
    print(f"[*] Generating {key_size}-bit RSA key pair...")
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=key_size,
        backend=default_backend(),
    )
    if passphrase:
        encryption = serialization.BestAvailableEncryption(passphrase.encode())
    else:
        encryption = serialization.NoEncryption()
    private_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=encryption,
    )
    public_key = private_key.public_key()
    public_pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    public_ssh = public_key.public_bytes(
        encoding=serialization.Encoding.OpenSSH,
        format=serialization.PublicFormat.OpenSSH,
    )
    print(f"[+] Key pair generated ({key_size} bits)")
    return {
        "private_pem": private_pem.decode(),
        "public_pem": public_pem.decode(),
        "public_ssh": public_ssh.decode(),
        "key_size": key_size,
        "encrypted": passphrase is not None,
    }


def save_key_pair(key_data, private_path, public_path):
    """Save key pair to files with secure permissions."""
    with open(private_path, "w") as f:
        f.write(key_data["private_pem"])
    os.chmod(private_path, 0o600)
    print(f"[+] Private key saved to {private_path} (mode 0600)")
    with open(public_path, "w") as f:
        f.write(key_data["public_pem"])
    os.chmod(public_path, 0o644)
    print(f"[+] Public key saved to {public_path}")


def audit_key_file(key_path):
    """Audit an existing RSA key file for security issues."""
    if not HAS_CRYPTO:
        print("[!] 'cryptography' required", file=sys.stderr)
        sys.exit(1)
    findings = []
    if not os.path.isfile(key_path):
        findings.append({"check": "File exists", "status": "FAIL", "severity": "CRITICAL"})
        return findings
    stat = os.stat(key_path)
    mode = oct(stat.st_mode)[-3:]
    if mode not in ("600", "400"):
        findings.append({
            "check": "File permissions",
            "status": f"FAIL (mode {mode})",
            "severity": "HIGH",
            "recommendation": "Set permissions to 600: chmod 600 " + key_path,
        })
    else:
        findings.append({"check": "File permissions", "status": f"PASS (mode {mode})", "severity": "INFO"})
    with open(key_path, "rb") as f:
        pem_data = f.read()
    is_encrypted = b"ENCRYPTED" in pem_data
    try:
        if b"PRIVATE" in pem_data:
            if is_encrypted:
                findings.append({"check": "Key encryption", "status": "PASS (encrypted)", "severity": "INFO"})
                findings.append({"check": "Key type", "status": "Private key (encrypted)", "severity": "INFO"})
                return findings
            private_key = serialization.load_pem_private_key(pem_data, password=None, backend=default_backend())
            key_size = private_key.key_size
            findings.append({"check": "Key encryption", "status": "FAIL (unencrypted)", "severity": "HIGH",
                            "recommendation": "Encrypt private key with a passphrase"})
        else:
            from cryptography.hazmat.primitives.serialization import load_pem_public_key
            public_key = load_pem_public_key(pem_data, backend=default_backend())
            key_size = public_key.key_size
            findings.append({"check": "Key type", "status": "Public key", "severity": "INFO"})

        if key_size < 2048:
            findings.append({"check": "Key strength", "status": f"FAIL ({key_size} bits)", "severity": "CRITICAL",
                            "recommendation": "Minimum 2048-bit; recommend 4096-bit for new keys"})
        elif key_size < 4096:
            findings.append({"check": "Key strength", "status": f"WARN ({key_size} bits)", "severity": "MEDIUM",
                            "recommendation": "Consider upgrading to 4096-bit"})
        else:
            findings.append({"check": "Key strength", "status": f"PASS ({key_size} bits)", "severity": "INFO"})
    except Exception as e:
        findings.append({"check": "Key parsing", "status": f"FAIL: {e}", "severity": "HIGH"})
    return findings


def scan_directory_for_keys(directory, recursive=True):
    """Scan a directory for key files and audit each one."""
    key_files = []
    key_extensions = (".pem", ".key", ".pub", ".rsa", ".der")
    key_markers = (b"BEGIN RSA PRIVATE", b"BEGIN PRIVATE", b"BEGIN PUBLIC", b"BEGIN OPENSSH")
    for root, dirs, files in os.walk(directory):
        for fname in files:
            full_path = os.path.join(root, fname)
            is_key = False
            if any(fname.endswith(ext) for ext in key_extensions):
                is_key = True
            else:
                try:
                    with open(full_path, "rb") as f:
                        header = f.read(64)
                    if any(marker in header for marker in key_markers):
                        is_key = True
                except (IOError, PermissionError):
                    pass
            if is_key:
                findings = audit_key_file(full_path)
                key_files.append({"path": full_path, "findings": findings})
        if not recursive:
            break
    return key_files


def format_summary(results, action):
    """Print a human-readable summary."""
    print(f"\n{'='*60}")
    print(f"  RSA Key Management Report")
    print(f"{'='*60}")
    print(f"  Action    : {action}")
    if action == "audit" and isinstance(results, list):
        total_keys = len(results)
        critical = sum(1 for r in results for f in r.get("findings", []) if f.get("severity") == "CRITICAL")
        high = sum(1 for r in results for f in r.get("findings", []) if f.get("severity") == "HIGH")
        print(f"  Keys Found: {total_keys}")
        print(f"  Critical  : {critical}")
        print(f"  High      : {high}")
        for r in results:
            print(f"\n    Key: {r['path']}")
            for f in r.get("findings", []):
                print(f"      [{f['severity']:8s}] {f['check']}: {f['status']}")


def main():
    parser = argparse.ArgumentParser(description="RSA key pair lifecycle management agent")
    sub = parser.add_subparsers(dest="command")

    p_gen = sub.add_parser("generate", help="Generate new RSA key pair")
    p_gen.add_argument("--key-size", type=int, default=4096, choices=[2048, 3072, 4096],
                       help="RSA key size in bits (default: 4096)")
    p_gen.add_argument("--passphrase", help="Passphrase to encrypt private key")
    p_gen.add_argument("--private-key", default="id_rsa", help="Private key output path")
    p_gen.add_argument("--public-key", default="id_rsa.pub", help="Public key output path")

    p_audit = sub.add_parser("audit", help="Audit existing key files")
    p_audit.add_argument("--path", required=True, help="Key file or directory to audit")
    p_audit.add_argument("--recursive", action="store_true", help="Scan directory recursively")

    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    if args.command == "generate":
        key_data = generate_key_pair(args.key_size, args.passphrase)
        save_key_pair(key_data, args.private_key, args.public_key)
        result = {"action": "generate", "key_size": args.key_size,
                  "private_key": args.private_key, "public_key": args.public_key,
                  "encrypted": key_data["encrypted"]}
    elif args.command == "audit":
        if os.path.isdir(args.path):
            results = scan_directory_for_keys(args.path, args.recursive)
        else:
            findings = audit_key_file(args.path)
            results = [{"path": args.path, "findings": findings}]
        format_summary(results, "audit")
        result = {"action": "audit", "keys_audited": results}

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "RSA Key Manager",
        "result": result,
    }
    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
process.py11.5 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
RSA Key Pair Management Tool

Implements RSA key generation, serialization, signing, verification,
encryption, and key rotation using the cryptography library.

Requirements:
    pip install cryptography

Usage:
    python process.py generate --size 4096 --output ./keys --passphrase "MyKeyPass"
    python process.py sign --key ./keys/private.pem --input document.pdf --passphrase "MyKeyPass"
    python process.py verify --key ./keys/public.pem --input document.pdf --signature document.pdf.sig
    python process.py info --key ./keys/public.pem
    python process.py rotate --keystore ./keys --passphrase "MyKeyPass"
"""

import os
import sys
import json
import hashlib
import argparse
import logging
import datetime
from pathlib import Path
from typing import Dict, Optional, Tuple

from cryptography.hazmat.primitives.asymmetric import rsa, padding, utils
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidSignature

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

RECOMMENDED_KEY_SIZE = 4096
PUBLIC_EXPONENT = 65537


def generate_rsa_keypair(
    key_size: int = RECOMMENDED_KEY_SIZE,
    passphrase: Optional[str] = None,
) -> Tuple[bytes, bytes, Dict]:
    """
    Generate an RSA key pair.

    Returns:
        Tuple of (private_key_pem, public_key_pem, metadata)
    """
    if key_size < 2048:
        raise ValueError("Key size must be at least 2048 bits (3072+ recommended)")

    private_key = rsa.generate_private_key(
        public_exponent=PUBLIC_EXPONENT,
        key_size=key_size,
        backend=default_backend(),
    )

    if passphrase:
        encryption = serialization.BestAvailableEncryption(passphrase.encode())
    else:
        encryption = serialization.NoEncryption()

    private_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=encryption,
    )

    public_key = private_key.public_key()
    public_pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )

    # Compute fingerprint (SHA-256 of DER-encoded public key)
    public_der = public_key.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    fingerprint = hashlib.sha256(public_der).hexdigest()

    metadata = {
        "algorithm": "RSA",
        "key_size": key_size,
        "public_exponent": PUBLIC_EXPONENT,
        "fingerprint_sha256": fingerprint,
        "created_at": datetime.datetime.utcnow().isoformat() + "Z",
        "passphrase_protected": passphrase is not None,
        "format": "PKCS#8 PEM",
    }

    return private_pem, public_pem, metadata


def save_keypair(
    output_dir: str,
    private_pem: bytes,
    public_pem: bytes,
    metadata: Dict,
    version: int = 1,
) -> Dict:
    """Save key pair to files with metadata."""
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    prefix = f"v{version}_" if version > 1 else ""

    private_path = output_path / f"{prefix}private.pem"
    public_path = output_path / f"{prefix}public.pem"
    meta_path = output_path / f"{prefix}key_metadata.json"

    private_path.write_bytes(private_pem)
    public_path.write_bytes(public_pem)

    metadata["version"] = version
    metadata["private_key_path"] = str(private_path)
    metadata["public_key_path"] = str(public_path)

    meta_path.write_text(json.dumps(metadata, indent=2))

    logger.info(f"Key pair saved to {output_dir} (version {version})")

    return metadata


def load_private_key(key_path: str, passphrase: Optional[str] = None):
    """Load an RSA private key from PEM file."""
    key_data = Path(key_path).read_bytes()
    pwd = passphrase.encode() if passphrase else None
    return serialization.load_pem_private_key(key_data, password=pwd, backend=default_backend())


def load_public_key(key_path: str):
    """Load an RSA public key from PEM file."""
    key_data = Path(key_path).read_bytes()
    return serialization.load_pem_public_key(key_data, backend=default_backend())


def sign_data(data: bytes, private_key) -> bytes:
    """Sign data using RSA-PSS with SHA-256."""
    signature = private_key.sign(
        data,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH,
        ),
        hashes.SHA256(),
    )
    return signature


def verify_signature(data: bytes, signature: bytes, public_key) -> bool:
    """Verify RSA-PSS signature."""
    try:
        public_key.verify(
            signature,
            data,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH,
            ),
            hashes.SHA256(),
        )
        return True
    except InvalidSignature:
        return False


def encrypt_data(plaintext: bytes, public_key) -> bytes:
    """Encrypt data using RSA-OAEP."""
    max_size = (public_key.key_size // 8) - 2 * 32 - 2  # OAEP with SHA-256
    if len(plaintext) > max_size:
        raise ValueError(
            f"Plaintext too large for RSA-OAEP ({len(plaintext)} bytes, max {max_size}). "
            "Use envelope encryption for large data."
        )
    return public_key.encrypt(
        plaintext,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )


def decrypt_data(ciphertext: bytes, private_key) -> bytes:
    """Decrypt RSA-OAEP encrypted data."""
    return private_key.decrypt(
        ciphertext,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )


def get_key_info(key_path: str, passphrase: Optional[str] = None) -> Dict:
    """Get information about an RSA key."""
    key_data = Path(key_path).read_bytes()

    try:
        pwd = passphrase.encode() if passphrase else None
        key = serialization.load_pem_private_key(key_data, password=pwd, backend=default_backend())
        key_type = "private"
        public_key = key.public_key()
    except (ValueError, TypeError):
        key = serialization.load_pem_public_key(key_data, backend=default_backend())
        key_type = "public"
        public_key = key

    public_der = public_key.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    fingerprint = hashlib.sha256(public_der).hexdigest()

    numbers = public_key.public_numbers()

    info = {
        "key_type": key_type,
        "algorithm": "RSA",
        "key_size": public_key.key_size,
        "public_exponent": numbers.e,
        "fingerprint_sha256": fingerprint,
        "modulus_hex_prefix": hex(numbers.n)[:32] + "...",
    }

    if public_key.key_size < 2048:
        info["warning"] = "Key size below 2048 bits is considered insecure"
    elif public_key.key_size < 3072:
        info["note"] = "Key size below 3072 bits; consider upgrading for post-2030 use"

    return info


def rotate_keys(keystore_dir: str, passphrase: Optional[str] = None) -> Dict:
    """Rotate RSA key pair, archiving the old one."""
    keystore = Path(keystore_dir)

    # Find current version
    version = 1
    meta_files = sorted(keystore.glob("*key_metadata.json"))
    if meta_files:
        for mf in meta_files:
            meta = json.loads(mf.read_text())
            v = meta.get("version", 1)
            if v >= version:
                version = v + 1

    # Generate new key pair
    private_pem, public_pem, metadata = generate_rsa_keypair(
        key_size=RECOMMENDED_KEY_SIZE, passphrase=passphrase
    )

    result = save_keypair(keystore_dir, private_pem, public_pem, metadata, version=version)

    # Update current key symlink info
    current_meta = {
        "current_version": version,
        "current_fingerprint": metadata["fingerprint_sha256"],
        "rotated_at": datetime.datetime.utcnow().isoformat() + "Z",
        "all_versions": list(range(1, version + 1)),
    }
    (keystore / "current.json").write_text(json.dumps(current_meta, indent=2))

    logger.info(f"Key rotated to version {version}")
    return result


def sign_file(key_path: str, input_path: str, passphrase: Optional[str] = None) -> str:
    """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)
    logger.info(f"Signature saved to {sig_path}")
    return sig_path


def verify_file(key_path: str, input_path: str, sig_path: str) -> bool:
    """Verify a file's signature."""
    public_key = load_public_key(key_path)
    data = Path(input_path).read_bytes()
    signature = Path(sig_path).read_bytes()
    valid = verify_signature(data, signature, public_key)
    logger.info(f"Signature verification: {'VALID' if valid else 'INVALID'}")
    return valid


def main():
    parser = argparse.ArgumentParser(description="RSA Key Pair Management Tool")
    subparsers = parser.add_subparsers(dest="command")

    gen = subparsers.add_parser("generate", help="Generate RSA key pair")
    gen.add_argument("--size", type=int, default=4096, help="Key size in bits")
    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 a signature")
    ver.add_argument("--key", required=True, help="Public key path")
    ver.add_argument("--input", "-i", required=True, help="Original file")
    ver.add_argument("--signature", "-s", required=True, help="Signature file")

    info = subparsers.add_parser("info", help="Show key information")
    info.add_argument("--key", required=True, help="Key file path")
    info.add_argument("--passphrase", "-p", help="Key passphrase")

    rot = subparsers.add_parser("rotate", help="Rotate key pair")
    rot.add_argument("--keystore", required=True, help="Keystore directory")
    rot.add_argument("--passphrase", "-p", help="Passphrase for new key")

    args = parser.parse_args()

    if args.command == "generate":
        priv, pub, meta = generate_rsa_keypair(args.size, args.passphrase)
        result = save_keypair(args.output, priv, pub, meta)
        print(json.dumps(result, indent=2))
    elif args.command == "sign":
        sig_path = sign_file(args.key, args.input, args.passphrase)
        print(json.dumps({"signature_file": sig_path}))
    elif args.command == "verify":
        valid = verify_file(args.key, args.input, args.signature)
        print(json.dumps({"valid": valid}))
        if not valid:
            sys.exit(1)
    elif args.command == "info":
        result = get_key_info(args.key, args.passphrase)
        print(json.dumps(result, indent=2))
    elif args.command == "rotate":
        result = rotate_keys(args.keystore, args.passphrase)
        print(json.dumps(result, indent=2))
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 1.6 KB
Keep exploring