cryptography

Performing Post-Quantum Cryptography Migration

Assesses organizational readiness for post-quantum cryptography migration per NIST FIPS 203/204/205 standards. Performs cryptographic inventory scanning to identify quantum-vulnerable algorithms (RSA, ECDH, ECDSA), evaluates hybrid TLS configurations with X25519MLKEM768, and validates CRYSTALS-Kyber (ML-KEM) and CRYSTALS-Dilithium (ML-DSA) readiness. Implements crypto-agility assessment using oqs-provider for OpenSSL. Use when planning or executing the transition from classical to post-quantum cryptographic algorithms across enterprise infrastructure.

crypto-agilitycrystals-kyberfips-203fips-204hybrid-tlsml-dsaml-kempost-quantum
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When assessing organizational readiness for the NIST post-quantum cryptography transition
  • When building a cryptographic inventory to identify quantum-vulnerable algorithms across infrastructure
  • When evaluating hybrid TLS 1.3 configurations using X25519MLKEM768 key exchange
  • When testing CRYSTALS-Kyber (ML-KEM) and CRYSTALS-Dilithium (ML-DSA) algorithm support
  • When implementing crypto-agility to support both classical and post-quantum algorithms
  • When preparing migration roadmaps aligned with NIST IR 8547 deprecation timelines
  • When configuring oqs-provider with OpenSSL 3.x for post-quantum algorithm support

Prerequisites

  • Python 3.8+ with cryptography, requests, pyOpenSSL libraries
  • OpenSSL 3.0+ (3.5+ recommended for native ML-KEM/ML-DSA support)
  • oqs-provider for OpenSSL (for hybrid TLS testing with older OpenSSL)
  • Network access to target servers for TLS assessment
  • Administrative access for infrastructure scanning
  • Familiarity with PKI, TLS, and cryptographic protocols

Core Concepts

NIST Post-Quantum Cryptography Standards

NIST published three finalized PQC standards on August 13, 2024:

Standard Algorithm Renamed To Purpose Based On
FIPS 203 CRYSTALS-Kyber ML-KEM Key Encapsulation Mechanism Module lattice
FIPS 204 CRYSTALS-Dilithium ML-DSA Digital Signatures Module lattice
FIPS 205 SPHINCS+ SLH-DSA Digital Signatures (backup) Stateless hash

ML-KEM (FIPS 203) -- Primary standard for key exchange and encryption. Replaces RSA and ECDH for key establishment. Three security levels: ML-KEM-512, ML-KEM-768, ML-KEM-1024.

ML-DSA (FIPS 204) -- Primary standard for digital signatures. Replaces RSA and ECDSA for signing. Three security levels: ML-DSA-44, ML-DSA-65, ML-DSA-87.

SLH-DSA (FIPS 205) -- Backup signature standard using hash-based approach. Intended as fallback if lattice-based ML-DSA is found vulnerable. Larger signatures but conservative security assumptions.

Quantum-Vulnerable Algorithms

These classical algorithms are vulnerable to quantum attack via Shor's algorithm:

Algorithm Usage Quantum Threat Migration Priority
RSA-2048/4096 Key exchange, signatures, encryption Shor's algorithm breaks factoring Critical
ECDH (P-256, P-384) TLS key exchange Shor's algorithm breaks ECDLP Critical
ECDSA Code signing, TLS certificates Shor's algorithm breaks ECDLP Critical
DSA Legacy signatures Shor's algorithm breaks DLP Critical
DH (Diffie-Hellman) Key exchange Shor's algorithm breaks DLP Critical
AES-128 Symmetric encryption Grover's halves key strength Medium (upgrade to AES-256)
SHA-256 Hashing Grover's reduces to 128-bit Low (still adequate)

NIST Migration Timeline (IR 8547)

  • 2024: Standards published, migration planning should begin
  • 2030: Deprecation of quantum-vulnerable algorithms for most federal systems
  • 2035: Complete removal of quantum-vulnerable algorithms from NIST standards
  • Now: "Harvest now, decrypt later" attacks make early migration essential for long-lived secrets and data requiring long-term confidentiality

Hybrid TLS Key Exchange

During the transition period, hybrid key exchange combines a classical algorithm with a post-quantum algorithm. If either algorithm is secure, the connection remains protected.

Hybrid Key Exchange: X25519MLKEM768
  = X25519 (classical ECDH) + ML-KEM-768 (post-quantum)
 
Client Hello:
  supported_groups: X25519MLKEM768, X25519, secp256r1
  key_share: X25519MLKEM768
 
Server Hello:
  selected_group: X25519MLKEM768
  key_share: X25519MLKEM768
 
Shared Secret = KDF(X25519_shared || MLKEM768_shared)

Instructions

Phase 1: Cryptographic Inventory Scanning

The first step in PQC migration is discovering all cryptographic algorithm usage across the enterprise. This includes TLS configurations, certificates, code libraries, key stores, and protocol configurations.

# Scan TLS endpoints for quantum-vulnerable algorithms
python scripts/agent.py --action scan_tls \
    --targets targets.txt \
    --output tls_inventory.json

The scanner identifies:

  • TLS protocol versions in use
  • Key exchange algorithms (RSA, ECDH, DH -- all quantum-vulnerable)
  • Certificate signature algorithms (RSA, ECDSA)
  • Cipher suite configurations
  • Certificate key sizes and expiration dates

Phase 2: Crypto-Agility Assessment

Evaluate the organization's ability to swap cryptographic algorithms without major infrastructure changes:

# Assess crypto-agility readiness
python scripts/agent.py --action assess_agility \
    --scan-results tls_inventory.json \
    --output agility_report.json

Key assessment areas:

  1. Protocol flexibility: Can TLS configurations be updated without downtime?
  2. Library versions: Do deployed crypto libraries support PQC algorithms?
  3. Certificate infrastructure: Can CA issue PQC certificates?
  4. Key management: Can KMS handle larger PQC key sizes?
  5. Hardware constraints: Can HSMs support PQC operations?

Phase 3: Hybrid TLS Readiness Testing

Test whether infrastructure supports hybrid key exchange with X25519MLKEM768:

# Test hybrid TLS support on target servers
python scripts/agent.py --action test_hybrid_tls \
    --target server.example.com:443 \
    --output hybrid_tls_report.json

OpenSSL 3.5+ (native ML-KEM support):

# Test with native PQC support
openssl s_client -connect server.example.com:443 \
    -groups X25519MLKEM768

OpenSSL 3.0-3.4 with oqs-provider:

# Configure oqs-provider
# /etc/ssl/openssl-oqs.cnf
[openssl_init]
providers = provider_sect
 
[provider_sect]
default = default_sect
oqsprovider = oqsprovider_sect
 
[default_sect]
activate = 1
 
[oqsprovider_sect]
activate = 1
module = /usr/lib/oqs-provider/oqsprovider.so
 
# Test hybrid TLS
OPENSSL_CONF=/etc/ssl/openssl-oqs.cnf \
openssl s_client -connect server.example.com:443 \
    -groups x25519_mlkem768

Web Server Configuration for Hybrid TLS:

Apache httpd:

SSLEngine on
SSLCertificateFile /etc/ssl/certs/server.crt
SSLCertificateKeyFile /etc/ssl/private/server.key
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1
SSLProtocol -all +TLSv1.2 +TLSv1.3

NGINX:

ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;

Phase 4: ML-KEM Key Encapsulation Validation

Validate that ML-KEM (CRYSTALS-Kyber) key encapsulation works correctly in your environment:

# Test ML-KEM key encapsulation at all security levels
python scripts/agent.py --action test_mlkem \
    --output mlkem_validation.json

ML-KEM parameter comparison:

Parameter ML-KEM-512 ML-KEM-768 ML-KEM-1024
Security Level NIST Level 1 NIST Level 3 NIST Level 5
Public Key Size 800 bytes 1,184 bytes 1,568 bytes
Ciphertext Size 768 bytes 1,088 bytes 1,568 bytes
Shared Secret 32 bytes 32 bytes 32 bytes
Comparable To AES-128 AES-192 AES-256

Phase 5: ML-DSA Digital Signature Validation

Validate ML-DSA (CRYSTALS-Dilithium) signature operations:

# Test ML-DSA digital signatures
python scripts/agent.py --action test_mldsa \
    --output mldsa_validation.json

ML-DSA parameter comparison:

Parameter ML-DSA-44 ML-DSA-65 ML-DSA-87
Security Level NIST Level 2 NIST Level 3 NIST Level 5
Public Key Size 1,312 bytes 1,952 bytes 2,592 bytes
Signature Size 2,420 bytes 3,293 bytes 4,595 bytes
Secret Key Size 2,560 bytes 4,032 bytes 4,896 bytes

Phase 6: Migration Roadmap Generation

Generate a prioritized migration roadmap based on inventory and assessment results:

# Generate complete migration roadmap
python scripts/agent.py --action roadmap \
    --scan-results tls_inventory.json \
    --agility-results agility_report.json \
    --output migration_roadmap.json

