# purecrypto

[![CI](https://github.com/KarpelesLab/purecrypto/actions/workflows/ci.yml/badge.svg)](https://github.com/KarpelesLab/purecrypto/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/purecrypto.svg)](https://crates.io/crates/purecrypto)
[![docs.rs](https://img.shields.io/docsrs/purecrypto)](https://docs.rs/purecrypto)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

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** (`cdylib` with a C ABI), and
- as a **standalone command-line tool** (`purecrypto`: hashing, randomness, key
  generation including PQ, CSRs, a small CA, TLS / DTLS / QUIC test clients
  and servers, …).

It's a **modular** toolkit with OpenSSL-like breadth — but unlike a monolithic
binary dependency, every algorithm and protocol layer is feature-gated (over a
`#![no_std]` core), so an application compiles in only the parts it needs.

> Status: **mostly stable.** Everything below is implemented and validated
> against published test vectors (RFCs, NIST FIPS ACVP, OpenSSL interop), and
> the public API is now mostly stable: breaking changes are rare, deliberate,
> and gated by `cargo-semver-checks` in CI. The `hazmat-*` features are the
> documented exception and carry no stability guarantee. The code has been
> through repeated internal security-review-and-hardening passes (including
> automated whole-codebase audits by Claude Fable 5), but has not yet received
> an independent third-party human audit — weigh that when deciding what to
> protect with it.

## Documentation

- **[Security policy](SECURITY.md)** — how to report a vulnerability; assurance status.
- **[Validation & assurance matrix](docs/validation.md)** — per module: test
  vectors, interop targets, fuzzing, negative-input coverage, constant-time
  posture, known limitations.
- **[Recommended usage](docs/recommended-usage.md)** — the opinionated *safe
  path*: blessed defaults vs. compatibility-only vs. hazmat.
- **[Threat model](docs/threat-model.md)** — what is and isn't defended against.
- **[Benchmarks](docs/benchmarks.md)** — real per-algorithm numbers + the
  constant-time tradeoffs behind them.

## 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
  [`ct`](src/ct) layer (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_std` core.** The crate is `#![no_std]`; `alloc` and `std` are opt-in
  features (`std` is the default and implies `alloc`).
- **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). Ascon-Hash256/XOF128/CXOF128 live in `ascon` |
| Randomness       | `rng`       | ✅ RngCore/CryptoRng, HMAC-DRBG (NIST SP 800-90A), OsRng (Unix + Windows) |
| Symmetric cipher | `cipher`    | ✅ AES-128/192/256 (constant-time, table-free); SM4 (GB/T 32907, constant-time, table-free); CBC/CFB/OFB/CTR; GCM, CCM, ChaCha20-Poly1305, XChaCha20-Poly1305, AEGIS-128L/256, AES-GCM-SIV (RFC 8452) and AES-SIV (RFC 5297, nonce-misuse-resistant) (AEAD); XTS (disk encryption); AES-KW + AES-KWP (RFC 3394 / 5649); DES + 3-DES (EDE3 / EDE2) with `Cbc64` for legacy interop. Ascon-AEAD128 lives in `ascon`, AEZ v5 (robust AE) in `aez` |
| MAC              | `mac`       | ✅ AES-CMAC (RFC 4493); GMAC (NIST SP 800-38D); UMAC-64 / UMAC-128 (RFC 4418); HMAC lives in `hash` |
| 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), SP 800-108 KBKDF (counter + feedback, HMAC/CMAC PRF) |
| 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), X448 (RFC 7748), Ed448 (EdDSA, RFC 8032), SM2 signature + encryption (GB/T 32918 / RFC 8998) |
| Prime-order group | `ristretto255` | ✅ ristretto255 (RFC 9496), a stable prime-order group API; low-level scalar/point arithmetic for threshold/FROST callers also exposed via `hazmat-secp256k1` / `hazmat-edwards25519` / `hazmat-mldsa` (**no semver guarantee** on `hazmat-*`) |
| 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 |
| Stateful HBS     | `lms`       | ✅ LMS / HSS (RFC 8554, NIST SP 800-208); LM-OTS W{1,2,4,8} × LMS H{5,10,15,20,25}; **stateful** advancing key; RFC 8554 KATs |
| Stateful HBS     | `xmss`      | ✅ XMSS / XMSS^MT (RFC 8391, NIST SP 800-208); **stateful** advancing key; RFC 8391 / reference KATs |
| Lightweight      | `ascon`     | ✅ Ascon (NIST SP 800-232): Ascon-AEAD128 + Ascon-Hash256 / XOF128 / CXOF128 from one 320-bit permutation |
| Diffie-Hellman   | `dh`        | ✅ Finite-field DH over RFC 3526 MODP groups (group14..group18) + RFC 4419 group-exchange, for SSH / legacy TLS / IKE interop (new code: ECDH in `ec`) |
| Unified keys     | `key`       | ✅ object-safe `PrivateKey`/`PublicKey` facade — `sign`/`decrypt`/`agree`, `verify`/`encrypt` — over every asymmetric key, with consume-checked params (unsupported params fail loudly) and generic PKCS#8/SPKI decoders (`AnyPrivateKey`/`AnyPublicKey`, which are facade keys themselves); stateful XMSS/LMS via `StatefulSigner`, KEMs via `Encapsulator`/`Decapsulator` |
| ASN.1 / DER      | `der`       | ✅ DER reader/writer, base64, PEM |
| X.509            | `x509`      | ✅ self-signed + CA issuance (RSA, ECDSA, Ed25519 & Ed448), PKCS#10 CSRs, parse, verify; PKIX SPKI; RFC 5280 nameConstraints enforcement across the chain; 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/Ed448/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 (client + server); 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. |
| HPKE             | `hpke`     | ✅ RFC 9180 hybrid public-key encryption — 4 KEMs × 3 KDFs × 3 AEADs + ExportOnly, all four modes (Base/PSK/Auth/AuthPSK) |
| ECH              | `ech`      | ✅ draft-ietf-tls-esni-22 Encrypted Client Hello — client + server, retry_configs, HRR confirmation signal, bit-shape GREASE |
| QUIC             | `quic`     | ✅ QUIC v1 (RFC 9000) + QUIC-TLS (RFC 9001) + recovery / congestion (RFC 9002) + DATAGRAM extension (RFC 9221), sans-I/O |
| Cert compression | `cert-compression` | ✅ RFC 8879 TLS 1.3 certificate compression (zlib via the `compcol` sibling crate) |
| Legacy TLS       | `tls-legacy` | ⚠️ **deprecated/insecure, off by default** — SSL 3.0 / TLS 1.0 / TLS 1.1 (RFC 8996) with CBC MAC-then-encrypt suites (`TLS_RSA_*` static-RSA + `TLS_ECDHE_RSA_*` over AES-CBC-SHA/SHA256 + 3DES), client + server. Last-resort interop only (e.g. VoIP-phone provisioning); requires lowering `Config::min_version`. BEAST 1/n-1 split + constant-time CBC decrypt, but MD5/SHA-1 PRF, Lucky13 residual, and SSLv3 POODLE remain — do not use against modern peers. |
| C ABI            | `ffi`       | ✅ hashing/HMAC + AES-CMAC + GMAC, KBKDF, RNG, AEAD (incl. AEGIS, Ascon) + AES-KW, RSA, ECDSA, Ed25519, Ed448, X25519, X448, SM2, ML-KEM, ML-DSA, SLH-DSA, LMS/XMSS, X.509, TLS / DTLS / QUIC (sans-I/O); opaque handles + caller buffers; `include/purecrypto.h` |
| CLI              | (binary)    | ✅ `hash`/`dgst`, `mac`, `kdf`, `enc`, `rand`, `genpkey` (classical + PQ), `pkey`, `pkeyutl`, `kem`, `kex`, `req`, `x509`, `ca`, `crl`, `s_client`, `s_server`, `s_dtls_client`, `s_dtls_server`, `q_client`, `q_server` |

## CLI + C-API coverage matrix

Each functional area below is callable from the Rust library, the
`purecrypto` CLI, and the C ABI (`include/purecrypto.h`).

| Area                                  | CLI                                  | C API                                                              |
| ------------------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
| Hashing (SHA-2/3, BLAKE2/3, SM3, Ascon, …) | `hash`                          | `pc_digest`, `pc_hash_*`, `pc_ascon_xof`/`pc_ascon_cxof`          |
| HMAC (SHA-1, SHA-2, SHA-3, SM3, …)    | `mac`                                | `pc_hmac`                                                          |
| AES-CMAC (RFC 4493)                   | `mac -alg cmac`                      | `pc_cmac`                                                          |
| GMAC (NIST SP 800-38D)                | `mac -alg gmac -nonce …`             | `pc_gmac`                                                         |
| KDFs (HKDF, PBKDF2, scrypt, Argon2)   | `kdf hkdf\|pbkdf2\|scrypt\|argon2`   | `pc_hkdf`, `pc_pbkdf2`, `pc_scrypt`, `pc_argon2`                   |
| KBKDF (SP 800-108, counter/feedback)  | `kdf kbkdf`                          | `pc_kbkdf_counter`, `pc_kbkdf_feedback`                            |
| AEAD (AES-GCM/CCM, ChaCha20-Poly1305, XChaCha20-Poly1305, AES-GCM-SIV, AES-SIV, AEGIS-128L/256, Ascon-AEAD128) | `enc`                                | `pc_aead_encrypt`, `pc_aead_decrypt`                               |
| AES key wrap (RFC 3394/5649)          | `enc -alg AES-KW\|AES-KWP`           | `pc_aes_kw_wrap/unwrap`, `pc_aes_kwp_wrap/unwrap`                  |
| Randomness                            | `rand`                               | `pc_rand_bytes`                                                    |
| RSA keygen + PKCS#1 sign/verify       | `genpkey`, `req`, `x509`, `pkeyutl`  | `pc_rsa_generate`, `pc_rsa_sign_pkcs1`, `pc_rsa_verify_pkcs1`      |
| RSA-PSS sign/verify                   | `pkeyutl sign/verify -pkeyopt pss`   | `pc_rsa_sign_pss`, `pc_rsa_verify_pss`                             |
| RSA-OAEP encrypt/decrypt              | `pkeyutl encrypt/decrypt -pkeyopt oaep` | `pc_rsa_encrypt_oaep`, `pc_rsa_decrypt_oaep`                    |
| ECDSA keygen + sign/verify            | `genpkey -alg EC`, `pkeyutl`         | `pc_ec_generate`, `pc_ec_sign`, `pc_ec_verify`                     |
| Ed25519 sign/verify                   | `genpkey -alg ED25519`, `pkeyutl`    | `pc_ed25519_*`                                                     |
| Ed448 sign/verify                     | `genpkey -alg ED448`, `pkeyutl`      | `pc_ed448_*`                                                       |
| SM2 sign/verify + encrypt/decrypt     | `genpkey -alg SM2`, `pkeyutl`        | `pc_sm2_*`                                                         |
| ECDH on NIST curves                   | `kex -alg ECDH-P{256,384,521}`       | `pc_ecdh`                                                          |
| X25519                                | `kex -alg X25519`                    | `pc_x25519`, `pc_x25519_public`                                    |
| X448                                  | `kex -alg X448`                      | `pc_x448`, `pc_x448_public`                                        |
| ML-KEM (FIPS 203)                     | `kem keygen\|encaps\|decaps`         | `pc_mlkem_*`                                                       |
| ML-DSA (FIPS 204)                     | `pkeyutl sign/verify` (ML-DSA keys)  | `pc_mldsa_*`                                                       |
| SLH-DSA (FIPS 205)                    | `pkeyutl sign/verify` (SLH-DSA keys) | `pc_slhdsa_*`                                                      |
| LMS / HSS (RFC 8554, stateful)        | `genpkey -alg LMS-…\|HSS-…`, `pkeyutl` | `pc_lms_*`, `pc_hss_*`                                          |
| XMSS / XMSS^MT (RFC 8391, stateful)   | `genpkey -alg XMSS-…\|XMSSMT-…`, `pkeyutl` | `pc_xmss_*`, `pc_xmssmt_*`                                  |
| CSR (PKCS#10)                         | `req`                                | `pc_csr_create_rsa`, `pc_csr_from_pem`, `pc_csr_verify_self_signed`|
| X.509 certificate parse + verify      | `x509`, `ca`                         | `pc_cert_*`, `pc_ec_self_signed_pem`                               |
| CRL parse + verify                    | `crl`                                | `pc_crl_*`                                                         |
| TLS 1.2 / 1.3 client + server         | `s_client`, `s_server`               | `pc_tls_cfg_*`, `pc_tls_*` (memory-BIO style)                      |
| DTLS 1.2 / 1.3 client + server        | `s_dtls_client`, `s_dtls_server`     | `pc_tls_cfg_*` (`PC_DTLS_1_*` selector), `pc_dtls_next_timeout/on_timeout` |
| QUIC v1 client + server               | `q_client`, `q_server`               | `pc_quic_cfg_*`, `pc_quic_*` (sans-I/O, datagram in/out + streams)  |

The C ABI is sans-I/O for TLS/DTLS: the caller pumps wire bytes through
`pc_tls_feed` / `pc_tls_pop` and application bytes through `pc_tls_send` /
`pc_tls_recv` (mirrors OpenSSL's `BIO_s_mem`).

## Cargo features

Default is `std + cli` with most modules on. A few are opt-in: `quic`, `hpke`,
`ech`, `falcon`, `tls-legacy`, `ristretto255`, the `ffi` C ABI, and the
`tokio` / `mio` I/O adapters. Disable defaults for a `no_std` build and
re-enable only what you need:

```toml
# Bare no_std, no allocator: just `ct` and primitives that fit.
purecrypto = { version = "0.6", default-features = false }

# no_std core + ML-KEM-768 (no alloc):
purecrypto = { version = "0.6", default-features = false, features = ["mlkem"] }

# Library with PQ signing only:
purecrypto = { version = "0.6", default-features = false, features = ["mldsa", "slhdsa"] }
```

Module gates: `hash`, `cipher`, `mac`, `kdf`, `bignum`, `rng`,
`linux-getrandom`, `rsa`, `dh`, `der`, `ec`, `key`, `ristretto255`, `x509`,
`pkcs12`, `tls`, `dtls`, `tls-legacy`, `quic`, `mlkem`, `mldsa`, `slhdsa`,
`falcon`, `lms`, `xmss`, `ascon`, `aez`, `hpke`, `ech`, `cert-compression`,
`embedded-roots`, `ffi`, `cli` (plus the `tokio` / `mio` I/O adapters) — plus
the unstable `hazmat-secp256k1` / `hazmat-edwards25519` / `hazmat-mldsa` gates
(no semver guarantee).
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

```sh
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.88+ (edition 2024); the MSRV is declared as `rust-version =
"1.88"` and enforced in CI.

## 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

```sh
purecrypto hash sha256 file.txt              # one-shot digest
echo -n abc | purecrypto hash sha3-256       # any algorithm from the `hash` module
```

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

```sh
purecrypto rand 32              # 32 random bytes as hex
purecrypto rand 16 --binary     # raw bytes to stdout
```

### `genpkey` — key generation (classical and post-quantum)

```sh
# Classical
purecrypto genpkey -algorithm RSA -bits 2048   -out rsa.pem    # also 3072, 4096
purecrypto genpkey -algorithm RSA -bits 8192   -out rsa8k.pem  # any even size, 512..=65536
purecrypto genpkey -algorithm EC  -curve P-256 -out ec.pem     # or P-384, P-521, secp256k1
purecrypto genpkey -algorithm ED25519          -out ed.pem

# Post-quantum signatures (FIPS 204 / FIPS 205)
purecrypto genpkey -algorithm ML-DSA-44               -out mldsa44.pem
purecrypto genpkey -algorithm ML-DSA-65               -out mldsa65.pem
purecrypto genpkey -algorithm ML-DSA-87               -out mldsa87.pem
purecrypto genpkey -algorithm SLH-DSA-SHA2-128f       -out slh128f.pem
purecrypto genpkey -algorithm SLH-DSA-SHAKE-256s      -out slh256s.pem

# Post-quantum KEM (FIPS 203) — all three security levels
purecrypto genpkey -algorithm ML-KEM-512              -out mlkem512.pem
purecrypto genpkey -algorithm ML-KEM-768              -out mlkem768.pem
purecrypto genpkey -algorithm ML-KEM-1024             -out mlkem1024.pem
```

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
> STRING` containing 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
> richer `SEQUENCE { 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

```sh
purecrypto pkey -in key.pem -text     # describe the key
purecrypto pkey -in key.pem -pubout   # emit the SPKI public-key PEM
purecrypto pkey < key.pem             # re-emit the private key (round-trip)
```

`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

```sh
purecrypto req -key leaf.pem -subj "/CN=leaf.example/O=Acme" \
               -addext "subjectAltName=DNS:leaf.example,DNS:www.leaf.example" \
               -out leaf.csr
purecrypto req -in leaf.csr -verify       # check the CSR self-signature
```

### `x509` — self-signed certificates and a small CA

```sh
# Build a self-signed CA cert
purecrypto x509 -new --ca -key ca.pem -subj "/CN=Internal CA" -out ca.crt

# Issue a leaf certificate from a CSR
purecrypto x509 -req -in leaf.csr -CA ca.crt -CAkey ca.pem -out leaf.crt

# Inspect a certificate
purecrypto x509 -in leaf.crt -text
```

### `s_client` — TLS 1.3 test client

```sh
purecrypto s_client -connect example.com:443
purecrypto s_client -connect 127.0.0.1:8443 -CAfile ca.crt -servername leaf.example
purecrypto s_client -connect 127.0.0.1:8443 -insecure -quiet      # skip cert verify, stdin → server

# Negotiate HTTP/2 (or fall back to http/1.1) via ALPN
purecrypto s_client -connect example.com:443 -alpn h2,http/1.1

# Dump the negotiated secrets in NSS SSLKEYLOGFILE format — Wireshark can
# then decrypt the captured pcap.
purecrypto s_client -connect example.com:443 -keylogfile sslkeys.log

# Present a client certificate (mTLS). The key may be Ed25519 (PKCS#8) or
# ECDSA (SEC1).
purecrypto s_client -connect server:443 -cert client.pem -key client.key
```

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, Ed448, 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.

```sh
# Plain TLS echo:
purecrypto s_server -cert server.pem -key server.key -accept 4433

# Serve a fixed HTTP response (text/plain) for one request:
purecrypto s_server -cert server.pem -key server.key -accept 4433 -www

# Negotiate ALPN, listen on 8443:
purecrypto s_server -cert server.pem -key server.key -accept 8443 -alpn h2,http/1.1

# mTLS: require + verify a client cert against the bundle in `client-ca.pem`.
purecrypto s_server -cert server.pem -key server.key -accept 8443 \
                    -Verify 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.

```sh
# Server (TLS 1.2)
purecrypto s_server -tls1_2 -accept 0.0.0.0:4443 -cert cert.pem -key key.pem

# Client (TLS 1.2)
purecrypto s_client -tls1_2 -connect example.com:443 -CAfile roots.pem
```

### 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.

```sh
# DTLS 1.2 echo
purecrypto s_dtls_server -dtls1_2 -accept 0.0.0.0:5684 -cert cert.pem -key key.pem
purecrypto s_dtls_client -dtls1_2 -connect localhost:5684

# DTLS 1.3 echo
purecrypto s_dtls_server -dtls1_3 -accept 0.0.0.0:5685 -cert cert.pem -key key.pem
purecrypto s_dtls_client -dtls1_3 -connect localhost:5685

# Equivalent via s_client / s_server with version flags
purecrypto s_server -dtls1_3 -accept 0.0.0.0:5685 -cert cert.pem -key key.pem
purecrypto s_client -dtls1_3 -connect localhost:5685
```

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`.

### QUIC — `q_client` / `q_server`

QUIC v1 (RFC 9000) over UDP, secured by TLS 1.3 keys (RFC 9001). Either use
the dedicated binaries, or pass `-quic` to `s_client` / `s_server` — the two
forms are equivalent. The client drives one bidirectional stream
(stdin → server, reply → stdout); the unreliable DATAGRAM extension
(RFC 9221) is reachable through the library API.

```sh
purecrypto q_server -accept 0.0.0.0:4434 -cert cert.pem -key key.pem -alpn h3
purecrypto q_client -connect localhost:4434 -alpn h3
```

### Cookbook

End-to-end CA + leaf with EC keys:

```sh
purecrypto genpkey -algorithm EC -curve P-256 -out ca.pem
purecrypto x509 -new --ca -key ca.pem -subj "/CN=My CA" -out ca.crt

purecrypto genpkey -algorithm EC -curve P-256 -out leaf.pem
purecrypto req -key leaf.pem -subj "/CN=leaf.example" \
               -addext "subjectAltName=DNS:leaf.example" -out leaf.csr
purecrypto x509 -req -in leaf.csr -CA ca.crt -CAkey ca.pem -out leaf.crt
```

A post-quantum signature key and its public counterpart:

```sh
purecrypto genpkey -algorithm ML-DSA-65 -out mldsa.pem
purecrypto pkey -in mldsa.pem -text                       # ML-DSA-65 private key
purecrypto pkey -in mldsa.pem -pubout > mldsa.pub.pem     # PKIX SPKI
```

A two-process mTLS handshake on a single host (client cert presented to the
server, both keys Ed25519):

```sh
# CA + server cert + client cert
purecrypto genpkey -algorithm ED25519 -out ca.pem
purecrypto x509 -new --ca -key ca.pem -subj "/CN=Local CA" -out ca.crt
purecrypto genpkey -algorithm ED25519 -out server.pem
purecrypto req -key server.pem -subj "/CN=127.0.0.1" \
               -addext "subjectAltName=DNS:127.0.0.1" -out server.csr
purecrypto x509 -req -in server.csr -CA ca.crt -CAkey ca.pem -out server.crt
purecrypto genpkey -algorithm ED25519 -out client.pem
purecrypto req -key client.pem -subj "/CN=alice" -out client.csr
purecrypto x509 -req -in client.csr -CA ca.crt -CAkey ca.pem -out client.crt

# In one terminal — server requires + verifies client certs against ca.crt:
purecrypto s_server -cert server.crt -key server.pem -accept 8443 -Verify ca.crt -www

# In another terminal — client presents its cert + key:
purecrypto s_client -connect 127.0.0.1:8443 -CAfile ca.crt \
                    -cert client.crt -key client.pem -alpn http/1.1 \
                    -keylogfile keys.log
```

## Library usage

Idiomatic Rust API — see [docs.rs/purecrypto](https://docs.rs/purecrypto) for
the full reference. A few common patterns:

```rust
use purecrypto::hash::{Digest, Sha256};
let d = Sha256::digest(b"abc");

use purecrypto::ec::Ed25519PrivateKey;
use purecrypto::rng::OsRng;
let sk = Ed25519PrivateKey::generate(&mut OsRng);
let sig = sk.sign(b"hello");
sk.public_key().verify(b"hello", &sig).unwrap();

use purecrypto::mldsa::MlDsa65PrivateKey;
let (sk, pk) = MlDsa65PrivateKey::generate(&mut OsRng);
let sig = sk.sign(&mut OsRng, b"hello", b"").unwrap();
assert!(pk.verify(&sig, b"hello", b""));

use purecrypto::mlkem::MlKem768DecapsKey;
let (dk, ek) = MlKem768DecapsKey::generate(&mut OsRng);
let (ct, ss_a) = ek.encapsulate(&mut OsRng);
let ss_b = dk.decapsulate(&ct);
assert_eq!(ss_a, ss_b);
```

### Versions and transports

`purecrypto` ships both TLS (TCP) and DTLS (UDP) at two protocol
versions each:

All four versions (TLS 1.2, TLS 1.3, DTLS 1.2, DTLS 1.3) and both roles
(client, server) share **one** public API: [`tls::Config`] +
[`tls::Connection`]. The version is selected by
`Config::builder().versions(min, max).build()`; the role is selected at
connection-construction time via `Connection::client(&cfg)` or
`Connection::server(&cfg)`.

- **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. Negotiates the same
  ECDHE-AEAD suites × groups × signature schemes the TLS 1.2 path
  supports.
- **DTLS 1.3** (RFC 9147) carries the TLS 1.3 handshake over UDP with
  selective ACK reliability, encrypted sequence numbers, and a
  HelloRetryRequest cookie. Negotiates the same TLS 1.3 suites,
  groups (including `X25519MLKEM768`), and signature schemes as the
  TLS 1.3 path.

### 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:

```text
// Client (TLS or DTLS, any version):
Config::builder()
    .versions(ProtocolVersion::TLSv1_2, ProtocolVersion::TLSv1_3)
    .roots(roots)
    .server_name("example.com")
    .alpn(vec![b"h2".to_vec(), b"http/1.1".to_vec()])
    .record_size_limit(4096)             // RFC 8449
    .identity(client_chain, client_key)  // mTLS (any SigningKey)
    .build();

// Server (TLS or DTLS, any version):
Config::builder()
    .tls_only()                          // shorthand for versions(TLSv1_2, TLSv1_3)
    .identity(chain, SigningKey::Rsa(rsa) | SigningKey::Ecdsa(ec) | ...)
    .alpn(...)
    .ticket_key([0u8; 32])               // enables NewSessionTicket emission
    .max_early_data(16384)               // accept up to N bytes of 0-RTT
    .client_auth(ClientAuth { roots, required: true }) // mTLS
    .build();

// DTLS variant:
Config::builder()
    .dtls()                              // shorthand for versions(DTLSv1_2, DTLSv1_3)
    .identity(chain, key)
    .cookie_secret([0u8; 32])            // amplification defense
    .max_record_size(1200)               // MTU ceiling
    .build();

let mut conn = Connection::client(&cfg)?;   // or Connection::server(&cfg)
```

After a handshake completes, both sides expose:

- `connection.alpn_selected()` — 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 a `ResumptionSession`
  derived from the server's NewSessionTicket; pass it to
  `Config::builder().resumption_session(...)` next time you connect to the
  same server (TLS 1.3 PSK or TLS 1.2 RFC 5077 ticket).
- (client) `connection.write_early_data(&[u8])` — sends application data
  under the early-traffic key before `ServerHello` arrives, 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.

```rust,no_run
use purecrypto::tls::{Config, Connection, HandshakeStatus, RootCertStore};

// Use the embedded root bundle (feature `embedded-roots`, on by default):
// a curated first-party store built from the Mozilla root program and
// others, following CA/Browser Forum rules. Or start from
// `RootCertStore::new()` and add your own PEMs.
let roots = RootCertStore::with_embedded_roots();
let cfg = Config::builder()
    .tls_only()
    .roots(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();

// Drive the handshake: pop wire bytes from `conn`, send them, recv from
// the peer, feed them back. The sans-I/O surface is the same for TLS and
// DTLS — the only difference is "stream" vs "datagram" framing.
# fn _h(_: Connection) -> std::io::Result<()> { Ok(()) }
```


### Signature algorithms

X.509 chain validation and TLS 1.3 `CertificateVerify` both dispatch through
the [`signature_registry`](src/signature_registry.rs) 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

```rust
use purecrypto::signature_registry::SignaturePolicy;
use purecrypto::tls::{Config, RootCertStore};

let roots = RootCertStore::new();

// Default — modern IANA-blessed set, RSA ≥ 2048 bits.
let cfg = Config::builder().roots(roots).build();

// Legacy interop: accept SHA-1 RSA and lower the RSA-bit floor to 1024.
let roots = RootCertStore::new();
let cfg = Config::builder()
    .roots(roots)
    .signature_policy(
        SignaturePolicy::modern()
            .permit("rsa-pkcs1-sha1")
            .with_min_rsa_bits(1024),
    )
    .build();

// PQC-strict: only ML-DSA + Ed25519, refuse everything classical.
let roots = RootCertStore::new();
let cfg = Config::builder()
    .roots(roots)
    .signature_policy(
        SignaturePolicy::empty()
            .permit("ml-dsa-65")
            .permit("ml-dsa-87")
            .permit("ed25519"),
    )
    .build();

// SLH-DSA chains: opt in to a single set the application expects.
let roots = RootCertStore::new();
let cfg = Config::builder()
    .roots(roots)
    .signature_policy(SignaturePolicy::modern().permit("slh-dsa-sha2-128f"))
    .build();
```

`signature_policy` on the unified [`Config`] applies to both client and
server roles — for the server 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](https://github.com/KarpelesLab/purecrypto/releases) 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`:

```sh
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

# Static link (self-contained):
cc app.c -I include target/release/libpurecrypto.a -lpthread -ldl -lm -o app
```

The API is declared in [`include/purecrypto.h`](include/purecrypto.h): one-shot
and streaming hashing, HMAC, OS randomness, RSA/ECDSA/Ed25519 key generation,
signing, verification and PEM I/O, ML-KEM (FIPS 203) / ML-DSA (FIPS 204) /
SLH-DSA (FIPS 205) keys, X.509 parsing/verification, and a sans-I/O TLS /
DTLS surface (`pc_tls_cfg_*`, `pc_tls_*`) including post-handshake
accessors for the negotiated cipher suite and peer SNI. 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.

## License

Licensed under the [MIT License](LICENSE).
