libsoliton/soliton_py
Kamal Tufekcic 18af877ef0
All checks were successful
CI / lint (push) Successful in 1m35s
CI / test-python (push) Successful in 1m46s
CI / test-zig (push) Successful in 1m37s
CI / test-wasm (push) Successful in 1m52s
CI / test (push) Successful in 14m22s
CI / miri (push) Successful in 13m57s
CI / build (push) Successful in 1m6s
CI / fuzz-regression (push) Successful in 9m4s
CI / publish-python (push) Successful in 1m46s
CI / publish (push) Successful in 1m52s
CI / publish-wasm (push) Successful in 1m55s
initial commit
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-03 22:42:26 +03:00
..
python/soliton initial commit 2026-04-02 23:48:10 +03:00
src initial commit 2026-04-02 23:48:10 +03:00
tests initial commit 2026-04-02 23:48:10 +03:00
Cargo.toml initial commit 2026-04-03 22:42:26 +03:00
LICENSE.md initial commit 2026-04-02 23:48:10 +03:00
pyproject.toml initial commit 2026-04-03 22:42:26 +03:00
README.md initial commit 2026-04-03 22:42:26 +03:00

soliton

Python bindings for libsoliton — a pure-Rust post-quantum cryptographic library providing composite identity keys (X-Wing + ML-DSA-65), hybrid signatures, KEM-based authentication, asynchronous key exchange, double-ratchet message encryption, streaming AEAD, and encrypted storage.

Install

pip install soliton-py

Builds from source via maturin — requires a Rust toolchain.

Quick Start

Identity Keys

import soliton

# Generate a post-quantum identity keypair.
with soliton.Identity.generate() as alice:
    # Sign a message (Ed25519 + ML-DSA-65 hybrid).
    sig = alice.sign(b"hello")
    alice.verify(b"hello", sig)

    # Fingerprint (SHA3-256 of public key).
    print(alice.fingerprint_hex())

    # Persist keys.
    pk = alice.public_key()   # 3200 bytes
    sk = alice.secret_key()   # 2496 bytes
# Secret key is zeroized on context exit.

Key Exchange (KEX)

# Bob generates a signed pre-key.
spk_pub, spk_sk = soliton.xwing_keygen()
spk_sig = soliton.kex_sign_prekey(bob_sk, spk_pub)

# Alice initiates a session.
initiated = soliton.kex_initiate(
    alice_pk, alice_sk, bob_pk,
    spk_pub, spk_id=1, spk_sig=spk_sig,
    crypto_version="lo-crypto-v1",
)

# Bob receives the session.
received = soliton.kex_receive(
    bob_pk, bob_sk, alice_pk,
    initiated.session_init_encoded(),
    initiated.sender_sig(), spk_sk,
)

Ratchet (Ongoing Messaging)

# First message (pre-ratchet).
aad = soliton.kex_build_first_message_aad(
    initiated.sender_fingerprint(),
    initiated.recipient_fingerprint(),
    initiated.session_init_encoded(),
)
ct, rik_a = soliton.Ratchet.encrypt_first_message(
    initiated.take_initial_chain_key(), b"hello bob", aad,
)
pt, rik_b = soliton.Ratchet.decrypt_first_message(
    received.take_initial_chain_key(), ct, aad,
)

# Initialize ratchets.
with soliton.Ratchet.init_alice(
    initiated.take_root_key(), rik_a,
    alice_fp, bob_fp, initiated.ek_pk(), initiated.ek_sk(),
) as alice_r:
    header, ciphertext = alice_r.encrypt(b"message 1")

    # Serialize for persistence.
    blob, epoch = alice_r.to_bytes()  # consumes the ratchet

Encrypted Storage

with soliton.StorageKeyRing(version=1, key=key_bytes) as ring:
    blob = ring.encrypt_blob("channel-1", "segment-0", plaintext)
    data = ring.decrypt_blob("channel-1", "segment-0", blob)

    # Key rotation.
    ring.add_key(version=2, key=new_key, make_active=True)

Streaming AEAD (File Encryption)

with soliton.StreamEncryptor(key) as enc:
    header = enc.header()  # 26 bytes — send first
    ct1 = enc.encrypt_chunk(chunk1)            # non-final: must be 1 MiB
    ct2 = enc.encrypt_chunk(chunk2, is_last=True)  # final: any size

with soliton.StreamDecryptor(key, header) as dec:
    pt1, is_last = dec.decrypt_chunk(ct1)
    pt2, is_last = dec.decrypt_chunk(ct2)

Authentication (Zero-Knowledge)

# Server generates challenge.
ct, token = soliton.auth_challenge(client_pk)

# Client responds.
proof = soliton.auth_respond(client_sk, ct)

# Server verifies (constant-time).
assert soliton.auth_verify(token, proof)

Primitives

digest = soliton.sha3_256(b"data")                      # 32 bytes
tag = soliton.hmac_sha3_256(key, data)                   # 32 bytes
okm = soliton.hkdf_sha3_256(salt, ikm, info, length=64)  # variable
ok = soliton.hmac_sha3_256_verify(tag1, tag2)             # constant-time
phrase = soliton.verification_phrase(pk_a, pk_b)           # 6 EFF words

Error Handling

All errors are subclasses of soliton.SolitonError:

Exception Meaning
AeadError AEAD decryption failed (wrong key, tampered ciphertext)
VerificationError Signature verification failed
BundleVerificationError Pre-key bundle invalid
DuplicateMessageError Replayed message counter
ChainExhaustedError Counter-space exhausted — re-establish session
InvalidLengthError Wrong-size parameter
InvalidDataError Malformed input

Context Managers

All types holding secret material support with statements for automatic zeroization:

with soliton.Identity.generate() as id:
    ...  # secret key zeroized on exit

with soliton.Ratchet.init_alice(...) as r:
    ...  # ratchet state reset on exit

with soliton.StorageKeyRing(1, key) as ring:
    ...  # key material zeroized on exit

Documentation

License

AGPL-3.0-only