The roadmap prioritizes systems by:

  1. Data sensitivity: Systems handling long-lived secrets migrate first
  2. Exposure level: Internet-facing services before internal
  3. Crypto-agility: Systems that can easily swap algorithms first
  4. Compliance requirements: Federal/regulated systems per NIST IR 8547 timeline
  5. Dependency chains: Libraries and frameworks before applications

Examples

Full Assessment Pipeline

# Step 1: Scan all TLS endpoints
python scripts/agent.py --action scan_tls --targets hosts.txt --output scan.json
 
# Step 2: Assess crypto-agility
python scripts/agent.py --action assess_agility --scan-results scan.json --output agility.json
 
# Step 3: Test hybrid TLS on critical servers
python scripts/agent.py --action test_hybrid_tls --target critical.example.com:443
 
# Step 4: Validate ML-KEM support
python scripts/agent.py --action test_mlkem --output mlkem.json
 
# Step 5: Validate ML-DSA support
python scripts/agent.py --action test_mldsa --output mldsa.json
 
# Step 6: Generate migration roadmap
python scripts/agent.py --action roadmap --scan-results scan.json --agility-results agility.json --output roadmap.json

Quick Server Assessment

# Single server PQC readiness check
python scripts/agent.py --action scan_tls --target server.example.com:443

Validation Checklist

  • Cryptographic inventory covers all TLS endpoints, certificates, and key stores
  • All quantum-vulnerable algorithms (RSA, ECDH, ECDSA, DH, DSA) are identified
  • Crypto-agility assessment documents library versions and upgrade paths
  • Hybrid TLS (X25519MLKEM768) tested on representative server configurations
  • ML-KEM key encapsulation validated at target security level (768 recommended)
  • ML-DSA signature verification validated for certificate chain use
  • SLH-DSA (FIPS 205) evaluated as backup signature algorithm
  • Migration roadmap prioritizes by data sensitivity and compliance timeline
  • OpenSSL version and oqs-provider compatibility confirmed
  • Key size increases accounted for in network and storage capacity planning
  • HSM/KMS compatibility with PQC algorithms verified
  • Performance impact of PQC algorithms benchmarked under production load
  • "Harvest now, decrypt later" risk assessed for sensitive data channels
  • Certificate Authority PQC readiness confirmed for certificate issuance

References

Source materials

References and resources

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

References 1

api-reference.md8.6 KB

API Reference: Post-Quantum Cryptography Migration

NIST PQC Standards Summary

FIPS 203 -- ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism)

Formerly CRYSTALS-Kyber. Primary standard for key exchange and encryption.

Security Levels:

Parameter Set NIST Level Public Key Ciphertext Shared Secret
ML-KEM-512 Level 1 800 B 768 B 32 B
ML-KEM-768 Level 3 1,184 B 1,088 B 32 B
ML-KEM-1024 Level 5 1,568 B 1,568 B 32 B

Operations:

  • KeyGen() -> (ek, dk) -- Generate encapsulation/decapsulation key pair
  • Encaps(ek) -> (K, c) -- Encapsulate: produce shared secret K and ciphertext c
  • Decaps(dk, c) -> K -- Decapsulate: recover shared secret K from ciphertext

Python (mlkem library):

from mlkem.ml_kem import ML_KEM
 
ml_kem = ML_KEM(768)  # ML-KEM-768
ek, dk = ml_kem.key_gen()
shared_secret, ciphertext = ml_kem.encaps(ek)
recovered_secret = ml_kem.decaps(dk, ciphertext)
assert shared_secret == recovered_secret

OpenSSL 3.5+ (native):

# Generate ML-KEM-768 key pair
openssl genpkey -algorithm mlkem768 -out mlkem768_key.pem
 
# Display key details
openssl pkey -in mlkem768_key.pem -text -noout
 
# Extract public key
openssl pkey -in mlkem768_key.pem -pubout -out mlkem768_pub.pem

FIPS 204 -- ML-DSA (Module-Lattice-Based Digital Signature Algorithm)

Formerly CRYSTALS-Dilithium. Primary standard for digital signatures.

Security Levels:

Parameter Set NIST Level Public Key Secret Key Signature
ML-DSA-44 Level 2 1,312 B 2,560 B 2,420 B
ML-DSA-65 Level 3 1,952 B 4,032 B 3,293 B
ML-DSA-87 Level 5 2,592 B 4,896 B 4,595 B

Operations:

  • KeyGen() -> (pk, sk) -- Generate signing/verification key pair
  • Sign(sk, M) -> sigma -- Sign message M with secret key
  • Verify(pk, M, sigma) -> bool -- Verify signature on message

OpenSSL 3.5+ (native):

# Generate ML-DSA-65 key pair
openssl genpkey -algorithm mldsa65 -out mldsa65_key.pem
 
# Extract public key
openssl pkey -in mldsa65_key.pem -pubout -out mldsa65_pub.pem
 
# Sign a file
openssl dgst -sign mldsa65_key.pem -out signature.bin message.txt
 
# Verify signature
openssl dgst -verify mldsa65_pub.pem -signature signature.bin message.txt

FIPS 205 -- SLH-DSA (Stateless Hash-Based Digital Signature Algorithm)

Formerly SPHINCS+. Backup signature standard using conservative hash-based approach.

Parameter Sets (SHA2 variants):

Parameter Set NIST Level Public Key Signature (fast) Signature (small)
SLH-DSA-128 Level 1 32 B 17,088 B 7,856 B
SLH-DSA-192 Level 3 48 B 35,664 B 16,224 B
SLH-DSA-256 Level 5 64 B 49,856 B 29,792 B

Variants: Each level has fast (f) and small (s) variants with SHA2 or SHAKE hash.

Hybrid TLS Configuration

X25519MLKEM768 Key Exchange

The hybrid key exchange combines classical X25519 ECDH with ML-KEM-768 post-quantum KEM. Both must be broken for the handshake to be compromised.

Apache httpd:

# httpd.conf or ssl.conf
SSLEngine on
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1
SSLCertificateFile /etc/ssl/certs/server.crt
SSLCertificateKeyFile /etc/ssl/private/server.key

NGINX:

server {
    listen 443 ssl;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
    ssl_prefer_server_ciphers on;
    ssl_certificate /etc/ssl/certs/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;
}

Verification:

# Test hybrid TLS connection
openssl s_client -connect server.example.com:443 -groups X25519MLKEM768
 
# Verify negotiated group
# Look for "Server Temp Key: X25519MLKEM768" in output

oqs-provider for OpenSSL 3.0+

Installation

# Clone and build oqs-provider
git clone https://github.com/open-quantum-safe/oqs-provider.git
cd oqs-provider
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make -j$(nproc)
sudo make install

Configuration

# /etc/ssl/openssl-oqs.cnf
openssl_conf = openssl_init
 
[openssl_init]
providers = provider_sect
ssl_conf = ssl_sect
 
[provider_sect]
default = default_sect
oqsprovider = oqsprovider_sect
 
[default_sect]
activate = 1
 
[oqsprovider_sect]
activate = 1
module = /usr/lib/oqs-provider/oqsprovider.so
 
[ssl_sect]
system_default = system_default_sect
 
[system_default_sect]
Groups = x25519_mlkem768:X25519:P-256:P-384
MinProtocol = TLSv1.2

Usage

# Set environment variable
export OPENSSL_CONF=/etc/ssl/openssl-oqs.cnf
 
# List available PQC algorithms
openssl list -kem-algorithms | grep -i ml
openssl list -signature-algorithms | grep -i ml
 
# Generate PQC key pair
openssl genpkey -algorithm mlkem768 -out key.pem
 
# Test hybrid TLS
openssl s_client -connect server:443 -groups x25519_mlkem768

Cryptographic Inventory Scanning

NIST SP 1800-38 Discovery Architecture

+------------------+     +------------------+     +------------------+
| Source Code Scan | --> |                  | --> | Risk Assessment  |
+------------------+    | Central Analysis |     +------------------+
+------------------+    |     Engine       |            |
| Binary Analysis  | -->|  (Normalization  |     +------------------+
+------------------+    |  & Correlation)  |     | Migration        |
+------------------+    |                  |     | Prioritization   |
| Network Traffic  | -->|                  |     +------------------+
+------------------+    +------------------+
+------------------+
| Certificate Scan | -->
+------------------+

Discovery Domains

Domain What to Scan Tools
CI/CD Pipeline Source code, build configs, dependencies SCA tools, Semgrep
Operational Systems Running services, installed libraries, key stores NIST SP 1800-38B tools
Network Services TLS endpoints, VPN configs, IPsec tunnels This agent, sslyze, testssl
Certificates CA chains, code signing certs, TLS certificates cert-manager, openssl

Quantum-Vulnerable Algorithm Reference

