Post-quantum crypto liboqs-backed. Every NIST PQC algorithm digger picks up at runtime — FIPS-finalized, Round 4, and the additional signature on-ramp.
Why PQC?
Classical signatures (RSA, ECDSA) become forgeable once a sufficiently large quantum computer exists. Evidence stores you sign today are expected to remain forensically valid for years; signing them with a post-quantum algorithm now means the signature is durable across that horizon (harvest-now-decrypt-later doesn't apply to signatures the way it does to KEM-encapsulated secrets, but durable non-repudiation does).
digger uses post-quantum signatures for evidence-chain attestation and post-quantum KEM + AES-256-GCM hybrid encryption for case-bundle archival.
Backend
We delegate to liboqs via the
oqs-python binding. liboqs is the canonical reference
implementation tracking every algorithm in the NIST PQC standardization
process. Re-implementing PQC primitives is a footgun; we don't.
The set of algorithms actually usable on your host is exactly whatever your installed liboqs build exposes:
$ digger pqc info --mode fips
or, in code:
from digger.crypto import available_kems, available_sigs
print(available_sigs()) # list[str]
print(available_kems())
Algorithm coverage
FIPS-finalized
The algorithms NIST has published as Federal Information Processing Standards. These are the only algorithms allowed under FIPS mode.
| FIPS | Family | Algorithms |
|---|---|---|
| 203 | Module-Lattice KEM (Kyber) | ML-KEM-512, ML-KEM-768, ML-KEM-1024 |
| 204 | Module-Lattice DSA (Dilithium) | ML-DSA-44, ML-DSA-65, ML-DSA-87 |
| 205 | Stateless Hash-Based DSA (SPHINCS+) | SLH-DSA-SHA2-{128,192,256}{s,f}, SLH-DSA-SHAKE-{128,192,256}{s,f} |
| 206 (draft) | FN-DSA (Falcon) | Falcon-512, Falcon-1024, Falcon-padded-{512,1024} |
NIST Round 4
Algorithms still under active review for additional KEM diversity.
- HQC-128 / 192 / 256 — code-based KEM, selected as the 5th KEM standard in 2025; FIPS draft pending.
- BIKE-L1 / L3 / L5 — quasi-cyclic code-based KEM.
- Classic-McEliece-348864 .. 8192128 — conservative code-based KEM with very large public keys.
Signature on-ramp
Round 1 of the additional-signature competition that NIST opened to diversify beyond lattice-based signatures. Names below track liboqs identifiers — digger autodetects whatever your build enables.
Included in our tables (and matched against availability): CROSS (RSDP / RSDPG, balanced/fast/small variants), MAYO-1/2/3/5, OV / UOV (Is, Ip, III, V with pkc / pkc-skc), SNOVA (24_5_4, 37_17_2, …), HAWK-512 / 1024, FAEST (regular and EM variants), LESS, MiRitH, MQOM (gf31, gf251), PERK, QR-UOV, RYDE, SDitH (gf256, gf251), SQIsign-I / III / V.
This is a moving target. Upgrade liboqs to pick up new ones — digger needs no code changes.
Signing the evidence chain
The signed payload is canonical bytes derived from the chain tip: the current artifact-table chain hash, the current finding-table chain hash, and the case_id. This binds the signature to a specific case state.
digger pqc sign --case-dir ./case-1 \
--algorithm ML-DSA-65 \
--key /secrets/digger.sk \
--note "investigator: analyst; engagement: q2-2026-ir"
- --algorithm
- Default
ML-DSA-65. Anything inavailable_sigs(). - --key
- Secret key file. Auto-generated (chmod 600) plus
.pubif it doesn't exist. - --note
- Free-form note baked into the signature bundle.
Output is case_signature.json in the case directory:
{
"algorithm": "ML-DSA-65",
"public_key_b64": "…",
"signature_b64": "…",
"message_sha256": "…",
"created": 1747780000.0,
"note": "investigator: analyst; engagement: q2-2026-ir",
"scheme": "NIST-PQC"
}
To verify later:
digger pqc verify --case-dir ./case-1
Exit code 0 on success, 1 on failure.
Hybrid PQC-KEM + AES-256-GCM encryption
For archiving a case bundle to untrusted storage, use the hybrid encryption
helpers in digger/crypto/pqc.py:
from digger.crypto import PQCBackend, hybrid_encrypt, hybrid_decrypt
backend = PQCBackend(kem_alg="ML-KEM-768")
pk, sk = backend.generate_kem_key() # recipient keypair
blob = hybrid_encrypt(b"<evidence bytes>", pk) # → dict with KEM ciphertext + AES-GCM ciphertext
plaintext = hybrid_decrypt(blob, sk) # recovers original
The shared secret from the KEM is run through HKDF-SHA256 to derive a 256-bit AES-GCM key with a per-message random nonce. Associated-data binding prevents cross-context replay.
FIPS-mode interaction
When FIPS mode is on, PQCBackend.generate_signing_key() and
generate_kem_key() call assert_approved_sig() /
assert_approved_kem() which raise FIPSViolation for
non-finalized algorithms:
$ digger --fips-mode pqc sign --case-dir x --algorithm CROSS-rsdp-128-balanced ...
digger.fips.mode.FIPSViolation: signature algorithm 'CROSS-rsdp-128-balanced' is not FIPS-approved.
In FIPS mode you must use one of ['Falcon-1024', 'Falcon-512', 'Falcon-padded-1024',
'Falcon-padded-512', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'SLH-DSA-SHA2-128f', ...]
When FIPS mode is off, every algorithm liboqs supports is fair game.
Notes on actual FIPS 140-3 compliance
Important. This module is a user-mode wrapper. True FIPS 140-3 compliance also requires:digger's FIPS mode enforces algorithm restrictions and runs known-answer tests, but does not by itself constitute a validated cryptographic module.
- An OpenSSL FIPS provider loaded, or a CMVP-listed liboqs build
- OS-level FIPS mode enabled (kernel.fips_enabled=1 on Linux, FipsAlgorithmPolicy on Windows, corecrypto on macOS by default)
- Independent module validation