Skip to main content

sk-pqc (Python)

PyPI Python License: Apache-2.0 Suite

pip install sk-pqc          # core; hybrid KEM needs the pq extra
pip install "sk-pqc[pq]"    # + the ML-KEM-768 leg (liboqs)

⚠️ Experimental · pre-1.0 · NOT independently security-audited. This is a clean-room reference implementation — tested and cross-impl-parity-verified against our Rust (sk-pqc) and Dart (sk_pqc) builds, but it has had no third-party security audit, fuzzing, or formal review. Primitives bind vetted libraries (liboqs/ML-KEM, cryptography); the original code is the wiring. Review it yourself before production use. We apply our own honest-claims discipline to the library itself: don't trust it beyond the evidence.

sk-pqc is a small, app-agnostic Python library of vetted hybrid post-quantum cryptographic primitives. Use it to add hybrid X25519 + ML-KEM-768 (FIPS 203) confidentiality to any app — without dragging in a messaging framework.

It is the Python sibling of the public Dart sk_pqc package and is byte-for-byte interoperable with it (shared cross-impl KAT vector). Import name sk_pqc; PyPI name sk-pqc.

  • Maturity tier: T2 — Hybrid KEM (key exchange/wrap is HKDF(X25519 ‖ ML-KEM-768); signatures stay classical/optional-hybrid). Per sk-standards CRYPTOGRAPHY_STANDARD.
  • License: Apache-2.0 · Python: ≥ 3.10 · Version: 0.1.0

Honest claim. "Hybrid" means a derived secret is confidential if EITHER the classical X25519 leg OR the ML-KEM-768 leg holds — it survives a future cryptographically-relevant quantum computer breaking X25519, and it survives a classical break of ML-KEM. It is not "quantum-proof", "quantum-safe", or "unbreakable". AES-256-GCM (the bulk cipher) is symmetric / Grover-only and already quantum-acceptable. Citations: FIPS 203 (ML-KEM), FIPS 204 (ML-DSA, registry only), RFC 7748 (X25519), RFC 5869 (HKDF), SP 800-38D (AES-GCM).


What's in the box

Module Primitive What it gives you
sk_pqc.pqkem Hybrid KEM x25519-mlkem768 hybrid_keypair / hybrid_encap / hybrid_decap. ML-KEM-768 leg = liboqs (oqs), X25519 leg + HKDF combiner = pyca cryptography. The combiner is the only original crypto: HKDF-SHA256(X25519_ss ‖ MLKEM768_ss) — concat-then-KDF, never XOR, never pure-PQ.
sk_pqc.pqdm PQXDH-style seal seal / open_sealed a body to a recipient's published hybrid prekey (PrekeyBundle), AES-256-GCM under a KEM-derived key, with a downgrade-lock AAD that makes silent classical downgrade detectable.
sk_pqc.pqroute Metadata-sealing routing envelope pqroute1 seal_routed / open_routed / read_route_header. Splits a plaintext next-hop header (a relay reads it, but it is AEAD-bound / tamper-evident) from a hybrid-sealed inner (final destination + content) a relay cannot read.
sk_pqc.group_ratchet Group epoch ratchet Per-epoch secret distributed once via the hybrid KEM; per-message keys derived symmetrically + index-addressable (loss/reorder tolerant). Forward secrecy across epochs, post-compromise security from independent epoch secrets.
sk_pqc.dm_ratchet 1:1 DM epoch ratchet The pairwise analogue of the group ratchet (distinct HKDF domain labels — a DM key can never collide with a group key).
sk_pqc.anon_queue Anon-queue addressing + deniable auth new_queue_pair (uncorrelated recipient/sender ids), the aqid: address codec, and a repudiable HMAC-SHA256 authenticator. Addressing + deniable-auth only — not a transport.
sk_pqc.crypto_suites Crypto-agility registry Machine-readable suite-ids → primitives + quantum-resistance status + FIPS refs. The honest predicate is_quantum_resistant(suite_id) no caller should hand-roll.

Never silently downgrades. If the liboqs backend is missing, hybrid operations raise PqKemUnavailable (a hard error). The pure-pyca pieces — combiner KAT, suite registry, anon-queue codec/MAC, key derivation — work with no PQ backend at all.


Architecture

