Migration tooling¶
The migration module helps you move an existing codebase from classical cryptography to hybrid PQC without breaking existing integrations.
Scanning for classical crypto¶
Scanner uses Python AST analysis
to detect classical-only cryptographic usage. It ships 14 built-in rules
covering:
RSA and ECDSA key generation and signing (
cryptography,pycryptodome)AES-ECB and 3DES usage
MD5 and SHA-1 digest usage — via both the
cryptographylibrary (hashes.MD5,hashes.SHA1) and the stdlibhashlibmodule (hashlib.md5(),hashlib.sha1(),hashlib.sha224())Classical JWT algorithm identifiers (
RS256,ES256, etc.)Hard-coded cryptographic constants
Note
The scanner is Python-only: it walks .py source files using the
ast module. JavaScript, Go, Java, and configuration files (nginx,
openssl.cnf) are not yet supported.
from quantum_safe.migrate import Scanner
report = Scanner.scan_directory("./src")
print(report.summary())
# Scanned 42 files in './src': 2 CRITICAL, 5 HIGH, 3 MEDIUM
for finding in report.critical + report.high:
print(f"{finding.file}:{finding.line} [{finding.rule_id}]")
print(f" {finding.message}")
print(f" Fix: {finding.fix_hint}")
# Exit 1 in CI if blocking findings exist
if report.has_blocking_findings:
import sys; sys.exit(1)
SARIF output (GitHub Code Scanning):
report = Scanner.scan_directory("./src")
sarif = report.to_sarif()
with open("migrate.sarif", "w") as f:
import json; json.dump(sarif, f)
Upgrading an existing key to hybrid¶
Upgrader takes an existing
classical key and produces a hybrid keypair. Old senders that still
use the classical-only public key can still encrypt to the new public key.
from quantum_safe.migrate import Upgrader
result = Upgrader.upgrade_kem_key(
classical_secret_bytes=x25519_private_bytes,
classical_public_bytes=x25519_public_bytes,
classical_algorithm="X25519",
target_pqc="ML-KEM-768",
)
new_kp = result.new_keypair
print(new_kp.public.algorithm) # "X25519+ML-KEM-768"
print(result.notes) # human-readable upgrade notes
Tracking migration progress¶
MigrationStateManager maintains
a per-key state machine tracking where each key sits in the migration path:
CLASSICAL_ONLY→HYBRID_TRANSITION→PQC_PREFERRED→PQC_ONLY
Note
Thread safety: transition() holds a per-key threading.Lock across
the read-check-write critical section, so concurrent in-process calls for the
same key_id are safe. For multi-process deployments (multiple workers
sharing a Redis or database store) you must additionally hold an external
distributed lock (e.g. Redis SETNX, a SELECT … FOR UPDATE row lock)
on the key_id before calling transition().
from quantum_safe.migrate import MigrationStateManager
from quantum_safe.types import MigrationState
store = {} # replace with Redis / DynamoDB / Postgres
mgr = MigrationStateManager(store)
mgr.transition(
key_id="user-123",
from_state=MigrationState.CLASSICAL_ONLY,
to_state=MigrationState.HYBRID_TRANSITION,
algorithm="X25519+ML-KEM-768",
actor="key-rotation-v2",
)
progress = mgr.migration_progress()
print(progress)
# {'classical_only': 847, 'hybrid_transition': 152, 'pqc_only': 1}
Drop-in shims¶
FernetShim and
JWTShim are drop-in replacements for
cryptography.fernet.Fernet and PyJWT. They log every usage so
you can identify callers before migrating them:
from quantum_safe.migrate.shims import FernetShim
# Drop-in for cryptography.fernet.Fernet
f = FernetShim(key)
tok = f.encrypt(b"payload") # logs: "FernetShim.encrypt called from ..."
msg = f.decrypt(tok)
from quantum_safe.migrate.shims import JWTShim
tok = JWTShim.encode({"sub": "1"}, secret, algorithm="HS256")
claims = JWTShim.decode(tok, secret, algorithms=["HS256"])
CLI¶
# Scan a codebase
qs-migrate scan ./src --format sarif --output migrate.sarif
# Check migration progress
qs-migrate status