Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
168 lines
4.8 KiB
Markdown
168 lines
4.8 KiB
Markdown
# soliton
|
|
|
|
Python bindings for [libsoliton](https://git.lo.sh/lo/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
|
|
|
|
```bash
|
|
pip install soliton
|
|
```
|
|
|
|
Builds from source via [maturin](https://www.maturin.rs/) — requires a Rust toolchain.
|
|
|
|
## Quick Start
|
|
|
|
### Identity Keys
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
- [Specification.md](https://git.lo.sh/lo/libsoliton/src/branch/main/Specification.md) — full cryptographic specification
|
|
- [CHEATSHEET.md](https://git.lo.sh/lo/libsoliton/src/branch/main/CHEATSHEET.md) — API quick reference
|
|
- [Abstract.md](https://git.lo.sh/lo/libsoliton/src/branch/main/Abstract.md) — formal security model
|
|
|
|
## License
|
|
|
|
[AGPL-3.0-only](LICENSE.md)
|