mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
1569 lines
55 KiB
Python
1569 lines
55 KiB
Python
#!/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()
|