Algorithm NIST Status (IR 8547) Quantum Threat Replacement
RSA (all sizes) Deprecated 2030, removed 2035 Shor's algorithm ML-KEM (encryption), ML-DSA (signing)
ECDH / ECDHE Deprecated 2030, removed 2035 Shor's algorithm ML-KEM / X25519MLKEM768 hybrid
ECDSA Deprecated 2030, removed 2035 Shor's algorithm ML-DSA
DSA Already deprecated Shor's algorithm ML-DSA
DH / DHE Deprecated 2030, removed 2035 Shor's algorithm ML-KEM
AES-128 Acceptable with caveat Grover's halves to 64-bit AES-256
AES-256 Quantum-safe Grover's reduces to 128-bit No change needed
SHA-256 Quantum-safe Grover's reduces to 128-bit No change needed
SHA-3 Quantum-safe Grover's reduces to 128-bit No change needed

MITRE ATT&CK Relevance

Technique ID PQC Relevance
Adversary-in-the-Middle T1557 Quantum computers can break key exchange in recorded sessions
Encrypted Channel T1573 Harvest-now-decrypt-later targets encrypted C2 traffic
Steal Application Access Token T1528 Quantum computers can forge digital signatures
Forge Web Credentials T1606 Quantum computers can break certificate private keys

References

Scripts 1

agent.py55.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Agent for performing post-quantum cryptography migration assessment.