flowchart TD
    subgraph backends["Vetted backends — no hand-rolled math"]
        OQS["liboqs (oqs)<br/>ML-KEM-768 · FIPS 203"]
        PYCA["pyca/cryptography<br/>X25519 · HKDF-SHA256 · AES-256-GCM"]
    end

    OQS --> KEM
    PYCA --> KEM

    KEM["pqkem<br/>hybrid X25519 ‖ ML-KEM-768 KEM<br/>HKDF(X25519_ss ‖ MLKEM_ss)"]

    KEM --> DM["pqdm<br/>PQXDH-style seal<br/>(downgrade-lock AAD)"]
    KEM --> ROUTE["pqroute1<br/>metadata-sealing<br/>routing envelope"]
    KEM --> GR["group_ratchet<br/>per-epoch group keys"]
    KEM --> DMR["dm_ratchet<br/>per-epoch 1:1 DM keys"]

    REG["crypto_suites<br/>agility registry<br/>+ honest self-report"]
    AQ["anon_queue<br/>aqid: addressing<br/>+ deniable HMAC auth"]

    REG -. "describes / status" .-> KEM
    REG -. "describes / status" .-> DM

    KEM --> VEC{{"cross-impl KAT vector<br/>↔ Dart sk_pqc"}}

    classDef prim fill:#e6f0ff,stroke:#369;
    classDef be fill:#eee,stroke:#999;
    class KEM,DM,ROUTE,GR,DMR,REG,AQ prim;
    class OQS,PYCA be;

Install

# Core (pure-pyca pieces work; hybrid KEM needs the pq extra)
pip install sk-pqc

# With the post-quantum (ML-KEM-768) leg via liboqs
pip install "sk-pqc[pq]"

The ML-KEM leg uses liboqs-python (import name oqs), which binds the native liboqs. Point oqs at a prebuilt liboqs.so with OQS_INSTALL_PATH (or SK_PQC_LIBOQS) to avoid a source build — sk_pqc.pqkem.ensure_liboqs_path() applies this best-effort on import.

Quickstart

from sk_pqc import hybrid_keypair, hybrid_encap, hybrid_decap

kp = hybrid_keypair()                       # 1216 B pub, 2432 B priv
ct, ss_sender = hybrid_encap(kp.public_key) # 1120 B ciphertext + 32 B secret
ss_recipient  = hybrid_decap(ct, kp.private_key)
assert ss_sender == ss_recipient            # secure if EITHER leg holds
from sk_pqc import PrekeyBundle, seal, open_sealed, SUITE_ID

bundle = PrekeyBundle(suite=SUITE_ID, hybrid_public_hex=kp.public_key.hex())
blob = seal(b"top secret", bundle, sender="alice", recipient="bob")
assert open_sealed(blob, kp.private_key, sender="alice", recipient="bob") == b"top secret"

1:1 DM epoch ratchet — distribute one epoch secret over the hybrid KEM, then key many messages off it symmetrically (the ~1.1 KB ML-KEM ciphertext is paid once per epoch, not per message):

import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from sk_pqc import DmRatchet, hybrid_keypair
from sk_pqc.dm_ratchet import new_epoch_secret, wrap_dm_epoch_secret, unwrap_dm_epoch_secret

bob = hybrid_keypair()
e0 = new_epoch_secret()
bob_e0 = unwrap_dm_epoch_secret(wrap_dm_epoch_secret(e0, bob.public_key), bob.private_key)

alice = DmRatchet(epoch=0, epoch_secret=e0)
bob_r = DmRatchet(epoch=0, epoch_secret=bob_e0)

idx, key = alice.next_outbound_key()           # carry idx on the wire
nonce = os.urandom(12)
ct = AESGCM(key).encrypt(nonce, b"hi bob", None)
assert AESGCM(bob_r.message_key(index=idx)).decrypt(nonce, ct, None) == b"hi bob"

Runnable examples

The examples/ directory has self-contained scripts (each is also a smoke test — they assert their own correctness):

Script What it shows
examples/hybrid_kem_roundtrip.py Hybrid KEM encap/decap roundtrip + using the shared secret as an AES-256-GCM key.
examples/dm_ratchet_roundtrip.py Two-party (Alice↔Bob) DM-ratchet roundtrip: per-epoch KEM wrap, symmetric per-message keys, out-of-order delivery, post-compromise rekey.
examples/bench.py timeit micro-benchmark of keygen/encap/decap (the table below).
SK_PQC_LIBOQS=$HOME/.local/lib/liboqs.so LD_LIBRARY_PATH=$HOME/.local/lib \
  python examples/hybrid_kem_roundtrip.py
SK_PQC_LIBOQS=$HOME/.local/lib/liboqs.so LD_LIBRARY_PATH=$HOME/.local/lib \
  python examples/dm_ratchet_roundtrip.py

Benchmarks

Whole-operation timings for the hybrid KEM (each op includes both the X25519 leg and the ML-KEM-768 leg plus the HKDF combiner — what a caller actually pays). Measured with examples/bench.py (timeit, 500 iters × 5 batches, median per call):

