A cryptography toolkit written entirely in Rust, with no foreign code. It is built from the ground up, from constant-time primitives through hashing, ciphers, bignum arithmetic, the classical and post-quantum asymmetric stacks, ASN.1, X.509, TLS, DTLS and QUIC, and it is usable three ways:
- as a Rust library (
no_stdcore, every layer feature-gated), - as a C library (
cdylib/staticlibwith a C ABI, also compiled to WebAssembly), and - as a command-line tool (
purecrypto: hashing, key generation including post-quantum, CSRs, a small CA, TLS / DTLS / QUIC test clients and servers).
It has OpenSSL-like breadth, but unlike a monolithic binary dependency an application compiles in only the parts it needs.
Rust:
use purecrypto::hash::{Digest, Sha256};
use purecrypto::ec::Ed25519PrivateKey;
use purecrypto::mlkem::MlKem768DecapsKey;
use purecrypto::rng::OsRng;
let digest = Sha256::digest(b"abc");
let sk = Ed25519PrivateKey::generate(&mut OsRng);
let sig = sk.sign(b"hello");
sk.public_key().verify(b"hello", &sig).unwrap();
let (dk, ek) = MlKem768DecapsKey::generate(&mut OsRng);
let (ct, ss_a) = ek.encapsulate(&mut OsRng);
assert_eq!(dk.decapsulate(&ct), ss_a);A TLS client that verifies against the embedded root store:
use purecrypto::rng::OsRng;
use purecrypto::tls::{Config, Connection, RootCertStore};
use std::sync::Arc;
let cfg = Config::builder()
.tls_only()
.rng(Arc::new(OsRng)) // no default: the entropy source is explicit
.roots(RootCertStore::with_embedded_roots())
.server_name("example.com")
.alpn(vec![b"h2".to_vec(), b"http/1.1".to_vec()])
.build();
let mut conn = Connection::client(&cfg).unwrap();
// Sans-I/O: pop wire bytes from `conn` and send them, feed received bytes
// back. `tls::Stream` wraps this for blocking TCP; `tokio` / `mio` adapters
// are behind features of the same name.Command line:
cargo install purecrypto
purecrypto hash sha256 file.txt
purecrypto genpkey -algorithm ML-DSA-65 -out mldsa.pem
purecrypto s_client -connect example.com:443 -alpn h2C:
cargo rustc --lib --release --features ffi --crate-type staticlib
cc app.c -I include target/release/libpurecrypto.a -lpthread -ldl -lm -o app- Command-line reference: every subcommand, with a cookbook (CA setup, mTLS, PQC keys, password-based encryption).
- Signature registry and policy: which signature algorithms X.509 and TLS verifiers accept, and how to change it.
- Recommended usage: the opinionated safe path, blessed defaults versus compatibility-only versus hazmat.
- Validation and assurance matrix: per module, test vectors, interop targets, fuzzing, negative-input coverage, constant-time posture, known limitations.
- Threat model: what is and is not defended against.
- Benchmarks: per-algorithm numbers and the constant-time trade-offs behind them.
- Security policy: how to report a vulnerability, and the audit status.
- Demo site: the real
library running in the browser through the C ABI compiled to WebAssembly
(source in
web/). - API reference on docs.rs.
- No foreign code. No C, no assembly borrowed from other libraries, no
third-party crypto crates. Everything is implemented here, in Rust. The
only dependencies are two sibling pure-Rust
no_stdcrates under the same maintainership,compcol(the zlib codec for RFC 8879 certificate compression) andcacrt(the embedded root bundle), plus the optionaltokio/mioI/O adapters. - Constant time by default. Secret-dependent values flow through the
ctlayer (branchless equality, selection, ordering). Where an algorithm is intrinsically variable-time (RSA key generation, the extended-Euclid inverse), it is used only on one-time or key-generation paths and documented as such. no_stdcore. The crate is#![no_std];allocandstdare opt-in features (stdis the default and impliesalloc). Most primitives, and the fixed-curve half ofec, need no allocator at all.- Validated. Where a standard publishes test vectors they run in CI (RFC 8439, RFC 8032, RFC 8448, FIPS 203/204/205 ACVP, BIP340, and more), the entire Wycheproof corpus (all 343 files, 145 000+ cases) runs through the public API, and the X.509, TLS and PQC stacks are cross-checked against OpenSSL 3.5. See docs/validation.md.
Single crate, one Cargo feature per module. Details, test-vector sources and known limitations for each row live in docs/validation.md.
| Feature | What it provides |
|---|---|
ct (always on) |
Branchless equality, selection, ordering, and Choice |
zeroize (always on) |
Zeroize / ZeroizeOnDrop traits and the Zeroizing<T> guard: volatile-store secret wiping, a drop-in for the zeroize crate |
hash |
SHA-2, SHA-3 / Keccak, SHAKE, cSHAKE, KMAC, TupleHash, ParallelHash, TurboSHAKE, KangarooTwelve, BLAKE2b/2s/2X, BLAKE3, SM3, Whirlpool, Streebog, MD2/4/5, SHA-1, RIPEMD-160; HMAC and the Mac trait |
cipher |
AES (constant-time, table-free), SM4, Camellia, ARIA; CBC/CFB/OFB/CTR; AES-GCM, CCM, EAX, ChaCha20-Poly1305, XChaCha20-Poly1305, AES-GCM-SIV, AES-SIV, AEGIS-128L/256, AES-CBC-HMAC-SHA2; XTS; AES-KW/KWP; DES/3DES for legacy interop |
legacy-ciphers |
SEED (table-free), MORUS-640/1280, AEGIS-128 and the v1.2 Ascon-128/128a/80pq AEADs (with ascon): interop and test-vector coverage only (opt-in) |
mac |
AES-CMAC, GMAC, UMAC-64/128, SipHash-c-d / SipHashX |
vmac |
VMAC-64/128 (draft-krovetz-vmac-01) (opt-in) |
kdf |
HKDF, PBKDF2, scrypt, Argon2id/2d/2i, SP 800-108 KBKDF, PBES2 |
rng |
RngCore/CryptoRng, HMAC-DRBG, OsRng (Unix, Linux getrandom(2), Windows, Apple, WASI, browser wasm) |
bignum |
Const-generic Uint and runtime BoxedUint, Montgomery arithmetic, constant-time modexp |
rsa |
Key generation (512 to 65536 bits), PKCS#1 v1.5, OAEP, PSS, blinded CRT with fault check, PKCS#1 DER/PEM |
dh |
Finite-field DH over RFC 3526 groups 14 to 18 plus RFC 4419 group exchange |
dsa |
FIPS 186-4 DSA (2048/224, 2048/256, 3072/256) with RFC 6979 nonces, for legacy interop (opt-in) |
bls |
BLS12-381 pairing, RFC 9380 hash-to-curve, BLS signatures (Basic, message augmentation, proof of possession, aggregation) (opt-in) |
fpe |
NIST SP 800-38G FF1 format-preserving encryption over AES, any radix up to 65536 (opt-in) |
chunked |
C2SP chunked encryption (Cobblestone-128/256): streaming, seekable, key-committing AES-GCM (opt-in) |
jose |
JWK / JWK Sets, JWS and JWE (RFC 7515-7518, 8037) with a strict self-contained JSON parser |
ec |
ECDSA/ECDH on P-224, P-256, P-384, P-521, secp256k1, Brainpool (224–512); X25519, X448, Ed25519, Ed448; SM2 signature and encryption (P-256 and secp256k1 ECDSA — with Bitcoin/Ethereum key recovery — need no alloc); with legacy-ec: P-192, secp160/192/224 (k1/r1/r2) and ECDH on the binary curves sect283/409/571 (k1/r1) |
9e7440c (build(ec): legacy-ec feature for the sub-112-bit and binary curves) |
legacy-ec| Curves below the 112-bit level or deprecated by SP 800-186: secp160k1/r1/r2, secp192k1, P-192, secp224k1 and the binary curves sect283/409/571 k1/r1 (ECDH only); interop with old certificates only (opt-in) | |bip340| BIP340 Schnorr signatures over secp256k1 | |zkp-*| Experimental secp256k1 extensions mirroringsecp256k1-zkp: sign-to-contract, ECDSA adaptor signatures, Pedersen commitments, Borromean range proofs, asset surjection proofs, half-aggregation, ring-signature whitelisting (zkpenables all; no semver guarantee) | |ristretto255| The RFC 9496 prime-order group (stable API) | |hazmat-*| Low-level secp256k1, edwards25519 and ML-DSA arithmetic for threshold / FROST work (no semver guarantee) | |mlkem| ML-KEM-512/768/1024 (FIPS 203), no allocator needed | |mldsa| ML-DSA-44/65/87 (FIPS 204), hedged and deterministic | |slhdsa| SLH-DSA, all 12 parameter sets (FIPS 205) | |falcon| Falcon-512/1024 (FN-DSA, FIPS 206 draft) with a constant-time emulated-float sampler | |lms,xmss| LMS/HSS and XMSS/XMSS^MT stateful hash-based signatures (SP 800-208) | |ascon| Ascon-AEAD128, Ascon-Hash256, XOF128, CXOF128 (SP 800-232) | |aez| AEZ v5 robust authenticated encryption | |hpke| RFC 9180: 4 KEMs, 3 KDFs, 3 AEADs, all four modes | |key| AnEVP_PKEY-stylePrivateKey/PublicKeyfacade over every asymmetric key, with generic PKCS#8/SPKI decoding | |der| DER reader/writer, base64, PEM | |x509| Certificates, CSRs, CRLs, OCSP, SCTs, chain building with name constraints and policy processing, CA issuance | |pkcs12| PKCS#12 / PFX archives, both directions | |tls| TLS 1.2 and 1.3, client and server, sans-I/O; mTLS, ALPN, resumption, 0-RTT, KeyUpdate, exporters, raw public keys, X25519MLKEM768 | |dtls| DTLS 1.2 and 1.3 (RFC 6347 / RFC 9147): cookies, fragmentation, replay windows, ACK-driven retransmission, KeyUpdate | |quic| QUIC v1 (RFC 9000/9001/9002) plus DATAGRAM (RFC 9221), sans-I/O | |ech| Encrypted Client Hello (draft-ietf-tls-esni-22), client and server | |cert-compression| RFC 8879 certificate compression | |embedded-roots| A curated root-certificate bundle (RootCertStore::with_embedded_roots()) | |tls-legacy| SSL 3.0 / TLS 1.0 / TLS 1.1 with CBC suites. Deprecated and insecure, off by default, for talking to legacy devices only | |tokio,mio| Async and non-blocking I/O adapters fortls| |ffi| The C ABI (include/purecrypto.h) | |cli| Thepurecryptobinary |
The default feature set is std plus most modules and the CLI. Opt-in
features are quic, hpke, ech, falcon, ristretto255, bip340, the
zkp-* set, the hazmat-* set, dsa, bls, fpe, chunked,
legacy-ec, legacy-ciphers, vmac, tls-legacy, wasi-getrandom,
ffi, tokio and mio. Disable the defaults for a no_std build and re-enable
only what you need:
# Bare no_std, no allocator: `ct` plus whatever primitives you turn on.
purecrypto = { version = "0.8", default-features = false, features = ["hash", "cipher"] }
# no_std ML-KEM-768, no allocator:
purecrypto = { version = "0.8", default-features = false, features = ["mlkem"] }
# no_std elliptic curves without an allocator: P-256 ECDSA/ECDH, X25519,
# X448, Ed25519, Ed448. Add `alloc` for the runtime multi-curve path
# (P-384/P-521/secp256k1/Brainpool), SM2, and the DER/PEM codecs.
purecrypto = { version = "0.8", default-features = false, features = ["ec"] }
# Post-quantum signing only:
purecrypto = { version = "0.8", default-features = false, features = ["mldsa", "slhdsa"] }
# TLS engine for a no_std target with an allocator:
purecrypto = { version = "0.8", default-features = false, features = ["tls", "dtls"] }Each feature pulls in only what it needs. alloc is required by anything
that must size buffers at runtime (DH, X.509, TLS, the boxed RSA/EC paths);
ct, hash, cipher, kdf, mlkem, mldsa, slhdsa, lms, xmss,
aez, rsa and the fixed-curve half of ec build without it.
cargo build # default: std + CLI binary
cargo build --no-default-features # bare no_std
cargo build --no-default-features --features alloc # no_std + alloc
cargo test # full suite
cargo test --release -- --ignored # heavy KATs (SLH-DSA 's' sets, RSA keygen)Requires Rust 1.89 or newer (edition 2024); the MSRV is declared in
Cargo.toml and enforced in CI, which also builds bare-metal
(thumbv7em-none-eabi), 32-bit ARM, RISC-V, wasm32 and WASI targets.
For WebAssembly, build the ffi feature as a cdylib for
wasm32-unknown-unknown; the module imports one host function,
purecrypto.random_get, for entropy. web/ is a complete example.
One binary, OpenSSL-style subcommands. Every subcommand reads stdin when
no -in is given and writes to stdout when no -out is given. Private
material is written mode 0600 and never overwrites an existing file. The
full reference with every flag is in docs/cli.md.
| Subcommand | Purpose | Example |
|---|---|---|
hash / dgst |
Message digests | purecrypto hash sha3-256 file |
mac |
HMAC, AES-CMAC, GMAC | purecrypto mac -alg hmac-sha256 -keyfile k -in msg |
kdf |
HKDF, PBKDF2, scrypt, Argon2, KBKDF | purecrypto kdf argon2 -variant 2id -password-file - -salt HEX -t-cost 3 -m-cost 65536 -len 32 |
enc |
AEAD encrypt/decrypt, AES key wrap | purecrypto enc -alg AES-256-GCM -keyfile k -nonce HEX -in plain -out ct |
rand |
OS randomness | purecrypto rand 32 |
genpkey |
Keys: RSA, EC, SM2, Ed25519/448, ML-DSA, ML-KEM, SLH-DSA, LMS/HSS, XMSS | purecrypto genpkey -algorithm EC -curve P-256 -out ec.pem |
pkey |
Inspect or convert a key | purecrypto pkey -in key.pem -pubout |
pkeyutl |
Sign, verify, encrypt, decrypt with any key | purecrypto pkeyutl sign -inkey k.pem -in msg -out msg.sig |
kem |
ML-KEM keygen / encaps / decaps | purecrypto kem encaps -peer ek.bin -out-ct ct -out-ss ss |
kex |
X25519, X448, ECDH shared secrets | purecrypto kex -alg X25519 -key my.pem -peer their.pub.pem |
req |
PKCS#10 CSRs | purecrypto req -key leaf.pem -subj /CN=leaf -out leaf.csr |
x509 |
Self-signed certs, issue from CSR, inspect | purecrypto x509 -req -in leaf.csr -CA ca.crt -CAkey ca.pem -san leaf.example -out leaf.crt |
ca |
A directory-backed development CA with revocation and CRLs | purecrypto ca init -dir ./myca -cn "My CA" |
crl |
Parse, verify, query CRLs | purecrypto crl -in x.crl -verify -CAfile ca.crt |
s_client, s_server |
TLS 1.3 / 1.2 test client and server (also DTLS and QUIC via flags) | purecrypto s_client -connect example.com:443 -alpn h2 |
s_dtls_client, s_dtls_server |
DTLS 1.2 / 1.3 | purecrypto s_dtls_server -dtls1_3 -accept 0.0.0.0:5685 -cert c.pem -key k.pem |
q_client, q_server |
QUIC v1 | purecrypto q_client -connect localhost:4434 -alpn h3 |
Behaviours worth knowing: s_client verifies the server certificate by
default (against the embedded roots or -CAfile) and prints a loud warning
under -insecure, and a TCP close without a TLS close_notify is reported
as a possible truncation with a non-zero exit. When issuing from a CSR
(x509 -req, ca sign-csr) the request's subjectAltName is not certified
unless you pass -copy-csr-san, and a leaf with no vetted SAN whose CSR
commonName looks like a DNS name, wildcard, or IP literal is refused: name
the SANs with -san, replace the subject with -subj /CN=..., or override
with -allow-cn-hostname. See docs/cli.md.
The idiomatic Rust API is documented on docs.rs/purecrypto. Runtime algorithm selection is available where it helps:
use purecrypto::hash::HashAlgorithm;
// `HashAlgorithm` names every digest in the crate; `Hasher` holds the state
// inline (no allocation) and implements `hash::DynDigest`.
let alg: HashAlgorithm = "sha256".parse().unwrap();
let mut h = alg.hasher(); // also an io::Write / fmt::Write sink
h.update(b"a");
h.update(b"bc");
assert_eq!(format!("{}", h.finalize()).len(), 64); // hex via Displaypurecrypto::ct mirrors the subtle crate (Choice, CtOption,
ConstantTimeEq, ConstantTimeGreater/Less, ConditionallySelectable,
ConditionallyNegatable) and purecrypto::zeroize mirrors the zeroize crate
(Zeroize, ZeroizeOnDrop, Zeroizing<T>, DefaultIsZeroes), so a project
that already depends on this crate needs neither. Two differences to know:
ct::ConditionallySelectable::conditional_select(a, b, choice)returnsawhenchoiceis true — the opposite ofsubtle. Ported code should callconditional_select_b_if_true, which hassubtle's argument order.- There is no
#[derive(Zeroize)]: implementZeroizeby wiping each field, add a two-lineDropthat callsself.zeroize(), or hold secret fields asZeroizing<T>and let them wipe themselves.
All four handshake versions (TLS 1.2, TLS 1.3, DTLS 1.2, DTLS 1.3) and both
roles share one API: tls::Config plus tls::Connection. The version range
is chosen with versions(min, max) (or the tls_only() / dtls()
shorthands); the role is chosen when the connection is built with
Connection::client(&cfg) or Connection::server(&cfg). QUIC reuses the
same Config for its TLS layer.
// Client (TLS or DTLS, any version):
Config::builder()
.versions(ProtocolVersion::TLSv1_2, ProtocolVersion::TLSv1_3)
.rng(Arc::new(OsRng)) // required; or a TPM/HSM EntropySource
.roots(roots)
.server_name("example.com")
.alpn(vec![b"h2".to_vec(), b"http/1.1".to_vec()])
.record_size_limit(4096) // RFC 8449
.try_identity(client_chain, client_key)? // mTLS; checks the key matches the leaf
.build();
// Server:
Config::builder()
.tls_only()
.rng(Arc::new(OsRng))
.try_identity(chain, SigningKey::Ecdsa(key))? // Rsa, Ecdsa, Ed25519, Ed448, MlDsa*, External
.alpn(...)
.ticket_key([0u8; 32]) // enables NewSessionTicket (rotate; see docs)
.max_early_data(16384) // accept up to N bytes of 0-RTT
.client_auth(ClientAuth { roots, required: true }) // mTLS
.build();
// DTLS server (one Connection per peer address):
Config::builder()
.dtls()
.rng(Arc::new(OsRng))
.try_identity(chain, key)?
.cookie_secret(current) // amplification defence
.previous_cookie_secret(old) // optional: honour cookies across a rotation
.peer_socket_addr(peer) // required whenever cookies are on
.max_record_size(1200) // MTU ceiling
.build();
There is no implicit RNG: Connection::client / Connection::server return
MissingEntropySource unless rng(...) was set, which is what lets a TPM or
HSM supply entropy instead of the OS. try_identity and try_private_key fail at configuration time when the
private key does not belong to the leaf certificate; the older identity /
private_key builders skip that check. A cookie-requiring DTLS server needs
peer_socket_addr (or peer_address), because the cookie binds the
client's address; without it, Connection::server refuses to start rather
than silently becoming a UDP reflection amplifier.
After a handshake completes, both sides expose:
alpn_selected(): the negotiated ALPN name, if any.tls_exporter(label, context, out): RFC 8446 §7.5 / RFC 5705 keying material.peer_certificates(): the validated chain, leaf first (also restored on a resumed mTLS session).received_close_notify(): whether the peer closed cleanly. A transport EOF without it is a truncation.- Client only:
take_session()returns aResumptionSessionderived from the server's NewSessionTicket; pass it toresumption_session(...)next time (TLS 1.3 PSK or TLS 1.2 RFC 5077 ticket).write_early_data(&[u8])sends 0-RTT on such a resumed connection.
0-RTT replay caveat. RFC 8446 §8: an active attacker can replay 0-RTT
data. The built-in ReplayWindow blocks repeated binders within one
process; cross-process defences are the application's job. Only send
idempotent requests through write_early_data.
X.509 and TLS verification dispatch through a registry of signature
algorithms gated by a strict whitelist, SignaturePolicy. The default
(modern()) is the IANA-blessed set plus ML-DSA, with RSA keys of 2048 bits
or more. To accept SHA-1 RSA for a legacy peer, or to go PQC-only:
use purecrypto::signature_registry::SignaturePolicy;
use purecrypto::tls::{Config, RootCertStore};
let legacy = Config::builder()
.roots(RootCertStore::new())
.signature_policy(SignaturePolicy::modern().permit("rsa-pkcs1-sha1").with_min_rsa_bits(1024))
.build();
let pqc_only = Config::builder()
.roots(RootCertStore::new())
.signature_policy(SignaturePolicy::empty().permit("ml-dsa-65").permit("ed25519"))
.build();The full registry table and try_permit (for ids that come from
configuration) are in docs/signature-registry.md.
Prebuilt archives (the CLI, the static and shared C libraries, and the header) are attached to each GitHub release for Linux, macOS and Windows. To build them yourself:
cargo rustc --lib --release --features ffi --crate-type cdylib # target/release/libpurecrypto.so
cargo rustc --lib --release --features ffi --crate-type staticlib # target/release/libpurecrypto.a
cc app.c -I include target/release/libpurecrypto.a -lpthread -ldl -lm -o appThe API is declared in include/purecrypto.h:
hashing, HMAC/CMAC/GMAC, KDFs, randomness, AEADs and key wrap, RSA, ECDSA,
Ed25519/Ed448, X25519/X448, SM2, ML-KEM, ML-DSA, SLH-DSA, LMS/HSS, XMSS,
CSRs, X.509 and CRLs, and sans-I/O TLS, DTLS and QUIC. Every function returns
a PcStatus (PC_OK or a negative code, for example PC_CLOSED once the
peer's close_notify has been processed, or PC_KEY_MISMATCH when a
certificate is configured with the wrong key). Variable-length output uses
an in/out length buffer; stateful objects are opaque handles freed by the
library; panics never cross the boundary.
The TLS surface mirrors OpenSSL's memory BIO: the caller pumps wire bytes
through pc_tls_feed / pc_tls_pop and application bytes through
pc_tls_send / pc_tls_recv. A DTLS server with cookies enabled needs
pc_dtls_cfg_set_peer_addr for each peer. The smoke tests in
tests/ (ffi_smoke.c, ffi_tls_smoke.c, ffi_dtls_smoke.c,
ffi_quic_smoke.c) are complete, runnable examples.
This crate has not had a third-party human audit and is not FIPS
validated. Whole-codebase automated audits are run regularly and their
fixes land on master; see SECURITY.md and
docs/validation.md for what that does and does not
cover, and report vulnerabilities privately through GitHub's security
advisories.
Licensed under the MIT License.