Scans TLS endpoints for quantum-vulnerable algorithms, assesses crypto-agility
readiness, tests hybrid TLS (X25519MLKEM768) support, validates ML-KEM and
ML-DSA algorithm functionality, and generates prioritized migration roadmaps
per NIST FIPS 203/204/205 standards.
"""

import os
import sys
import json
import ssl
import socket
import struct
import argparse
import logging
import subprocess
import hashlib
import re
from datetime import datetime, timezone
from pathlib import Path
from collections import defaultdict

import requests

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
logger = logging.getLogger("pqc-migration-agent")

# ---------------------------------------------------------------------------
# Constants: Quantum-vulnerable algorithm classification
# ---------------------------------------------------------------------------

QUANTUM_VULNERABLE_KEY_EXCHANGE = {
    "RSA",
    "DH",
    "DHE",
    "ECDH",
    "ECDHE",
}

QUANTUM_VULNERABLE_SIGNATURE = {
    "RSA",
    "ECDSA",
    "DSA",
    "Ed25519",
    "Ed448",
}

QUANTUM_SAFE_KEY_EXCHANGE = {
    "X25519MLKEM768",
    "X25519_MLKEM768",
    "SecP256r1MLKEM768",
    "MLKEM512",
    "MLKEM768",
    "MLKEM1024",
    "X448MLKEM1024",
}

PQC_SIGNATURE_ALGORITHMS = {
    "MLDSA44",
    "MLDSA65",
    "MLDSA87",
    "SLHDSA_SHA2_128S",
    "SLHDSA_SHA2_128F",
    "SLHDSA_SHA2_192S",
    "SLHDSA_SHA2_192F",
    "SLHDSA_SHA2_256S",
    "SLHDSA_SHA2_256F",
    "SLHDSA_SHAKE_128S",
    "SLHDSA_SHAKE_128F",
    "SLHDSA_SHAKE_192S",
    "SLHDSA_SHAKE_192F",
    "SLHDSA_SHAKE_256S",
    "SLHDSA_SHAKE_256F",
}

# Minimum key sizes that provide adequate classical security
MIN_SECURE_KEY_SIZES = {
    "RSA": 2048,
    "EC": 256,
    "AES": 256,  # For post-quantum, AES-256 recommended (Grover's halves effective strength)
}

NIST_MIGRATION_DEADLINES = {
    "deprecation": "2030",
    "disallowed": "2035",
    "description": "NIST IR 8547: quantum-vulnerable algorithms deprecated by 2030, "
                   "disallowed by 2035 for federal systems",
}

# ML-KEM parameters per FIPS 203
MLKEM_PARAMS = {
    "ML-KEM-512": {
        "security_level": 1,
        "pk_bytes": 800,
        "sk_bytes": 1632,
        "ct_bytes": 768,
        "ss_bytes": 32,
        "comparable_to": "AES-128",
    },
    "ML-KEM-768": {
        "security_level": 3,
        "pk_bytes": 1184,
        "sk_bytes": 2400,
        "ct_bytes": 1088,
        "ss_bytes": 32,
        "comparable_to": "AES-192",
    },
    "ML-KEM-1024": {
        "security_level": 5,
        "pk_bytes": 1568,
        "sk_bytes": 3168,
        "ct_bytes": 1568,
        "ss_bytes": 32,
        "comparable_to": "AES-256",
    },
}

# ML-DSA parameters per FIPS 204
MLDSA_PARAMS = {
    "ML-DSA-44": {
        "security_level": 2,
        "pk_bytes": 1312,
        "sk_bytes": 2560,
        "sig_bytes": 2420,
        "comparable_to": "NIST Level 2 (~AES-128+)",
    },
    "ML-DSA-65": {
        "security_level": 3,
        "pk_bytes": 1952,
        "sk_bytes": 4032,
        "sig_bytes": 3293,
        "comparable_to": "NIST Level 3 (~AES-192)",
    },
    "ML-DSA-87": {
        "security_level": 5,
        "pk_bytes": 2592,
        "sk_bytes": 4896,
        "sig_bytes": 4595,
        "comparable_to": "NIST Level 5 (~AES-256)",
    },
}

# SLH-DSA parameters per FIPS 205
SLHDSA_PARAMS = {
    "SLH-DSA-SHA2-128s": {"security_level": 1, "pk_bytes": 32, "sig_bytes": 7856},
    "SLH-DSA-SHA2-128f": {"security_level": 1, "pk_bytes": 32, "sig_bytes": 17088},
    "SLH-DSA-SHA2-192s": {"security_level": 3, "pk_bytes": 48, "sig_bytes": 16224},
    "SLH-DSA-SHA2-192f": {"security_level": 3, "pk_bytes": 48, "sig_bytes": 35664},
    "SLH-DSA-SHA2-256s": {"security_level": 5, "pk_bytes": 64, "sig_bytes": 29792},
    "SLH-DSA-SHA2-256f": {"security_level": 5, "pk_bytes": 64, "sig_bytes": 49856},
    "SLH-DSA-SHAKE-128s": {"security_level": 1, "pk_bytes": 32, "sig_bytes": 7856},
    "SLH-DSA-SHAKE-128f": {"security_level": 1, "pk_bytes": 32, "sig_bytes": 17088},
    "SLH-DSA-SHAKE-192s": {"security_level": 3, "pk_bytes": 48, "sig_bytes": 16224},
    "SLH-DSA-SHAKE-192f": {"security_level": 3, "pk_bytes": 48, "sig_bytes": 35664},
    "SLH-DSA-SHAKE-256s": {"security_level": 5, "pk_bytes": 64, "sig_bytes": 29792},
    "SLH-DSA-SHAKE-256f": {"security_level": 5, "pk_bytes": 64, "sig_bytes": 49856},
}


# ---------------------------------------------------------------------------
# TLS Endpoint Scanning
# ---------------------------------------------------------------------------

def scan_tls_endpoint(host, port=443, timeout=10):
    """
    Scan a TLS endpoint to extract cryptographic algorithm details.

    Connects to the target, performs TLS handshake, and extracts:
    - Protocol version
    - Cipher suite (key exchange, encryption, MAC)
    - Certificate details (algorithm, key size, validity)
    - Supported groups / curves

    Args:
        host: Target hostname or IP
        port: Target port (default 443)
        timeout: Connection timeout in seconds

    Returns:
        Dict with comprehensive TLS cryptographic inventory
    """
    result = {
        "host": host,
        "port": port,
        "scan_time": datetime.now(timezone.utc).isoformat(),
        "tls_version": None,
        "cipher_suite": None,
        "key_exchange": None,
        "certificate": {},
        "quantum_vulnerable": True,
        "vulnerabilities": [],
        "recommendations": [],
    }

    try:
        context = ssl.create_default_context()
        context.check_hostname = True
        context.verify_mode = ssl.CERT_REQUIRED

        with socket.create_connection((host, port), timeout=timeout) as sock:
            with context.wrap_socket(sock, server_hostname=host) as tls_sock:
                # Extract TLS session details
                cipher = tls_sock.cipher()
                result["tls_version"] = tls_sock.version()
                result["cipher_suite"] = cipher[0] if cipher else None
                result["cipher_protocol"] = cipher[1] if cipher and len(cipher) > 1 else None
                result["cipher_bits"] = cipher[2] if cipher and len(cipher) > 2 else None

                # Parse key exchange from cipher suite name
                cipher_name = cipher[0] if cipher else ""
                result["key_exchange"] = _extract_key_exchange(cipher_name)

                # Extract certificate details
                cert = tls_sock.getpeercert()
                cert_der = tls_sock.getpeercert(binary_form=True)
                result["certificate"] = _parse_certificate(cert, cert_der)

                # Assess quantum vulnerability
                _assess_quantum_vulnerability(result)

        logger.info("Scanned %s:%d -- %s [%s]", host, port,
                     result["cipher_suite"], result["tls_version"])

    except ssl.SSLError as e:
        result["error"] = f"SSL error: {e}"
        logger.warning("SSL error scanning %s:%d: %s", host, port, e)
    except socket.timeout:
        result["error"] = "Connection timed out"
        logger.warning("Timeout scanning %s:%d", host, port)
    except socket.gaierror as e:
        result["error"] = f"DNS resolution failed: {e}"
        logger.warning("DNS error for %s: %s", host, e)
    except ConnectionRefusedError:
        result["error"] = "Connection refused"
        logger.warning("Connection refused: %s:%d", host, port)
    except Exception as e:
        result["error"] = f"Unexpected error: {e}"
        logger.error("Error scanning %s:%d: %s", host, port, e)

    return result


def _extract_key_exchange(cipher_name):
    """Extract key exchange algorithm from cipher suite name."""
    cipher_upper = cipher_name.upper()
    if "ECDHE" in cipher_upper:
        return "ECDHE"
    elif "DHE" in cipher_upper or "EDH" in cipher_upper:
        return "DHE"
    elif "ECDH" in cipher_upper:
        return "ECDH"
    elif "DH" in cipher_upper:
        return "DH"
    elif "RSA" in cipher_upper:
        return "RSA"
    return "Unknown"


def _parse_certificate(cert, cert_der=None):
    """Parse certificate details from Python ssl cert dict."""
    cert_info = {
        "subject": "",
        "issuer": "",
        "not_before": "",
        "not_after": "",
        "serial_number": "",
        "signature_algorithm": "Unknown",
        "public_key_algorithm": "Unknown",
        "public_key_bits": 0,
        "san": [],
    }

    if not cert:
        return cert_info

    # Extract subject
    subject = cert.get("subject", ())
    for rdn in subject:
        for attr_type, attr_value in rdn:
            if attr_type == "commonName":
                cert_info["subject"] = attr_value

    # Extract issuer
    issuer = cert.get("issuer", ())
    for rdn in issuer:
        for attr_type, attr_value in rdn:
            if attr_type == "organizationName":
                cert_info["issuer"] = attr_value

    cert_info["not_before"] = cert.get("notBefore", "")
    cert_info["not_after"] = cert.get("notAfter", "")
    cert_info["serial_number"] = cert.get("serialNumber", "")

    # Extract SANs
    sans = cert.get("subjectAltName", ())
    cert_info["san"] = [value for san_type, value in sans if san_type == "DNS"]

    # Try to get detailed cert info via openssl
    if cert_der:
        try:
            cert_details = _openssl_parse_cert_der(cert_der)
            cert_info.update(cert_details)
        except Exception:
            pass

    return cert_info


def _openssl_parse_cert_der(cert_der):
    """Use openssl CLI to parse DER certificate for algorithm details."""
    details = {}
    try:
        proc = subprocess.run(
            ["openssl", "x509", "-inform", "DER", "-noout", "-text"],
            input=cert_der,
            capture_output=True,
            timeout=10,
        )
        output = proc.stdout.decode("utf-8", errors="replace")

        # Extract signature algorithm
        sig_match = re.search(r"Signature Algorithm:\s+(.+)", output)
        if sig_match:
            details["signature_algorithm"] = sig_match.group(1).strip()

        # Extract public key algorithm and size
        pk_match = re.search(r"Public Key Algorithm:\s+(.+)", output)
        if pk_match:
            details["public_key_algorithm"] = pk_match.group(1).strip()

        bits_match = re.search(r"(?:RSA Public-Key|Public-Key):\s+\((\d+) bit\)", output)
        if bits_match:
            details["public_key_bits"] = int(bits_match.group(1))

        ec_match = re.search(r"ASN1 OID:\s+(.+)", output)
        if ec_match:
            details["ec_curve"] = ec_match.group(1).strip()

    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass

    return details


def _assess_quantum_vulnerability(result):
    """Assess whether the TLS connection uses quantum-vulnerable cryptography."""
    vulnerabilities = []
    recommendations = []
    is_vulnerable = False

    # Check key exchange
    kx = result.get("key_exchange", "").upper()
    kx_base = kx.replace("_", "")
    if any(v in kx_base for v in ["RSA", "ECDH", "ECDHE", "DHE", "DH"]):
        if not any(pq in kx_base for pq in ["MLKEM", "KYBER"]):
            is_vulnerable = True
            vulnerabilities.append({
                "component": "key_exchange",
                "algorithm": kx,
                "threat": "Shor's algorithm can break this key exchange",
                "severity": "critical",
            })
            recommendations.append(
                "Migrate to hybrid key exchange X25519MLKEM768 for TLS 1.3"
            )

    # Check certificate signature algorithm
    sig_algo = result.get("certificate", {}).get("signature_algorithm", "").lower()
    if any(v in sig_algo for v in ["rsa", "ecdsa", "dsa"]):
        if not any(pq in sig_algo for pq in ["mldsa", "dilithium", "slhdsa", "sphincs"]):
            is_vulnerable = True
            vulnerabilities.append({
                "component": "certificate_signature",
                "algorithm": sig_algo,
                "threat": "Shor's algorithm can forge signatures",
                "severity": "high",
            })
            recommendations.append(
                "Plan migration to ML-DSA (FIPS 204) for certificate signatures"
            )

    # Check public key algorithm
    pk_algo = result.get("certificate", {}).get("public_key_algorithm", "").lower()
    pk_bits = result.get("certificate", {}).get("public_key_bits", 0)

    if "rsa" in pk_algo:
        is_vulnerable = True
        if pk_bits < 2048:
            vulnerabilities.append({
                "component": "certificate_public_key",
                "algorithm": f"RSA-{pk_bits}",
                "threat": "Below minimum key size AND quantum-vulnerable",
                "severity": "critical",
            })
        else:
            vulnerabilities.append({
                "component": "certificate_public_key",
                "algorithm": f"RSA-{pk_bits}",
                "threat": "Quantum-vulnerable (adequate classically)",
                "severity": "high",
            })

    if "ec" in pk_algo or "ecdsa" in pk_algo:
        is_vulnerable = True
        vulnerabilities.append({
            "component": "certificate_public_key",
            "algorithm": pk_algo,
            "threat": "Shor's algorithm breaks elliptic curve discrete log",
            "severity": "high",
        })

    # Check TLS version
    tls_version = result.get("tls_version", "")
    if tls_version and "1.3" not in tls_version:
        recommendations.append(
            f"Upgrade from {tls_version} to TLS 1.3 (required for hybrid PQC key exchange)"
        )

    result["quantum_vulnerable"] = is_vulnerable
    result["vulnerabilities"] = vulnerabilities
    result["recommendations"] = recommendations


def scan_multiple_endpoints(targets_file, port=443):
    """
    Scan multiple TLS endpoints from a targets file.

    Args:
        targets_file: Path to file with one host[:port] per line
        port: Default port if not specified per target

    Returns:
        List of scan results for all targets
    """
    targets_path = Path(targets_file)
    if not targets_path.exists():
        logger.error("Targets file not found: %s", targets_file)
        return []

    results = []
    with open(targets_path, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue

            if ":" in line:
                host, target_port = line.rsplit(":", 1)
                try:
                    target_port = int(target_port)
                except ValueError:
                    target_port = port
            else:
                host = line
                target_port = port

            result = scan_tls_endpoint(host, target_port)
            results.append(result)

    return results


# ---------------------------------------------------------------------------
# Crypto-Agility Assessment
# ---------------------------------------------------------------------------

def assess_crypto_agility(scan_results):
    """
    Assess organizational crypto-agility based on TLS scan results.

    Evaluates the ability to migrate from quantum-vulnerable algorithms
    to post-quantum alternatives without major infrastructure changes.

    Args:
        scan_results: List of TLS scan result dicts

    Returns:
        Crypto-agility assessment report
    """
    assessment = {
        "assessment_time": datetime.now(timezone.utc).isoformat(),
        "total_endpoints": len(scan_results),
        "quantum_vulnerable_endpoints": 0,
        "tls13_ready": 0,
        "algorithm_inventory": defaultdict(int),
        "certificate_algorithms": defaultdict(int),
        "key_exchange_algorithms": defaultdict(int),
        "risk_summary": {},
        "agility_score": 0,
        "findings": [],
        "recommendations": [],
    }

    for result in scan_results:
        if result.get("error"):
            continue

        if result.get("quantum_vulnerable"):
            assessment["quantum_vulnerable_endpoints"] += 1

        tls_ver = result.get("tls_version", "")
        if "1.3" in tls_ver:
            assessment["tls13_ready"] += 1

        cipher = result.get("cipher_suite", "Unknown")
        assessment["algorithm_inventory"][cipher] += 1

        kx = result.get("key_exchange", "Unknown")
        assessment["key_exchange_algorithms"][kx] += 1

        sig_algo = result.get("certificate", {}).get("signature_algorithm", "Unknown")
        assessment["certificate_algorithms"][sig_algo] += 1

    total = assessment["total_endpoints"]
    if total == 0:
        return assessment

    vuln_pct = (assessment["quantum_vulnerable_endpoints"] / total) * 100
    tls13_pct = (assessment["tls13_ready"] / total) * 100

    # Calculate agility score (0-100)
    score = 0
    score += min(40, tls13_pct * 0.4)  # TLS 1.3 readiness (up to 40 points)
    score += max(0, 30 - (vuln_pct * 0.3))  # Fewer vulnerabilities (up to 30 points)

    # Bonus for algorithm diversity (indicates flexibility)
    unique_ciphers = len(assessment["algorithm_inventory"])
    score += min(15, unique_ciphers * 3)  # Up to 15 points for diversity

    # Bonus for modern configurations
    modern_kx = sum(
        v for k, v in assessment["key_exchange_algorithms"].items()
        if k in ("ECDHE", "DHE")
    )
    if total > 0:
        score += min(15, (modern_kx / total) * 15)  # Up to 15 points for PFS

    assessment["agility_score"] = round(score, 1)

    # Risk summary
    assessment["risk_summary"] = {
        "quantum_vulnerable_percentage": round(vuln_pct, 1),
        "tls13_percentage": round(tls13_pct, 1),
        "unique_cipher_suites": unique_ciphers,
        "risk_level": (
            "critical" if vuln_pct > 90 else
            "high" if vuln_pct > 70 else
            "medium" if vuln_pct > 40 else
            "low"
        ),
    }

    # Findings
    if vuln_pct > 0:
        assessment["findings"].append({
            "finding": f"{assessment['quantum_vulnerable_endpoints']}/{total} "
                       f"endpoints ({vuln_pct:.0f}%) use quantum-vulnerable algorithms",
            "severity": "critical" if vuln_pct > 70 else "high",
            "category": "quantum_vulnerability",
        })

    if tls13_pct < 100:
        not_tls13 = total - assessment["tls13_ready"]
        assessment["findings"].append({
            "finding": f"{not_tls13} endpoints do not support TLS 1.3 "
                       f"(required for hybrid PQC key exchange)",
            "severity": "high",
            "category": "protocol_version",
        })

    # Recommendations
    if tls13_pct < 100:
        assessment["recommendations"].append({
            "priority": 1,
            "action": "Upgrade all TLS endpoints to TLS 1.3",
            "rationale": "Hybrid PQC key exchange (X25519MLKEM768) requires TLS 1.3",
            "effort": "medium",
        })

    assessment["recommendations"].append({
        "priority": 2,
        "action": "Deploy hybrid key exchange X25519MLKEM768 on TLS 1.3 endpoints",
        "rationale": "Provides quantum-resistant key exchange while maintaining "
                     "classical security as fallback",
        "effort": "low" if tls13_pct > 80 else "medium",
    })

    assessment["recommendations"].append({
        "priority": 3,
        "action": "Update OpenSSL to 3.5+ or install oqs-provider for PQC support",
        "rationale": "OpenSSL 3.5 provides native ML-KEM/ML-DSA support; "
                     "oqs-provider adds PQC to OpenSSL 3.0-3.4",
        "effort": "medium",
    })

    assessment["recommendations"].append({
        "priority": 4,
        "action": "Plan certificate migration to ML-DSA (FIPS 204) signatures",
        "rationale": "Certificate signatures need PQC before the NIST 2030 "
                     "deprecation deadline",
        "effort": "high",
    })

    # Convert defaultdicts to regular dicts for JSON serialization
    assessment["algorithm_inventory"] = dict(assessment["algorithm_inventory"])
    assessment["certificate_algorithms"] = dict(assessment["certificate_algorithms"])
    assessment["key_exchange_algorithms"] = dict(assessment["key_exchange_algorithms"])

    return assessment


# ---------------------------------------------------------------------------
# Hybrid TLS Testing
# ---------------------------------------------------------------------------

def test_hybrid_tls_support(host, port=443):
    """
    Test whether a server supports hybrid post-quantum TLS key exchange.

    Attempts connection using X25519MLKEM768 and other hybrid groups.
    Requires OpenSSL 3.5+ or oqs-provider.

    Args:
        host: Target hostname
        port: Target port

    Returns:
        Dict with hybrid TLS support test results
    """
    result = {
        "host": host,
        "port": port,
        "test_time": datetime.now(timezone.utc).isoformat(),
        "openssl_version": "",
        "hybrid_groups_tested": [],
        "pqc_supported": False,
        "details": {},
    }

    # Check OpenSSL version
    try:
        proc = subprocess.run(
            ["openssl", "version"],
            capture_output=True, timeout=5,
        )
        result["openssl_version"] = proc.stdout.decode().strip()
    except (FileNotFoundError, subprocess.TimeoutExpired):
        result["openssl_version"] = "openssl not found"

    # Test hybrid key exchange groups
    hybrid_groups = [
        "X25519MLKEM768",
        "x25519_mlkem768",  # oqs-provider naming
    ]

    for group in hybrid_groups:
        test_result = _test_tls_group(host, port, group)
        result["hybrid_groups_tested"].append(test_result)
        if test_result.get("supported"):
            result["pqc_supported"] = True

    # Also test classical groups for comparison
    classical_groups = ["X25519", "P-256", "P-384"]
    for group in classical_groups:
        test_result = _test_tls_group(host, port, group)
        result["hybrid_groups_tested"].append(test_result)

    return result


def _test_tls_group(host, port, group):
    """Test a specific TLS key exchange group against a server."""
    test = {
        "group": group,
        "supported": False,
        "error": None,
    }

    try:
        cmd = [
            "openssl", "s_client",
            "-connect", f"{host}:{port}",
            "-groups", group,
            "-brief",
        ]
        proc = subprocess.run(
            cmd,
            input=b"",
            capture_output=True,
            timeout=15,
        )
        output = proc.stdout.decode("utf-8", errors="replace")
        stderr = proc.stderr.decode("utf-8", errors="replace")

        # Check if connection succeeded with the specified group
        if "Protocol" in output or "Verification" in output:
            test["supported"] = True
            # Extract negotiated protocol and cipher
            proto_match = re.search(r"Protocol version:\s+(\S+)", output)
            cipher_match = re.search(r"Ciphersuite:\s+(\S+)", output)
            if proto_match:
                test["protocol"] = proto_match.group(1)
            if cipher_match:
                test["cipher"] = cipher_match.group(1)
        elif "no protocols available" in stderr.lower():
            test["error"] = "Group not supported by server"
        elif "unknown group" in stderr.lower():
            test["error"] = "Group not supported by local OpenSSL"
        else:
            test["error"] = "Connection failed"
            if stderr:
                # Take first line of error
                test["error_detail"] = stderr.split("\n")[0][:200]

    except subprocess.TimeoutExpired:
        test["error"] = "Connection timed out"
    except FileNotFoundError:
        test["error"] = "openssl binary not found"

    return test


# ---------------------------------------------------------------------------
# ML-KEM (FIPS 203) Validation
# ---------------------------------------------------------------------------

def test_mlkem_support():
    """
    Test ML-KEM (CRYSTALS-Kyber / FIPS 203) key encapsulation support.

    Tests keygen, encapsulation, and decapsulation at all three security levels
    using either the mlkem Python library or OpenSSL with oqs-provider.

    Returns:
        Dict with ML-KEM validation results for each security level
    """
    results = {
        "test_time": datetime.now(timezone.utc).isoformat(),
        "library": None,
        "levels": {},
    }

    # Try Python mlkem library first
    try:
        from mlkem.ml_kem import ML_KEM

        results["library"] = "mlkem (Python)"

        for level_name, params in MLKEM_PARAMS.items():
            level_result = _test_mlkem_python(level_name, params)
            results["levels"][level_name] = level_result

        logger.info("ML-KEM tested via Python mlkem library")
        return results

    except ImportError:
        logger.info("mlkem Python library not available, trying OpenSSL")

    # Fallback to OpenSSL CLI
    try:
        for level_name, params in MLKEM_PARAMS.items():
            level_result = _test_mlkem_openssl(level_name, params)
            results["levels"][level_name] = level_result

        results["library"] = "OpenSSL"
        logger.info("ML-KEM tested via OpenSSL")
        return results

    except Exception as e:
        logger.warning("ML-KEM testing failed: %s", e)
        results["error"] = str(e)

    return results


def _test_mlkem_python(level_name, params):
    """Test ML-KEM at a specific level using the Python mlkem library."""
    result = {
        "level": level_name,
        "security_level": params["security_level"],
        "supported": False,
        "keygen": False,
        "encaps": False,
        "decaps": False,
        "shared_secret_match": False,
        "performance": {},
    }

    try:
        from mlkem.ml_kem import ML_KEM
        import time

        # Map level name to ML_KEM parameter
        param_map = {
            "ML-KEM-512": 512,
            "ML-KEM-768": 768,
            "ML-KEM-1024": 1024,
        }
        k = param_map.get(level_name)
        if k is None:
            result["error"] = f"Unknown level: {level_name}"
            return result

        ml_kem = ML_KEM(k)

        # Key generation
        t0 = time.perf_counter()
        ek, dk = ml_kem.key_gen()
        keygen_ms = (time.perf_counter() - t0) * 1000
        result["keygen"] = True
        result["performance"]["keygen_ms"] = round(keygen_ms, 2)

        # Verify key sizes
        result["pk_bytes"] = len(ek)
        result["sk_bytes"] = len(dk)

        # Encapsulation
        t0 = time.perf_counter()
        shared_secret, ciphertext = ml_kem.encaps(ek)
        encaps_ms = (time.perf_counter() - t0) * 1000
        result["encaps"] = True
        result["performance"]["encaps_ms"] = round(encaps_ms, 2)
        result["ct_bytes"] = len(ciphertext)

        # Decapsulation
        t0 = time.perf_counter()
        shared_secret_dec = ml_kem.decaps(dk, ciphertext)
        decaps_ms = (time.perf_counter() - t0) * 1000
        result["decaps"] = True
        result["performance"]["decaps_ms"] = round(decaps_ms, 2)

        # Verify shared secrets match
        result["shared_secret_match"] = (shared_secret == shared_secret_dec)
        result["ss_bytes"] = len(shared_secret)
        result["supported"] = result["shared_secret_match"]

    except Exception as e:
        result["error"] = str(e)

    return result


def _test_mlkem_openssl(level_name, params):
    """Test ML-KEM support via OpenSSL CLI (requires OpenSSL 3.5+ or oqs-provider)."""
    result = {
        "level": level_name,
        "security_level": params["security_level"],
        "supported": False,
        "error": None,
    }

    algo_map = {
        "ML-KEM-512": "mlkem512",
        "ML-KEM-768": "mlkem768",
        "ML-KEM-1024": "mlkem1024",
    }
    algo = algo_map.get(level_name, "mlkem768")

    try:
        # Test key generation with openssl
        proc = subprocess.run(
            ["openssl", "pkey", "-algorithm", algo, "-text", "-noout"],
            input=b"",
            capture_output=True,
            timeout=15,
        )

        # Also try via genpkey
        proc2 = subprocess.run(
            ["openssl", "genpkey", "-algorithm", algo, "-outform", "PEM"],
            capture_output=True,
            timeout=15,
        )

        if proc2.returncode == 0:
            result["supported"] = True
            result["keygen"] = True
            logger.info("OpenSSL supports %s (%s)", level_name, algo)
        else:
            stderr = proc2.stderr.decode("utf-8", errors="replace")
            result["error"] = stderr.split("\n")[0][:200] if stderr else "keygen failed"

    except subprocess.TimeoutExpired:
        result["error"] = "OpenSSL command timed out"
    except FileNotFoundError:
        result["error"] = "openssl binary not found"

    return result


# ---------------------------------------------------------------------------
# ML-DSA (FIPS 204) Validation
# ---------------------------------------------------------------------------

def test_mldsa_support():
    """
    Test ML-DSA (CRYSTALS-Dilithium / FIPS 204) digital signature support.

    Tests key generation, signing, and verification at all three security levels
    using OpenSSL with native support or oqs-provider.

    Returns:
        Dict with ML-DSA validation results
    """
    results = {
        "test_time": datetime.now(timezone.utc).isoformat(),
        "library": None,
        "levels": {},
    }

    algo_map = {
        "ML-DSA-44": "mldsa44",
        "ML-DSA-65": "mldsa65",
        "ML-DSA-87": "mldsa87",
    }

    for level_name, params in MLDSA_PARAMS.items():
        algo = algo_map.get(level_name, "mldsa65")
        level_result = _test_mldsa_openssl(level_name, algo, params)
        results["levels"][level_name] = level_result

    results["library"] = "OpenSSL"
    return results


def _test_mldsa_openssl(level_name, algo, params):
    """Test ML-DSA at a specific level via OpenSSL CLI."""
    result = {
        "level": level_name,
        "security_level": params["security_level"],
        "supported": False,
        "keygen": False,
        "sign": False,
        "verify": False,
        "error": None,
    }

    import tempfile

    try:
        with tempfile.TemporaryDirectory() as tmpdir:
            key_path = os.path.join(tmpdir, "key.pem")
            pub_path = os.path.join(tmpdir, "pub.pem")
            msg_path = os.path.join(tmpdir, "message.txt")
            sig_path = os.path.join(tmpdir, "signature.bin")

            # Generate key pair
            proc = subprocess.run(
                ["openssl", "genpkey", "-algorithm", algo, "-out", key_path],
                capture_output=True, timeout=30,
            )
            if proc.returncode != 0:
                stderr = proc.stderr.decode("utf-8", errors="replace")
                result["error"] = f"keygen failed: {stderr.split(chr(10))[0][:200]}"
                return result
            result["keygen"] = True

            # Extract public key
            proc = subprocess.run(
                ["openssl", "pkey", "-in", key_path, "-pubout", "-out", pub_path],
                capture_output=True, timeout=15,
            )
            if proc.returncode != 0:
                result["error"] = "public key extraction failed"
                return result

            # Create test message
            with open(msg_path, "w") as f:
                f.write("Post-quantum cryptography migration test message")

            # Sign
            proc = subprocess.run(
                ["openssl", "pkeyutl", "-sign",
                 "-inkey", key_path,
                 "-in", msg_path,
                 "-out", sig_path],
                capture_output=True, timeout=30,
            )
            if proc.returncode != 0:
                # Try dgst approach
                proc = subprocess.run(
                    ["openssl", "dgst", "-sign", key_path,
                     "-out", sig_path, msg_path],
                    capture_output=True, timeout=30,
                )
            if proc.returncode == 0:
                result["sign"] = True
            else:
                result["error"] = "signing failed"
                return result

            # Verify
            proc = subprocess.run(
                ["openssl", "pkeyutl", "-verify",
                 "-pubin", "-inkey", pub_path,
                 "-in", msg_path,
                 "-sigfile", sig_path],
                capture_output=True, timeout=30,
            )
            if proc.returncode != 0:
                proc = subprocess.run(
                    ["openssl", "dgst", "-verify", pub_path,
                     "-signature", sig_path, msg_path],
                    capture_output=True, timeout=30,
                )
            if proc.returncode == 0:
                result["verify"] = True
                result["supported"] = True
            else:
                result["error"] = "verification failed"

    except subprocess.TimeoutExpired:
        result["error"] = "OpenSSL command timed out"
    except FileNotFoundError:
        result["error"] = "openssl binary not found"

    return result


# ---------------------------------------------------------------------------
# Migration Roadmap Generation
# ---------------------------------------------------------------------------

def generate_migration_roadmap(scan_results, agility_assessment=None):
    """
    Generate a prioritized PQC migration roadmap.

    Prioritizes systems based on data sensitivity, exposure, crypto-agility,
    compliance requirements, and dependency chains.

    Args:
        scan_results: List of TLS scan results
        agility_assessment: Optional crypto-agility assessment

    Returns:
        Migration roadmap with phased recommendations
    """
    roadmap = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "nist_timeline": NIST_MIGRATION_DEADLINES,
        "executive_summary": "",
        "phases": [],
        "risk_register": [],
        "quick_wins": [],
    }

    total = len(scan_results)
    vuln_count = sum(1 for r in scan_results if r.get("quantum_vulnerable"))
    tls13_count = sum(1 for r in scan_results
                      if "1.3" in r.get("tls_version", ""))

    roadmap["executive_summary"] = (
        f"Scanned {total} TLS endpoints: {vuln_count} ({vuln_count/total*100:.0f}%) "
        f"use quantum-vulnerable algorithms. {tls13_count} ({tls13_count/total*100:.0f}%) "
        f"support TLS 1.3 (prerequisite for hybrid PQC). "
        f"NIST mandates deprecation of quantum-vulnerable algorithms by 2030 and "
        f"complete removal by 2035."
    ) if total > 0 else "No endpoints scanned."

    # Phase 1: Immediate (0-6 months)
    phase1_actions = [
        {
            "action": "Complete cryptographic inventory across all systems",
            "priority": "P0",
            "effort": "medium",
            "description": "Extend scanning beyond TLS to include code libraries, "
                           "key stores, HSMs, certificates, VPN configurations, "
                           "and embedded systems.",
        },
        {
            "action": "Upgrade OpenSSL to 3.5+ on development and staging systems",
            "priority": "P0",
            "effort": "low",
            "description": "OpenSSL 3.5 provides native ML-KEM, ML-DSA, SLH-DSA support. "
                           "For OpenSSL 3.0-3.4, install oqs-provider as interim solution.",
        },
        {
            "action": "Enable X25519MLKEM768 hybrid key exchange on TLS 1.3 endpoints",
            "priority": "P1",
            "effort": "low",
            "description": "Add X25519MLKEM768 to supported_groups in TLS configuration. "
                           "This is a drop-in change for servers already on TLS 1.3 with "
                           "OpenSSL 3.5+ or oqs-provider.",
        },
    ]

    # Phase 2: Short-term (6-18 months)
    phase2_actions = [
        {
            "action": "Upgrade all endpoints to TLS 1.3",
            "priority": "P1",
            "effort": "medium",
            "description": f"{total - tls13_count} endpoints need TLS 1.3 upgrade. "
                           "Hybrid PQC key exchange is only available in TLS 1.3.",
        },
        {
            "action": "Deploy hybrid key exchange across production infrastructure",
            "priority": "P1",
            "effort": "medium",
            "description": "Configure X25519MLKEM768 as preferred key exchange group "
                           "on all production TLS endpoints.",
        },
        {
            "action": "Test ML-DSA certificate chains in staging environments",
            "priority": "P2",
            "effort": "high",
            "description": "Issue test certificates with ML-DSA signatures from internal CA. "
                           "Validate certificate chain verification across all clients.",
        },
        {
            "action": "Assess HSM and KMS PQC compatibility",
            "priority": "P2",
            "effort": "medium",
            "description": "Verify that hardware security modules and key management "
                           "systems support PQC key sizes and algorithms.",
        },
    ]

    # Phase 3: Medium-term (18-36 months)
    phase3_actions = [
        {
            "action": "Migrate certificate infrastructure to hybrid or PQC signatures",
            "priority": "P2",
            "effort": "high",
            "description": "Deploy hybrid certificates (classical + ML-DSA) for backward "
                           "compatibility, then transition to pure ML-DSA.",
        },
        {
            "action": "Update code signing and software supply chain to PQC",
            "priority": "P2",
            "effort": "high",
            "description": "Migrate code signing certificates, package signatures, "
                           "and firmware signing to ML-DSA or SLH-DSA.",
        },
        {
            "action": "Replace quantum-vulnerable VPN and IPsec configurations",
            "priority": "P2",
            "effort": "medium",
            "description": "Upgrade VPN concentrators and IPsec configurations to "
                           "support PQC key exchange.",
        },
    ]

    # Phase 4: Long-term (36-60 months, by 2030)
    phase4_actions = [
        {
            "action": "Complete deprecation of all quantum-vulnerable algorithms",
            "priority": "P3",
            "effort": "high",
            "description": "Remove RSA, ECDH, ECDSA, DH, DSA from all systems. "
                           "Ensure 100% PQC coverage before NIST 2030 deadline.",
        },
        {
            "action": "Validate SLH-DSA (FIPS 205) as backup signature standard",
            "priority": "P3",
            "effort": "low",
            "description": "Maintain tested SLH-DSA deployment capability as fallback "
                           "in case ML-DSA is found vulnerable.",
        },
    ]

    roadmap["phases"] = [
        {"name": "Phase 1: Discovery and Quick Wins", "timeline": "0-6 months",
         "actions": phase1_actions},
        {"name": "Phase 2: Hybrid Deployment", "timeline": "6-18 months",
         "actions": phase2_actions},
        {"name": "Phase 3: Full PQC Migration", "timeline": "18-36 months",
         "actions": phase3_actions},
        {"name": "Phase 4: Algorithm Deprecation", "timeline": "36-60 months",
         "actions": phase4_actions},
    ]

    # Quick wins (can be done immediately with minimal effort)
    if tls13_count > 0:
        roadmap["quick_wins"].append({
            "action": f"Enable X25519MLKEM768 on {tls13_count} TLS 1.3 endpoints",
            "effort": "configuration change only",
            "impact": "Immediate quantum-resistant key exchange for existing TLS 1.3 servers",
        })

    roadmap["quick_wins"].append({
        "action": "Increase AES key sizes to 256-bit where currently using 128-bit",
        "effort": "configuration change",
        "impact": "Grover's algorithm halves effective symmetric key strength; "
                  "AES-256 provides 128-bit post-quantum security",
    })

    # Risk register
    roadmap["risk_register"] = [
        {
            "risk": "Harvest Now, Decrypt Later (HNDL) attacks",
            "description": "Adversaries record encrypted traffic today to decrypt when "
                           "quantum computers become available",
            "likelihood": "high",
            "impact": "critical for long-lived secrets (government, healthcare, finance)",
            "mitigation": "Priority migration of systems handling data with >10yr confidentiality",
        },
        {
            "risk": "Algorithm implementation vulnerabilities",
            "description": "Side-channel attacks or implementation bugs in new PQC libraries",
            "likelihood": "medium",
            "impact": "high",
            "mitigation": "Use NIST-validated implementations, conduct security audits, "
                          "deploy hybrid schemes for defense-in-depth",
        },
        {
            "risk": "Performance degradation",
            "description": "PQC algorithms have larger key/signature sizes and may be slower",
            "likelihood": "medium",
            "impact": "medium",
            "mitigation": "Benchmark PQC under production load, optimize TLS handshake "
                          "configurations, consider ML-KEM-768 (balanced performance)",
        },
        {
            "risk": "Compatibility issues",
            "description": "Older clients/devices may not support PQC algorithms",
            "likelihood": "high",
            "impact": "medium",
            "mitigation": "Hybrid schemes ensure backward compatibility; maintain classical "
                          "fallback during transition",
        },
    ]

    return roadmap


# ---------------------------------------------------------------------------
# OpenSSL and oqs-provider Configuration
# ---------------------------------------------------------------------------

def check_openssl_pqc_support():
    """
    Check the local OpenSSL installation for PQC algorithm support.

    Returns:
        Dict with OpenSSL version, provider status, and PQC algorithm availability
    """
    result = {
        "check_time": datetime.now(timezone.utc).isoformat(),
        "openssl_version": "",
        "providers": [],
        "pqc_kem_algorithms": [],
        "pqc_signature_algorithms": [],
        "hybrid_groups": [],
        "pqc_ready": False,
    }

    # Get OpenSSL version
    try:
        proc = subprocess.run(["openssl", "version", "-a"],
                              capture_output=True, timeout=5)
        result["openssl_version"] = proc.stdout.decode().strip()
    except (FileNotFoundError, subprocess.TimeoutExpired):
        result["error"] = "openssl not found"
        return result

    # List providers
    try:
        proc = subprocess.run(["openssl", "list", "-providers"],
                              capture_output=True, timeout=5)
        output = proc.stdout.decode()
        result["providers"] = [
            line.strip() for line in output.split("\n")
            if line.strip() and "name:" in line.lower()
        ]
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass

    # Check for PQC KEM algorithms
    try:
        proc = subprocess.run(["openssl", "list", "-kem-algorithms"],
                              capture_output=True, timeout=5)
        output = proc.stdout.decode()
        for line in output.split("\n"):
            line = line.strip().lower()
            if any(pqc in line for pqc in ["mlkem", "kyber", "bike", "hqc", "frodo"]):
                result["pqc_kem_algorithms"].append(line)
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass

    # Check for PQC signature algorithms
    try:
        proc = subprocess.run(["openssl", "list", "-signature-algorithms"],
                              capture_output=True, timeout=5)
        output = proc.stdout.decode()
        for line in output.split("\n"):
            line = line.strip().lower()
            if any(pqc in line for pqc in ["mldsa", "dilithium", "slhdsa", "sphincs", "falcon"]):
                result["pqc_signature_algorithms"].append(line)
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass

    # Check for hybrid groups
    try:
        proc = subprocess.run(["openssl", "list", "-tls1-3-groups"],
                              capture_output=True, timeout=5)
        output = proc.stdout.decode()
        for line in output.split("\n"):
            line_stripped = line.strip()
            if any(pqc in line_stripped.lower() for pqc in ["mlkem", "kyber"]):
                result["hybrid_groups"].append(line_stripped)
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass

    result["pqc_ready"] = bool(
        result["pqc_kem_algorithms"] or result["pqc_signature_algorithms"]
    )

    return result


def generate_oqs_provider_config():
    """
    Generate an OpenSSL configuration file for oqs-provider.

    Returns the configuration text for enabling PQC algorithms via
    the Open Quantum Safe provider in OpenSSL 3.x.
    """
    config = """# OpenSSL configuration for oqs-provider (Post-Quantum Cryptography)