sk_pqc hybrid KEM bench  (suite x25519-mlkem768)
  python   3.12.3 (x86_64)
  platform Linux-6.17.0-35-generic-x86_64-with-glibc2.39

op          median (us)      mean (us)      ops/sec
----------------------------------------------------
keygen            226            279          ~4,400
encap             360            512          ~2,800
decap             341            416          ~2,900

Sub-millisecond per operation on a commodity x86-64 CPU (no GPU); the ML-KEM-768 leg dominates over X25519. These are machine-specific — reproduce on your own hardware with python examples/bench.py [iters]. Note the per-epoch DM-ratchet design means this KEM cost is amortised across every message in an epoch, not paid per message.

Test

# From a checkout (run from HOME to avoid local-namespace collisions)
cd ~ && python -m pytest /path/to/sk-pqc-py/tests -q

The cross-implementation interop gate (test_pqkem.py::test_cross_impl_vector_matches_sk_pqc) decapsulates the shared Dart/Python KAT vector and asserts the recorded shared secret — this is what proves the two implementations agree byte-for-byte. PQ tests skip cleanly if liboqs is unavailable; the pure-pyca combiner KAT + registry tests always run.

API docs

Full HTML API reference (every public symbol, generated from docstrings with pdoc) lives in docs/api/ — open docs/api/index.html in a browser. The pages carry the same experimental / not-audited banner as this README.

Regenerate after changing any public API (pure-Python, no compile):

pip install pdoc
scripts/build-api-docs.sh   # or: make docs

⚠️ docs/api/ is generated HTML committed to the repo, and nothing regenerates or validates it on push. It is a snapshot that will drift silently from the docstrings. If it disagrees with src/, src/ wins. Regenerating it is a manual step, so treat the pages as a convenience, not as the contract.

See also the prose docs/ARCHITECTURE.md.

Self-report (claim evidence)

from sk_pqc import get_suite, is_quantum_resistant
s = get_suite("x25519-mlkem768")
print(s.status.value, s.fips_refs, is_quantum_resistant("x25519-mlkem768"))
# hybrid-pq ('FIPS 203', 'RFC 7748', 'RFC 5869') True

This output is machine-checked. SOP.md's docs-evidence block executes that exact call on every push and asserts each field, so the claim cannot drift away from the code without the docs-check gate going red. The registry it reads (crypto_suites) is pure stdlib, so the self-report answers even where liboqs is absent and the hybrid operations would raise PqKemUnavailable.

Provenance / clean-room

The pqroute1 routing split and the anon_queue addressing are inspired by no-identity / mix-network messaging designs (clean-room, original implementation) — only the protocol idea; no third-party code was copied or translated. The wire formats, codecs, and MAC constructions are original and built solely on the vetted backends above.

  • ↔️ Sibling (Dart): sk_pqc (pub.dev) — the Dart hybrid-KEM companion this package interoperates with (shared KAT vector).
  • ↔️ Sibling (Rust): sk-pqc (crates.io) — the Rust implementation of the same suite + wire formats (full module set: kem/pqdm/pqroute/ratchets/anon_queue/suites).
  • ⬇️ Used by: skcomms — sovereign multi-transport comms (envelope payload + routing seal).
  • ⬇️ Used by: skchat — AI-native encrypted chat (group + 1:1 DM ratchets).
  • ↔️ Sibling: sk_pgp — sovereign OpenPGP-PQC signing library (the signature counterpart).
  • 📐 Standards: sk-standards — crypto, data-flow, version, and doc/SOP standards this repo conforms to.

Metadata

Release files for sk-pqc 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sk-pqc 0.1.2
File Size Uploaded
sk_pqc-0.1.2.tar.gz 60.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sk-pqc 0.1.2
File Interpreter ABI Platform
sk_pqc-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 106.3 kB

Release files / sk_pqc-0.1.2.tar.gz

Download URL sk_pqc-0.1.2.tar.gz
Size 60.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c67559ecb7f940fb2ddf2cdc5b96e19d274bd249a2296af0acbe461192f0007a
BLAKE2b-256 checksum
How to use checksums
e031cdf140175e939a8150e22bb6582a20c74d33ced6dc3f4538b941a1bb63e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sk_pqc-0.1.2-py3-none-any.whl

Download URL sk_pqc-0.1.2-py3-none-any.whl
Size 45.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e2d43d5ef053e7cfe6ef5cae6dfec39ff468d98210fde43ae55d4a209e5eca4f
BLAKE2b-256 checksum
How to use checksums
bacd5d9fb099fbcb1acb9be1b19891c7b74a170d35d9d0dbb457bbf8c187e379
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page