purecrypto
A cryptography toolkit written entirely in Rust, depending on no foreign
code. purecrypto is built from the ground up — starting at constant-time
primitives and working up through hashing, ciphers, bignum arithmetic, the
classical and post-quantum asymmetric stacks, ASN.1, X.509 and TLS — and is
usable three ways:
- as a Rust library,
- as a C library (
cdylibwith a C ABI), and - as a standalone command-line tool (
purecrypto: hashing, randomness, key generation including PQ, CSRs, a small CA, a TLS 1.3 test client, …).
Status: work in progress. Everything below is implemented and validated against published test vectors (RFCs, NIST FIPS ACVP, OpenSSL interop), but APIs are unstable and nothing here has been audited — do not use it for anything real yet.
Design principles
- No foreign code. No C, no assembly pulled from other libraries, and no third-party crypto crates. Everything is implemented here, in Rust.
- Constant time by default. Secret-dependent values flow through the
ctlayer (branchless equality, selection, ordering) so higher layers avoid timing side channels. Where an algorithm is intrinsically non-constant-time (RSA keygen, modular inverse), it's used only on one-time/key-generation paths and documented as such. no_stdcore. The crate is#![no_std];allocandstdare opt-in features (stdis the default and impliesalloc).- Validated. Where a standard publishes test vectors we run them — RFC 8439, RFC 8032, RFC 8448, FIPS 203/204/205 ACVP — and cross-check the X.509 / TLS / PQC stacks against OpenSSL 3.5.
Layout
Single crate, modules gated by Cargo features:
| Layer | Module | Status |
|---|---|---|
| Constant-time | ct |
✅ implemented |
| Hashing | hash |
✅ SHA-2, SHA-3 + Keccak-256, SHAKE/cSHAKE/KMAC/TupleHash/ParallelHash, TurboSHAKE/KangarooTwelve, BLAKE2b/2s (+keyed/X), BLAKE3, SM3, MD4/MD5/SHA-1/RIPEMD-160; HMAC + Mac trait (constant-time verify, drop-zeroizing) |
| Randomness | rng |
✅ RngCore/CryptoRng, HMAC-DRBG (NIST SP 800-90A), OsRng (Unix + Windows) |
| Symmetric cipher | cipher |
✅ AES-128/192/256 (constant-time, table-free); CBC/CFB/OFB/CTR; GCM, CCM and ChaCha20-Poly1305 (AEAD); XTS (disk encryption); AES-KW + AES-KWP (RFC 3394 / 5649) |
| Bignum (CT) | bignum |
✅ Uint<LIMBS> and runtime-sized BoxedUint, widening mul, Montgomery modular arith, modexp, Fermat & extended-Euclid inverse |
| Asymmetric keys | rsa |
✅ RSA keygen (compile-time + runtime, 512–65536 bits), raw, PKCS#1 v1.5 enc/sign, OAEP enc, PSS sign/verify, PKCS#1 DER/PEM |
| Key derivation | kdf |
✅ PBKDF2, HKDF, scrypt (RFC 7914), Argon2id/2d/2i (RFC 9106) |
| Elliptic curve | ec |
✅ ECDSA/ECDH on P-256/P-384/P-521/secp256k1 (runtime multi-curve) + fast const-generic P-256, X25519, Ed25519 (EdDSA, RFC 8032) |
| Post-quantum KEM | mlkem |
✅ ML-KEM-512 / 768 / 1024 (FIPS 203), no_std/no-alloc; OpenSSL-interop on -768 |
| Post-quantum sig | mldsa |
✅ ML-DSA-44/65/87 (FIPS 204); hedged + deterministic; FIPS 204 ACVP + OpenSSL-interop |
| Post-quantum sig | slhdsa |
✅ SLH-DSA, all 12 sets (FIPS 205, SHA-2/SHAKE × 128/192/256 × s/f); FIPS 205 ACVP + OpenSSL-interop |
| ASN.1 / DER | der |
✅ DER reader/writer, base64, PEM |
| X.509 | x509 |
✅ self-signed + CA issuance (RSA, ECDSA & Ed25519), PKCS#10 CSRs, parse, verify; PKIX SPKI; OpenSSL-interop |
| TLS | tls |
✅ TLS 1.2 and 1.3, DTLS 1.2 and 1.3 client + server (sans-I/O core + blocking Stream); x25519/secp256r1 + X25519MLKEM768 hybrid (1.3); AES-GCM & ChaCha20-Poly1305; Ed25519/ECDSA/RSA auth; ALPN, record_size_limit (RFC 8449), TLS-Exporter (RFC 5705); PSK session resumption + 0-RTT (early_data) with an anti-replay window (1.3); RFC 5077 session tickets (1.2); mTLS / client certificate authentication; HelloRetryRequest; bidirectional KeyUpdate; RFC 8448 KATs; DTLS HelloVerifyRequest / cookie DoS guard, handshake fragmentation + reassembly, 64-bit sliding-window anti-replay; DTLS 1.3 encrypted sequence numbers + ACK-driven retransmission. |
| C ABI | ffi |
✅ hashing/HMAC, RNG, RSA, ECDSA & Ed25519 keys/signatures, X.509; opaque handles + caller buffers; include/purecrypto.h |
| CLI | (binary) | ✅ hash, rand, genpkey (classical + PQ), pkey, req, x509 (CA), s_client, s_server, s_dtls_client, s_dtls_server |
Cargo features
Default is std + cli with every module on. Disable defaults for a no_std
build and re-enable only what you need:
# Bare no_std, no allocator: just `ct` and primitives that fit.
= { = "0.0.3", = false }
# no_std core + ML-KEM-768 (no alloc):
= { = "0.0.3", = false, = ["mlkem"] }
# Library with PQ signing only:
= { = "0.0.3", = false, = ["mldsa", "slhdsa"] }
Module gates: hash, cipher, kdf, bignum, rng, rsa, der, ec,
x509, tls, mlkem, mldsa, slhdsa, ffi, cli. Each pulls in only its
own dependencies. alloc is required by anything that needs heap (most things
except ct, hash, cipher, and the no-alloc mlkem core).
Building
Requires Rust 1.95+ (edition 2024).
Command-line tool
The purecrypto binary (built by default; or cargo build --features cli).
Every subcommand reads stdin when no -in is given and writes to stdout
when no -out is given, so commands compose with pipes.
hash — message digests
|
Algorithms: sha224, sha256, sha384, sha512, sha512-224, sha512-256,
sha3-224, sha3-256, sha3-384, sha3-512, keccak256, blake2b256,
blake2b384, blake2b512, blake2s256, blake3, sm3, sha1, md5,
ripemd160. (The XOFs shake128/shake256 and the BLAKE2X/cSHAKE/KMAC
variants are exposed through the Rust library, not the CLI.)
rand — randomness
genpkey — key generation (classical and post-quantum)
# Classical
# Post-quantum signatures (FIPS 204 / FIPS 205)
# Post-quantum KEM (FIPS 203) — all three security levels
The full SLH-DSA matrix is supported:
SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f} (12 parameter sets).
Output format:
- RSA →
-----BEGIN RSA PRIVATE KEY-----(PKCS#1) - EC →
-----BEGIN EC PRIVATE KEY-----(SEC1) - Ed25519 / ML-DSA / ML-KEM / SLH-DSA →
-----BEGIN PRIVATE KEY-----(PKCS#8, algorithm identified by the embedded OID)
PKCS#8 interop note. purecrypto uses the simple PKCS#8 form —
OCTET STRINGcontaining the raw expanded key bytes — for every PQ scheme. This matches OpenSSL 3.5 byte-for-byte for SLH-DSA, and OpenSSL parses the resulting private keys directly. For ML-DSA and ML-KEM, OpenSSL writes a richerSEQUENCE { seed, expanded }form; purecrypto's PEM round-trips through itself but may not load into OpenSSL as a private key. Public-key SPKI is fully interoperable for every scheme.
pkey — inspect or convert a key
pkey auto-detects every supported flavor (RSA PKCS#1, EC SEC1, and the PKCS#8
types above) and routes by the embedded OID for PKCS#8 inputs.
req — PKCS#10 certificate signing requests
x509 — self-signed certificates and a small CA
# Build a self-signed CA cert
# Issue a leaf certificate from a CSR
# Inspect a certificate
s_client — TLS 1.3 test client
# Negotiate HTTP/2 (or fall back to http/1.1) via ALPN
# Dump the negotiated secrets in NSS SSLKEYLOGFILE format — Wireshark can
# then decrypt the captured pcap.
# Present a client certificate (mTLS). The key may be Ed25519 (PKCS#8) or
# ECDSA (SEC1).
The client offers X25519MLKEM768 (post-quantum hybrid) first, then x25519
and secp256r1; all three TLS 1.3 cipher suites
(TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384,
TLS_CHACHA20_POLY1305_SHA256); and Ed25519, ECDSA, and RSA peer signatures.
s_server — TLS 1.3 echo / -www server
A one-shot test server: it binds, accepts one connection, performs the handshake, exchanges data, and exits.
# Plain TLS echo:
# Serve a fixed HTTP response (text/plain) for one request:
# Negotiate ALPN, listen on 8443:
# mTLS: require + verify a client cert against the bundle in `client-ca.pem`.
TLS 1.2
s_client / s_server default to TLS 1.3. Pass -tls1_2 on either side
to force TLS 1.2. The TLS 1.2 path is ECDHE-AEAD only (AES-GCM and
ChaCha20-Poly1305) and supports mTLS plus RFC 5077 session tickets.
# Server (TLS 1.2)
# Client (TLS 1.2)
DTLS — s_dtls_client / s_dtls_server
DTLS runs the TLS handshake over UDP. Either use the dedicated
s_dtls_client / s_dtls_server binaries, or pass -dtls1_2 / -dtls1_3
to s_client / s_server. The two forms are equivalent.
# DTLS 1.2 echo
# DTLS 1.3 echo
# Equivalent via s_client / s_server with version flags
The DTLS server stands up a HelloVerifyRequest cookie exchange (1.2) or
HelloRetryRequest cookie (1.3) before allocating any per-connection
state, and both directions install a 64-bit sliding-window replay
filter once the handshake-protected keys are in place. The default
record size is 1200 bytes to stay below common path MTUs; override with
-mtu.
Cookbook
End-to-end CA + leaf with EC keys:
A post-quantum signature key and its public counterpart:
A two-process mTLS handshake on a single host (client cert presented to the server, both keys Ed25519):
# CA + server cert + client cert
# In one terminal — server requires + verifies client certs against ca.crt:
# In another terminal — client presents its cert + key:
Library usage
Idiomatic Rust API — see docs.rs/purecrypto for the full reference. A few common patterns:
use ;
let d = digest;
use Ed25519PrivateKey;
use OsRng;
let sk = generate;
let sig = sk.sign;
sk.public_key.verify.unwrap;
use MlDsa65PrivateKey;
let = generate;
let sig = sk.sign.unwrap;
assert!;
use MlKem768DecapsKey;
let = generate;
let = ek.encapsulate;
let ss_b = dk.decapsulate;
assert_eq!;
Versions and transports
purecrypto ships both TLS (TCP) and DTLS (UDP) at two protocol
versions each:
| Version | Transport | Client | Server |
|---|---|---|---|
| TLS 1.2 | TCP | tls::ClientConnection12 |
tls::ServerConnection12 |
| TLS 1.3 | TCP | tls::ClientConnection |
tls::ServerConnection |
| DTLS 1.2 | UDP | dtls::DtlsClientConnection12 |
dtls::DtlsServerConnection12 |
| DTLS 1.3 | UDP | dtls::DtlsClientConnection13 |
dtls::DtlsServerConnection13 |
- TLS 1.2 is ECDHE-AEAD only (AES-128/256-GCM, ChaCha20-Poly1305) — no static RSA, no static DH, no CBC. Forward secrecy by construction. Includes mTLS and RFC 5077 stateless session tickets.
- TLS 1.3 is the full RFC 8446 with PSK resumption, 0-RTT, exporter, ALPN, mTLS, and downgrade-detection.
- DTLS 1.2 (RFC 6347) carries the TLS 1.2 handshake over UDP with
HelloVerifyRequest cookies, handshake fragmentation/reassembly,
replay protection, and retransmission. Currently restricted to the
ECDHE-ECDSA-AES128-GCM-SHA256suite + X25519 + ECDSA server certs. - DTLS 1.3 (RFC 9147) carries the TLS 1.3 handshake over UDP with
selective ACK reliability, encrypted sequence numbers, and a
HelloRetryRequest cookie. Currently restricted to
AES-128-GCM-SHA256+ X25519 + ECDSA server certs. - Both DTLS variants accept additional cipher suites / groups / signature schemes as follow-up commits — the underlying primitives and the chassis are already present.
TLS 1.3
The tls module is a sans-I/O TLS 1.3 implementation with a thin
std::io::Read + Write adapter for blocking TCP. The full feature surface,
configured per side:
ClientConfig::new(roots)
.with_alpn(vec![b"h2".to_vec(), b"http/1.1".to_vec()])
.with_record_size_limit(4096) // RFC 8449
.with_session(stored) // PSK resumption (+ 0-RTT if allowed)
.with_client_cert(client_cert_cfg) // mTLS (Ed25519 / ECDSA)
ServerConfig::with_rsa(chain, key) | with_ecdsa(...) | with_ed25519(...)
.with_alpn(...)
.with_record_size_limit(...)
.with_ticket_key([u8; 32]) // enables NewSessionTicket emission
.with_ticket_lifetime(secs)
.with_max_early_data(N) // accept up to N bytes of 0-RTT
.with_replay_window(window) // 0-RTT anti-replay (shared across configs)
.with_client_auth(roots, required) // mTLS
After a handshake completes, both sides expose:
connection.alpn_protocol()— the negotiated ALPN name, if any.connection.tls_exporter(label, context, out)— RFC 8446 §7.5 / RFC 5705 application-layer keying material.connection.peer_certificates()— the validated chain (leaf first).- (client)
connection.take_session()— moves out aStoredSessionderived from the server's NewSessionTicket; pass it toClientConfig::with_sessionnext time you connect to the same server. - (client)
connection.write_early_data(&[u8])— sends application data under the early-traffic key beforeServerHelloarrives, valid only on a resumed connection whose session enabled 0-RTT.
0-RTT replay caveat. RFC 8446 §8: 0-RTT data is replayable by an active
attacker, since the server cannot bind the early bytes to a unique
client-server handshake instance. The provided ReplayWindow blocks repeated
binders within a process, but cross-process / cross-server replay defenses
are application-level. Mark any data sent via write_early_data as
idempotent (a HEAD/GET, an idempotent RPC, …) and never as a state-changing
write.
use OsRng;
use ;
let roots = new; // populate from a PEM bundle …
let mut conn = new;
let mut sock = connect.unwrap;
let mut tls = new;
tls.complete_handshake.unwrap;
println!;
The keylogfile output from purecrypto s_client -keylogfile <path> follows
the standard NSS SSLKEYLOGFILE format and decrypts captured traffic in
Wireshark by setting Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log filename.
Signature algorithms
X.509 chain validation and TLS 1.3 CertificateVerify both dispatch through
the signature_registry module. Every signature
primitive purecrypto can do appears as a registry entry; a strict whitelist
[SignaturePolicy] controls which ones a verifier will accept.
Registry
id (whitelist key) |
X.509 OID | TLS 1.3 scheme | Default modern() |
|---|---|---|---|
rsa-pkcs1-sha1 |
1.2.840.113549.1.1.5 |
(none) | opt-in |
rsa-pkcs1-sha256 |
1.2.840.113549.1.1.11 |
0x0401 |
✅ |
rsa-pkcs1-sha384 |
1.2.840.113549.1.1.12 |
0x0501 |
✅ |
rsa-pkcs1-sha512 |
1.2.840.113549.1.1.13 |
(none) | opt-in |
rsa-pss-rsae-sha256 |
1.2.840.113549.1.1.11 (RSAE) |
0x0804 |
✅ |
rsa-pss-rsae-sha384 |
1.2.840.113549.1.1.12 (RSAE) |
0x0805 |
✅ |
rsa-pss-rsae-sha512 |
1.2.840.113549.1.1.13 (RSAE) |
0x0806 |
✅ |
rsa-pss-pss-sha256 |
1.2.840.113549.1.1.10 (PSS-keys) |
(none) | opt-in |
ecdsa-with-sha256 |
1.2.840.10045.4.3.2 (any curve) |
(none) | ✅ |
ecdsa-with-sha384 |
1.2.840.10045.4.3.3 (any curve) |
(none) | ✅ |
ecdsa-with-sha512 |
1.2.840.10045.4.3.4 (any curve) |
(none) | ✅ |
ecdsa-secp256r1-sha256 |
(TLS-only — strict curve) | 0x0403 |
✅ |
ecdsa-secp384r1-sha384 |
(TLS-only — strict curve) | 0x0503 |
✅ |
ecdsa-secp521r1-sha512 |
(TLS-only — strict curve) | 0x0603 |
✅ |
ecdsa-secp256r1-sha384/512, ecdsa-secp384r1-sha256/512, ecdsa-secp521r1-sha256/384 |
cross-hash, policy-only | (none) | opt-in |
ecdsa-secp256k1-sha256/384/512 |
secp256k1, policy-only | (none) | opt-in |
ed25519 |
1.3.101.112 |
0x0807 |
✅ |
ml-dsa-44 / -65 / -87 |
2.16.840.1.101.3.4.3.17/18/19 |
0x0904/05/06 |
✅ (NIST FIPS 204) |
slh-dsa-sha2-128s/128f/192s/192f/256s/256f, slh-dsa-shake-128s/128f/192s/192f/256s/256f |
2.16.840.1.101.3.4.3.20..31 |
(none) | opt-in (FIPS 205) |
The matched-curve / matched-hash ECDSA pairs (e.g. P-256 + SHA-256) have IANA
TLS scheme codes; cross-hash pairs and all secp256k1 entries are reachable for
chain dispatch via the OID-keyed ecdsa-with-shaN entries — which accept any
supported curve — and as fine-grained policy-keyed entries for TLS opt-in.
ML-DSA is on the default whitelist (the modern PQC future). SLH-DSA's twelve parameter sets are registered but never on the default whitelist: signatures are 7–50 KB and rarely the right default for X.509 leaves.
Configuring the policy
use SignaturePolicy;
use ;
let roots = new;
// Default — modern IANA-blessed set, RSA ≥ 2048 bits.
let cfg = new;
// Legacy interop: accept SHA-1 RSA and lower the RSA-bit floor to 1024.
let roots = new;
let cfg = new.with_signature_policy;
// PQC-strict: only ML-DSA + Ed25519, refuse everything classical.
let roots = new;
let cfg = new.with_signature_policy;
// SLH-DSA chains: opt in to a single set the application expects.
let roots = new;
let cfg = new.with_signature_policy;
The same with_signature_policy builder exists on ServerConfig (it gates
client-certificate validation under mTLS). The policy is a strict whitelist:
adding an entry to the registry does NOT auto-permit it — the caller has to
add the id explicitly.
C library
Prebuilt archives — the purecrypto CLI, the static (.a/.lib) and shared
(.so/.dylib/.dll) C libraries, and the header — are attached to each
GitHub release for Linux,
macOS, and Windows.
The same code is callable from C via the ffi feature. Because the crate stays
rlib by default (so the no_std build is unaffected), produce the C library
with cargo rustc:
# Static link (self-contained):
The API is declared in include/purecrypto.h: one-shot
and streaming hashing, HMAC, OS randomness, RSA/ECDSA/Ed25519 key generation,
signing, verification and PEM I/O, and X.509 parsing/verification. Functions
return a pc_status code; variable-length output uses an in/out length buffer;
stateful objects are opaque handles freed by the library; panics never cross
the boundary. The C ABI currently covers the classical asymmetric stack;
post-quantum keys are reached via the Rust library or the CLI.
License
Licensed under the MIT License.