# Place at /etc/ssl/openssl-oqs.cnf
# Set OPENSSL_CONF=/etc/ssl/openssl-oqs.cnf before running OpenSSL/nginx/Apache

openssl_conf = openssl_init

[openssl_init]
providers = provider_sect
ssl_conf = ssl_sect

[provider_sect]
default = default_sect
oqsprovider = oqsprovider_sect

[default_sect]
activate = 1

[oqsprovider_sect]
activate = 1
# Adjust path to match your oqs-provider installation
module = /usr/lib/oqs-provider/oqsprovider.so

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
# Hybrid PQC groups: prefer X25519MLKEM768 with classical fallbacks
Groups = x25519_mlkem768:X25519:P-256:P-384

# Minimum TLS version (1.3 required for PQC key exchange)
MinProtocol = TLSv1.2
"""
    return config


# ---------------------------------------------------------------------------
# Main CLI
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Post-Quantum Cryptography Migration Assessment Agent",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Actions:
  scan_tls        Scan TLS endpoints for quantum-vulnerable algorithms
  assess_agility  Assess crypto-agility from scan results
  test_hybrid_tls Test hybrid PQC TLS key exchange support
  test_mlkem      Test ML-KEM (FIPS 203) key encapsulation
  test_mldsa      Test ML-DSA (FIPS 204) digital signatures
  check_openssl   Check local OpenSSL PQC algorithm support
  roadmap         Generate prioritized PQC migration roadmap
  full_assessment Run complete assessment pipeline

Examples:
  python agent.py --action scan_tls --targets hosts.txt --output scan.json
  python agent.py --action scan_tls --target server.example.com:443
  python agent.py --action test_hybrid_tls --target server.example.com:443
  python agent.py --action test_mlkem --output mlkem.json
  python agent.py --action check_openssl
  python agent.py --action roadmap --scan-results scan.json --output roadmap.json
  python agent.py --action full_assessment --targets hosts.txt --output full_report.json
        """,
    )
    parser.add_argument("--action", required=True, choices=[
        "scan_tls", "assess_agility", "test_hybrid_tls",
        "test_mlkem", "test_mldsa", "check_openssl",
        "roadmap", "full_assessment",
    ])
    parser.add_argument("--target", default=None,
                        help="Single target host[:port] for scanning")
    parser.add_argument("--targets", default=None,
                        help="File with one host[:port] per line")
    parser.add_argument("--port", type=int, default=443,
                        help="Default port for TLS scanning")
    parser.add_argument("--scan-results", default=None,
                        help="Path to previous scan results JSON (for assess/roadmap)")
    parser.add_argument("--agility-results", default=None,
                        help="Path to agility assessment JSON (for roadmap)")
    parser.add_argument("--output", default="pqc_report.json",
                        help="Output file for results")
    args = parser.parse_args()

    report = {
        "agent": "pqc-migration-assessment",
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "action": args.action,
        "nist_standards": {
            "FIPS_203": "ML-KEM (CRYSTALS-Kyber) -- Key Encapsulation",
            "FIPS_204": "ML-DSA (CRYSTALS-Dilithium) -- Digital Signatures",
            "FIPS_205": "SLH-DSA (SPHINCS+) -- Digital Signatures (backup)",
        },
    }

    # --- TLS Scanning ---
    scan_results = []
    if args.action in ("scan_tls", "full_assessment"):
        if args.targets:
            scan_results = scan_multiple_endpoints(args.targets, args.port)
        elif args.target:
            host_port = args.target.split(":")
            host = host_port[0]
            port = int(host_port[1]) if len(host_port) > 1 else args.port
            scan_results = [scan_tls_endpoint(host, port)]
        else:
            print("[!] Provide --target or --targets for TLS scanning")
            sys.exit(1)

        report["tls_scan"] = scan_results
        vuln = sum(1 for r in scan_results if r.get("quantum_vulnerable"))
        print(f"[+] Scanned {len(scan_results)} endpoints: "
              f"{vuln} quantum-vulnerable")
        for r in scan_results:
            status = "VULNERABLE" if r.get("quantum_vulnerable") else "OK"
            err = r.get("error", "")
            if err:
                print(f"    {r['host']}:{r['port']} -- ERROR: {err}")
            else:
                print(f"    {r['host']}:{r['port']} -- [{status}] "
                      f"{r.get('cipher_suite', 'N/A')} ({r.get('tls_version', 'N/A')})")

    # --- Crypto-Agility Assessment ---
    if args.action in ("assess_agility", "full_assessment"):
        if not scan_results and args.scan_results:
            with open(args.scan_results, encoding="utf-8") as f:
                data = json.load(f)
                scan_results = data.get("tls_scan", data) if isinstance(data, dict) else data

        if scan_results:
            agility = assess_crypto_agility(scan_results)
            report["agility_assessment"] = agility
            print(f"[+] Crypto-agility score: {agility['agility_score']}/100")
            print(f"    Risk level: {agility['risk_summary'].get('risk_level', 'unknown')}")
            print(f"    TLS 1.3 ready: {agility['risk_summary'].get('tls13_percentage', 0)}%")
        else:
            print("[!] No scan results available for agility assessment")

    # --- Hybrid TLS Testing ---
    if args.action in ("test_hybrid_tls", "full_assessment"):
        target = args.target
        if not target and scan_results:
            target = f"{scan_results[0]['host']}:{scan_results[0]['port']}"
        if target:
            host_port = target.split(":")
            host = host_port[0]
            port = int(host_port[1]) if len(host_port) > 1 else 443
            hybrid_result = test_hybrid_tls_support(host, port)
            report["hybrid_tls"] = hybrid_result
            pqc_status = "SUPPORTED" if hybrid_result["pqc_supported"] else "NOT SUPPORTED"
            print(f"[+] Hybrid TLS (X25519MLKEM768): {pqc_status}")
            print(f"    OpenSSL: {hybrid_result.get('openssl_version', 'unknown')}")
            for group_test in hybrid_result.get("hybrid_groups_tested", []):
                status = "OK" if group_test.get("supported") else "FAIL"
                print(f"    {group_test['group']}: [{status}] "
                      f"{group_test.get('error', '')}")

    # --- ML-KEM Testing ---
    if args.action in ("test_mlkem", "full_assessment"):
        mlkem_result = test_mlkem_support()
        report["mlkem_validation"] = mlkem_result
        print(f"[+] ML-KEM (FIPS 203) validation via {mlkem_result.get('library', 'N/A')}:")
        for level, result in mlkem_result.get("levels", {}).items():
            status = "PASS" if result.get("supported") else "FAIL"
            perf = result.get("performance", {})
            perf_str = ""
            if perf:
                perf_str = (f" (keygen={perf.get('keygen_ms', '?')}ms, "
                            f"encaps={perf.get('encaps_ms', '?')}ms, "
                            f"decaps={perf.get('decaps_ms', '?')}ms)")
            print(f"    {level}: [{status}]{perf_str}")

    # --- ML-DSA Testing ---
    if args.action in ("test_mldsa", "full_assessment"):
        mldsa_result = test_mldsa_support()
        report["mldsa_validation"] = mldsa_result
        print(f"[+] ML-DSA (FIPS 204) validation:")
        for level, result in mldsa_result.get("levels", {}).items():
            status = "PASS" if result.get("supported") else "FAIL"
            err = result.get("error", "")
            print(f"    {level}: [{status}] {err}")

    # --- OpenSSL PQC Check ---
    if args.action in ("check_openssl", "full_assessment"):
        ossl = check_openssl_pqc_support()
        report["openssl_pqc"] = ossl
        print(f"[+] OpenSSL PQC support check:")
        print(f"    Version: {ossl.get('openssl_version', 'unknown')}")
        print(f"    PQC ready: {ossl.get('pqc_ready', False)}")
        if ossl.get("pqc_kem_algorithms"):
            print(f"    KEM algorithms: {', '.join(ossl['pqc_kem_algorithms'][:5])}")
        if ossl.get("pqc_signature_algorithms"):
            print(f"    Signature algorithms: {', '.join(ossl['pqc_signature_algorithms'][:5])}")
        if ossl.get("hybrid_groups"):
            print(f"    Hybrid groups: {', '.join(ossl['hybrid_groups'][:5])}")

        if not ossl.get("pqc_ready"):
            print("\n[*] To enable PQC, either:")
            print("    1. Upgrade to OpenSSL 3.5+ (native ML-KEM/ML-DSA)")
            print("    2. Install oqs-provider for OpenSSL 3.0+:")
            print("       https://github.com/open-quantum-safe/oqs-provider")
            config = generate_oqs_provider_config()
            report["oqs_provider_config"] = config

    # --- Migration Roadmap ---
    if args.action in ("roadmap", "full_assessment"):
        if not scan_results and args.scan_results:
            with open(args.scan_results, encoding="utf-8") as f:
                data = json.load(f)
                scan_results = data.get("tls_scan", data) if isinstance(data, dict) else data

        agility = None
        if args.agility_results:
            with open(args.agility_results, encoding="utf-8") as f:
                agility = json.load(f)

        if scan_results:
            roadmap = generate_migration_roadmap(scan_results, agility)
            report["migration_roadmap"] = roadmap
            print(f"\n[+] Migration Roadmap")
            print(f"    {roadmap['executive_summary']}")
            print(f"\n    NIST Timeline: deprecation by {NIST_MIGRATION_DEADLINES['deprecation']}, "
                  f"removal by {NIST_MIGRATION_DEADLINES['disallowed']}")
            for phase in roadmap["phases"]:
                print(f"\n    {phase['name']} ({phase['timeline']}):")
                for action in phase["actions"]:
                    print(f"      [{action['priority']}] {action['action']}")
            if roadmap["quick_wins"]:
                print(f"\n    Quick Wins:")
                for qw in roadmap["quick_wins"]:
                    print(f"      - {qw['action']}")
        else:
            print("[!] No scan results available for roadmap generation")

    # --- Write report ---
    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2, default=str)
    print(f"\n[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
Keep exploring