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 cryptography library (hashes.MD5, hashes.SHA1) and the stdlib hashlib module (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_ONLYHYBRID_TRANSITIONPQC_PREFERREDPQC_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