# ASYMMETRIC
## Arithmetic Foundation
The public-key layer is built on:
- `BigUint`
- `BigInt`
- `MontgomeryCtx`
- shared number-theory helpers in `src/public_key/primes.rs`
The in-tree bigint backend stores `u64` limbs in little-endian limb order and
uses Montgomery multiplication for repeated modular arithmetic under odd
moduli. That is the common case for every implemented public-key
scheme here.
Implementation references for multiplication-kernel upgrades are tracked in
`pubs/comba-1990-exponentiation-cryptosystems-on-the-ibm-pc.pdf` and
`pubs/karatsuba-ofman-1963-multiplication-of-multidigit-numbers-on-automata.pdf`.
The design goal is:
- keep the arithmetic visible and auditable
- keep the scheme logic close to the published arithmetic
- keep open the option of swapping the arithmetic backend later if larger-key
performance demands it
The broader implementation policy matches the rest of the crate:
- pure idiomatic Rust
- no architecture intrinsics
- no C/FFI escape hatches
- minimal dependencies unless they clearly improve interoperability or
maintainability
That is why the bigint and Montgomery code live in-tree, while XML parsing uses
`quick-xml` and RSA key persistence uses standard DER/PEM structures where that
buys real compatibility.
## Three-Level API
The public-key layer uses a common pattern, but it is not literally identical
across every scheme:
1. Arithmetic maps such as `encrypt_raw`, `encrypt_with_nonce`,
`encrypt_point_with_nonce`, or `sign_digest_with_nonce`, which keep the underlying math
explicit.
2. Typed wrappers such as `encrypt`, `decrypt`, `sign_message`, and
`verify_message`, which work with the scheme's natural ciphertext or
signature type.
3. Byte wrappers such as `encrypt_bytes`, `decrypt_bytes`,
`verify_message_bytes`, standard compact wire encodings, and crate-defined
key blobs.
Not every scheme exposes all three layers, and that is intentional:
- key-agreement schemes return shared-secret material, not ciphertexts
- signature schemes expose signing and verification rather than encryption
- hybrid schemes such as `ECIES` are naturally byte-oriented at the top layer
The consistency target for new APIs is:
- use `*_with_nonce` for deterministic or caller-supplied randomness entry points
- use `to_wire_bytes` / `from_wire_bytes` for compact standard encodings that
omit curve or algorithm parameters
- use `to_key_blob` / `from_key_blob` for the crate-defined self-describing
binary formats
Level 1 remains the right place for arithmetic tests and direct cross-checks.
Level 2 is the normal typed interface. Level 3 is the byte-oriented convenience
layer for schemes that naturally have one.
## Naming Conventions
Naming follows explicit intent throughout:
- Serialization distinguishes compact from self-describing formats:
`to_key_blob` / `from_key_blob` for the crate-defined binary blob,
`to_wire_bytes` / `from_wire_bytes` for compact standard encodings.
- Deterministic or caller-supplied randomness entry points use
`sign_digest_with_nonce` rather than generic `sign_with_k`.
- Verification of precomputed digests uses `verify_digest_scalar`.
- DH agreement methods name the returned form explicitly:
- finite-field DH: `agree_element`
- short-Weierstrass ECDH: `agree_x_coordinate`
- Edwards DH: `agree_compressed_point`
Public-key exports are grouped under `cryptography::vt` to make variable-time
behavior explicit at import sites.
## Public-Key Surface
### Integer and finite-field schemes
- `Rsa` — encryption and signatures
- `Dsa` — signatures (FIPS 186-5)
- `Cocks` — encryption (historical; 1973)
- `ElGamal` — encryption
- `Rabin` — encryption
- `Paillier` — additively homomorphic encryption
- `SchmidtSamoa` — encryption
- `Dh` — finite-field Diffie-Hellman key exchange
### Post-quantum lattice schemes
- `MlKem` (`ML-KEM-512/768/1024`) — key encapsulation mechanism
- `MlDsa` (`ML-DSA-44/65/87`) — digital signatures
- Details, usage notes, and PQ-specific benchmarks are documented in
[POSTQUANTUM.md](POSTQUANTUM.md).
### Short-Weierstrass elliptic-curve schemes
- `Ecdh` — EC Diffie-Hellman key exchange (ANSI X9.63 / SEC 1)
- `Ecdsa` — EC Digital Signature Algorithm (FIPS 186-5)
- `EcElGamal` — EC-ElGamal encryption with additive homomorphism
- `Ecies` — Elliptic Curve Integrated Encryption Scheme (ephemeral ECDH + AES-256-GCM)
### Twisted Edwards schemes
- `EdwardsDh` — Edwards-curve Diffie-Hellman key agreement
- `EdDsa` — generic Edwards-curve Schnorr/EdDSA-style signatures
- `Ed25519` — RFC 8032 Edwards-curve signatures
- `EdwardsElGamal` — Edwards-curve ElGamal encryption
### Montgomery-curve ECDH (RFC 7748)
- `X25519` — Curve25519 ECDH, constant-time Montgomery ladder
- `X448` — Curve448 ECDH, constant-time Montgomery ladder
These two are the only public-key primitives in the crate that aim for
constant-time execution; see the [Curve25519 / Curve448 ECDH section
below](#curve25519--curve448-ecdh-rfc-7748) for details.
The Edwards arithmetic is generic over `TwistedEdwardsCurve`, but the only
built-in named Edwards domain currently shipped in-tree is `ed25519()`.
### Wrapper layers
- `RsaOaep<H>` for `RSAES-OAEP`
- `RsaPss<H>` for `RSASSA-PSS`
Every implemented scheme has:
- explicit key construction from mathematical parameters
- built-in key generation
- key serialization
- byte-oriented encrypt/decrypt helpers where encryption is defined
- byte-oriented sign/verify helpers where signatures are defined
`RSA` has the richest standards surface because RFC 8017 defines both
encryption and signature encodings. `DSA` and `ECDSA` are the standard
signature constructions; they do not need extra padding profiles. The other
schemes expose crate-defined message and serialization wrappers, which is the
honest thing to do because there is no equally universal RFC/NIST padding story
for those primitive forms.
## Serialization
### RSA
`RSA` uses real modern standards:
- public keys:
- PKCS #1
- SubjectPublicKeyInfo (SPKI)
- private keys:
- PKCS #1
- PKCS #8
- containers:
- DER
- PEM
RSA also has an optional XML export/import path purely for orthogonality and
debugging convenience; the canonical interoperable formats remain PKCS / X.509.
### Non-RSA Schemes
Most non-RSA key types use the crate-defined integer-sequence framing for
`to_key_blob()` / `from_key_blob()`. `Ed25519` is the main exception: its
canonical fixed-width forms are exposed as `to_raw_bytes()` /
`from_raw_bytes()` (32-byte compressed public key or 32-byte seed), matching
RFC 8032.
`Dsa`, `Cocks`, `ElGamal`, `Rabin`, `Paillier`, `SchmidtSamoa`, `Dh`,
`Ecdsa`, `EcElGamal`, `Ecies`, `Ecdh`, `EdwardsDh`, `EdwardsElGamal`,
`EdDsa`, and `Ed25519` use crate-defined formats:
- binary: DER `SEQUENCE` of positive `INTEGER`s
- text:
- scheme-specific PEM labels
- a simple fixed-schema XML form
This deliberately copies the structural simplicity of the RSA key material
without pretending that those schemes have standard OIDs or a real PKCS/X.509
profile.
The short-Weierstrass EC public key types (`EcdhPublicKey`, `EcdsaPublicKey`,
`EciesPublicKey`, `EcElGamalPublicKey`) encode the curve domain parameters
`(p, a, b, n, h, Gx, Gy)` alongside the public point `(Qx, Qy)`, so
deserialization can reconstruct the `CurveParams` without a separate OID lookup
or parameter database. The Edwards key types do the same job for
`TwistedEdwardsCurve`, carrying the Edwards parameters together with the
compressed public point.
## Scheme Notes
### Integer and finite-field schemes
#### RSA
Reference: PKCS #1 v2.2 (RFC 8017) for OAEP, PSS, and the conventional key
formats used by the interoperable RSA layer in this crate.
Core arithmetic:
```math
c = m^e \bmod n,\qquad m = c^d \bmod n
```
with:
```math
n = pq,\qquad d \equiv e^{-1} \pmod{\lambda(n)}
```
The default key-generation path deliberately chooses the standard sparse public
exponent:
```math
e = 65{,}537
```
That keeps the public operation cheap while preserving the conventional RSA
shape. The matching private exponent `d` is the full modular inverse modulo
`\lambda(n)`, so the raw private operation is much heavier than the raw public
operation.
The practical RSA layer is the most complete in the crate:
- standards-based OAEP encryption
- standards-based PSS signatures
- standard key serialization
- generated or imported keys
So RSA is the "real protocol" path in the integer family: the raw arithmetic is
still present, but the intended surface is padded OAEP/PSS rather than textbook
RSA on caller-supplied integers.
The serialization story is also distinct from the other public-key families.
RSA uses PKCS#1, PKCS#8, and SPKI-compatible encodings, so it interoperates
with external tooling instead of relying on the crate-defined integer-sequence
format used elsewhere.
One practical caveat matters for the benchmark tables: private operations use
CRT recombination ($d_P$, $d_Q$, $q_{\text{Inv}}$), which substantially reduces
`decrypt`/`sign` latency, but the public side remains much faster because
it uses the standard sparse $e = 65{,}537$.
#### ElGamal
Reference: Taher ElGamal, "A Public Key Cryptosystem and a Signature Scheme
Based on Discrete Logarithms" (1985); see `pubs/elgamal-1985.pdf`.
Core arithmetic:
```math
\gamma = g^k \bmod p,\qquad \delta = m \cdot y^k \bmod p,\qquad y = g^a \bmod p
```
The key-generation path uses a prime-order subgroup construction instead of the
older safe-prime search. A safe prime is a modulus of the form $p = 2q + 1$
with `q` prime; it gives simple subgroup structure, but searching for those
moduli is much slower than generating $p = kq + 1$ directly. The
implementation keeps the subgroup structure explicit while avoiding that
pathological key-generation cost.
The public key stores the real ephemeral bound used for encryption, so the
random ephemeral exponent is sampled from the right range instead of from the
full `p - 1` interval. Generated keys use the actual subgroup order `q` for
that bound; explicitly constructed keys fall back to `p - 1` when the subgroup
order is not derivable from the supplied parameters.
The API follows the same layered pattern as the EC and Edwards ElGamal wrappers:
- an explicit-nonce entry point for deterministic fixtures
- a randomized ciphertext layer over the raw group element
- byte helpers that frame the bigint ciphertext pair into the crate-defined
binary format
So the finite-field ElGamal path is still useful for reproducible KATs and
in-repo byte-oriented tests even though its wire format is crate-specific.
This is still multiplicative ElGamal, not one of the additive homomorphic
variants. The native plaintext group law is multiplication modulo `p`; the byte
helpers are only a serialization layer over that arithmetic.
#### DSA
Reference: FIPS 186-5, Digital Signature Standard (see
`pubs/fips186-5.pdf` and the matching BibTeX entry in the top-level
references).
Core arithmetic:
```math
r = (g^k \bmod p) \bmod q,\qquad
s = k^{-1}(z + xr) \bmod q
```
with verification:
```math
w = s^{-1} \bmod q,\qquad
u_1 = zw \bmod q,\qquad
u_2 = rw \bmod q
```
and acceptance when:
```math
\bigl(g^{u_1} y^{u_2} \bmod p\bigr) \bmod q = r
```
The implementation reuses the same prime-order subgroup generation shape as
`ElGamal`: generated keys store `(p, q, g)` explicitly, and signatures sample
their per-message nonce from `[1, q)`. The digest representative is reduced to
the leftmost $N = \mathrm{bits}(q)$ bits before signing and verification,
matching the Digital Signature Standard's treatment of hash outputs that are
wider than the subgroup order.
For generated keys, the implementation uses:
```math
N = \mathrm{clamp}(\lfloor L / 4 \rfloor, 16, 256)
```
for a modulus size $L = \mathrm{bits}(p)$. That is not the exact FIPS menu of $(L, N)$
pairs (`(1024, 160)`, `(2048, 224)`, `(2048, 256)`, `(3072, 256)`), but it
keeps the subgroup order conservative for the representative benchmark sizes
used here while staying within the same finite-field `DSA` structure.
The public API is intentionally parallel to `ECDSA`:
- digest-level signing and verification for callers who already own the hash
- message-level helpers parameterized by a `Digest`
- an explicit-nonce signing entry point for deterministic tests and fixtures
The important distinction from `EdDsa` and `Ed25519` is that `DSA` signs a
digest representative `z`; it does not hash internally unless the caller uses
the message-level wrapper.
Like `ElGamal` and `Dh`, generated `DSA` keys carry the full subgroup domain
parameters `(p, q, g)` in the key object and in the crate-defined key blob.
That keeps key import self-contained instead of depending on an external
parameter registry.
#### Cocks
Reference: the historical Clifford Cocks construction; the implementation here
keeps the original arithmetic rather than wrapping it in a modern standards
profile.
Core arithmetic:
```math
c = m^n \bmod n,\qquad n = pq,\qquad \pi \equiv p^{-1} \pmod{q - 1}
```
with the private recovery map:
```math
m = c^\pi \bmod q
```
Cocks is historically important: Clifford Cocks proposed it in 1973, five
years before RSA. The scheme is unusual because the public exponent is the
modulus itself. The crate keeps that arithmetic intact and adds the byte-level
serialization layer on top instead of inventing a modernized padding story
that the literature does not standardize.
The private exponent is:
```math
\pi \equiv p^{-1} \pmod{q - 1}
```
and the key observation is the CRT reduction modulo $q$: when
$c = m^{pq} \bmod n$, raising $c$ to $\pi$ modulo $q$ reduces the exponent
from $pq\pi$ to $q$, so Fermat brings the result back to $m$.
From an API perspective, `Cocks` stays intentionally narrow:
- raw arithmetic on the integer plaintext representative
- byte helpers for the crate-defined framed encoding
- no attempt at standards-style padding or interoperable key containers
That restraint is deliberate. This is an educational historical primitive in
the repo, not a recommendation for modern deployment.
#### Rabin
Reference: the classic Rabin trapdoor permutation; the implementation keeps the
core squaring trapdoor visible and adds only the minimal disambiguation layer
needed for practical decryption.
Core arithmetic:
```math
c = m^2 \bmod n,\qquad n = pq
```
Decryption computes square roots modulo `p` and `q`, then recombines them with
the Chinese remainder theorem to recover the four square roots modulo `n`.
Because plain Rabin is ambiguous, the implementation uses a tagged-message
variant: the tag is carried inside the encoded plaintext and is used to select
the intended root deterministically at decrypt time.
The implementation requires Blum primes:
```math
p \equiv q \equiv 3 \pmod 4
```
That condition makes square-root extraction cheap, because a square root of
`c` modulo `p` can be written directly as:
```math
c^{(p + 1)/4} \bmod p
```
and likewise modulo `q`, avoiding a heavier general-purpose square-root
algorithm during decryption.
Rabin is historically important because it is one of the earliest public-key
trapdoor constructions with a tight reduction story: in the plain setting,
inverting the squaring map modulo $n = pq$ is essentially equivalent to
factoring $n$. The fixed disambiguation tag used here is what lets the code
identify the intended root among the four CRT roots and turn the raw squaring
trapdoor into a deterministic decryptor.
The API follows that same philosophy:
- raw encryption over the integer representative
- byte wrappers that carry the tagged plaintext encoding
- key generation that enforces the Blum-prime precondition directly
So the practical wrapper is small, but it is enough to make the square-root
ambiguity explicit and auditable rather than leaving that selection logic to
callers.
#### Paillier
Reference: Pascal Paillier, "Public-Key Cryptosystems Based on Composite
Degree Residuosity Classes" (1999); see `pubs/paillier-1999.pdf`.
Core arithmetic:
```math
c = \zeta^m r^n \bmod n^2
```
with decryption:
```math
m = L(c^\lambda \bmod n^2)\,\mu \bmod n,\qquad L(u) = \frac{u - 1}{n}
```
`Paillier` exposes both encryption/decryption and the natural homomorphic
operations:
- ciphertext rerandomization
- ciphertext multiplication modulo $n^2$, corresponding to plaintext addition
That homomorphic surface is a real part of the scheme, not an extra trick, so
it is intentionally part of the usable API.
If `c_1` encrypts `m_1` and `c_2` encrypts `m_2`, then:
```math
c_1 c_2 \bmod n^2
```
decrypts to:
```math
m_1 + m_2 \pmod n
```
The wrapper keeps that property visible through
`PaillierPublicKey::add_ciphertexts(...)`, and `rerandomize(...)` preserves the
same plaintext while refreshing the random factor so identical messages do not
stay linkable across ciphertext refreshes.
That is the intended way to read the API surface:
- the raw ciphertext type is still just the integer modulo $n^2$
- the byte helpers serialize that integer into a crate-defined framing
- the homomorphic operations are first-class because they are part of the
reason to choose the scheme at all
Among the integer schemes, this is the clearest "use it for its special
algebra" path rather than for generic public-key encryption.
#### Schmidt-Samoa
Reference: Katja Schmidt-Samoa (2005); see `pubs/schmidt-samoa.pdf` and the
matching BibTeX entry in the repository references.
Core arithmetic:
```math
c = m^n \bmod n,\qquad n = p^2 q,\qquad \gamma = pq
```
with the private exponent chosen so that:
```math
d \equiv n^{-1} \pmod{\mathrm{lcm}(p - 1, q - 1)}
```
and decryption:
```math
m = c^d \bmod \gamma
```
The unusual choice $n = p^2 q$ is the point of the construction: it gives the
scheme enough structure to choose
$d \equiv n^{-1} \pmod{\mathrm{lcm}(p-1, q-1)}$ and recover the plaintext
modulo $\gamma = pq$, rather than modulo the full public
modulus.
Like Cocks, Schmidt-Samoa uses the modulus itself as the public exponent. It
is mathematically neat and implemented faithfully here, but it does not have
the same standards ecosystem or deployment relevance as RSA.
The wrapper therefore stays minimal:
- raw arithmetic for the underlying construction
- byte helpers for crate-local usability
- no attempt to present it as a standards-grade interoperable scheme
This keeps the scheme available for study and comparison without pretending it
belongs in the same operational category as the RSA layer.
#### Diffie-Hellman
Reference: the classic finite-field Diffie-Hellman model, with subgroup
validation handled in the same prime-order subgroup framework used for `DSA`
and `ElGamal`.
Core arithmetic:
```math
y = g^x \bmod p
```
with shared secret:
```math
s = y_{\mathrm{peer}}^x \bmod p
```
`DH` uses a prime-order subgroup construction identical to `DSA` and
`ElGamal`: a Sophie-Germain-style group with explicit subgroup order `q`. The
public key stores `(p, q, g, y)` so the receiver can validate that the peer's
contribution actually lies in the correct subgroup before computing the shared
secret. The validation check is:
```math
1 < y < p \qquad \text{and} \qquad y^q \equiv 1 \pmod{p}
```
`DhPrivateKey::agree` returns `None` when the peer key belongs to a different
group or fails the subgroup check. The raw shared secret is returned as a
`BigUint`; callers are expected to apply their own KDF before using it as
keying material.
That return shape is intentionally lower-level than the EC variants. `DH`
returns the shared group element itself, not a byte-oriented KDF input chosen
by the library. The crate leaves that derivation step to the caller rather than
quietly committing to a KDF policy here.
Like `DSA`, the key blobs carry `(p, q, g)` explicitly. That makes `DhParams`
and the generated keys self-contained and avoids any hidden dependency on an
external parameter database.
### Short-Weierstrass elliptic-curve schemes
#### ECDH
Reference: SEC 1 v2.0, SEC 2 v2.0, and NIST SP 800-56A Rev. 3 (these are
external standards; no local PDFs are checked into `pubs/`).
Shared secret:
```math
S = d \cdot Q_{\mathrm{peer}}, \qquad \text{secret} = S_x
```
`ECDH` follows SEC 1 v2.0: the shared secret is the x-coordinate of the point
product, zero-padded to the curve's coordinate length.
`EcdhPrivateKey::agree` returns `None` when the product is the point at
infinity.
`EcdhPublicKey` and `EcdhPrivateKey` carry the full `CurveParams` so both sides
can use any of the named curves (`p256`, `p384`, `p521`, `secp256k1`, etc.)
without a separate curve-identifier negotiation layer.
On the representation side, the short-Weierstrass public key types now expose
both of the forms the Edwards writeup already calls out:
- compact SEC 1 point encodings via `to_wire_bytes` / `from_wire_bytes`
- the crate-defined self-describing key blob that carries the full curve
parameters
That split is deliberate. The compact form is what a peer would normally place
on the wire when the curve is already known; the self-describing blob is what
the repo uses when it wants a standalone serialized key without an external OID
or curve registry.
As with `DH`, `EcdhPrivateKey::agree` returns raw shared-secret material, not a
KDF output. The returned bytes are the padded x-coordinate and should be fed
through a KDF before use as a symmetric key.
#### ECIES
Reference: SEC 1 v2.0 and NIST SP 800-56A Rev. 3 for the EC key-establishment
model and point encodings (external standards; no local PDFs are checked into
`pubs/`).
`ECIES` is the standard way to encrypt arbitrary byte strings to a static EC
public key. It combines ephemeral ECDH with a symmetric encryption step, so the
per-message overhead is a single scalar multiplication by the sender and a
single scalar multiplication by the receiver.
**Encryption:**
1. Generate an ephemeral key pair $(k, R)$ where $R = k \cdot G$.
2. Compute the shared point $S = k \cdot Q$.
3. Derive symmetric key and nonce from $S_x$:
```math
\text{key} = \mathrm{SHA\text{-}256}(\mathtt{0x01} \mathbin\| S_x)
\qquad
\text{nonce} = \mathrm{SHA\text{-}256}(\mathtt{0x02} \mathbin\| S_x)_{[0..12]}
```
4. Encrypt the message with AES-256-GCM, using $R_{\text{bytes}}$ as the
additional authenticated data (AAD). The AAD binding prevents `R` from being
silently swapped without triggering a tag failure.
**Wire format:**
```text
R_bytes (1 + 2·coord_len bytes, SEC 1 uncompressed)
ciphertext (same length as plaintext)
tag (16 bytes, GCM authentication tag)
```
**Decryption:**
1. Parse `R_bytes` from the front of the ciphertext.
2. Compute $S = d \cdot R$.
3. Re-derive key and nonce from $S_x$.
4. AES-256-GCM decrypt; return `None` if the tag fails.
The GCM tag simultaneously authenticates the ciphertext and the ephemeral
public key, so no separate MAC layer is needed.
This makes `ECIES` the practical "encrypt arbitrary bytes to an EC key" path
in the short-Weierstrass family. Unlike `EC-ElGamal`, it does not try to expose
the group law of the plaintext space; it uses the EC operation only for key
establishment, then hands the real data path to AES-256-GCM.
The key objects follow the same representation pattern as `ECDH` and `ECDSA`:
they can be serialized either as compact SEC 1 points when the curve is known
out-of-band or as the crate-defined self-describing blob when the curve
parameters need to travel with the key.
#### EC-ElGamal
Reference: the ElGamal paper for the discrete-logarithm construction
(`pubs/elgamal-1985.pdf`); SEC 1 v2.0 and SEC 2 v2.0 for the elliptic-curve
group and point encodings (external standards; no local PDFs are checked into
`pubs/`).
EC-ElGamal has three distinct plaintext layers stacked on the same key pair.
**Point layer** — encrypt an arbitrary curve point `M`:
```math
(C_1, C_2) = (k \cdot G,\; M + k \cdot Q)
```
Decryption recovers `M` via:
```math
M = C_2 - d \cdot C_1
```
**Byte layer** — encrypt arbitrary bytes via Koblitz embedding: the message
bytes are padded and placed into an x-coordinate candidate; `decode_point` is
called with the `0x02` compressed prefix until a valid curve point is found.
The last byte of the padded x-coordinate is an iteration counter
$j \in [0, 255]$; the first byte of the decoded x-coordinate is stripped
during recovery, leaving the original message bytes. This approach works on
every named curve in this crate because all have $p \equiv 3 \pmod{4}$, which means the
compressed-point square root exists and the iteration succeeds quickly in
practice.
The message capacity per ciphertext is `coord_len - 1` bytes.
**Integer layer** — additively homomorphic encryption of a small integer `m`:
```math
\text{encrypt\_int}(m) = \text{encrypt\_point}(m \cdot G)
```
Homomorphic addition of two ciphertexts:
```math
(C_1 + C_1',\; C_2 + C_2') \;\xrightarrow{\text{decrypt}}\; (m_1 + m_2) \cdot G
```
The integer $m$ is recovered from $m \cdot G$ via baby-step giant-step
(BSGS) with $O\!\left(\sqrt{m_{\max}}\right)$ precomputation.
So `EC-ElGamal` is intentionally the arithmetic-rich counterpart to `ECIES`:
- point encryption for direct group-element work
- byte encryption for bounded arbitrary payloads via Koblitz embedding
- additive homomorphism on the integer layer
The practical constraint is capacity. Because the byte layer embeds the payload
into an x-coordinate candidate, each ciphertext can carry only `coord_len - 1`
bytes. That is why `ECIES` exists alongside it: `ECIES` is the general-purpose
byte-encryption path, while `EC-ElGamal` is the path that preserves the group
structure when that algebra matters.
As with the other short-Weierstrass public key types, the public key can be
serialized either as a compact SEC 1 point or as the crate-defined blob that
embeds the full curve parameters.
#### ECDSA
Reference: FIPS 186-5 (`pubs/fips186-5.pdf`); SEC 1 v2.0 and SEC 2 v2.0 for the
underlying elliptic-curve point encodings (external standards; no local PDFs are
checked into `pubs/`).
Core arithmetic (FIPS 186-5):
```math
r = (k \cdot G)_x \bmod n,\qquad
s = k^{-1}(z + rd) \bmod n
```
with verification:
```math
w = s^{-1} \bmod n,\qquad
u_1 = zw \bmod n,\qquad
u_2 = rw \bmod n
```
and acceptance when:
```math
(u_1 \cdot G + u_2 \cdot Q)_x \bmod n = r
```
The per-message nonce `k` is generated from the crate's `Csprng`. The digest
representative `z` is the leftmost `bits(n)` bits of the hash output, matching
the FIPS 186-5 truncation rule for hash functions wider than the group order.
The key types (`EcdsaPublicKey`, `EcdsaPrivateKey`) carry the full `CurveParams`
and work with any named curve.
The API mirrors the `DSA` surface closely:
- digest-level signing and verification
- message-level helpers parameterized by a `Digest`
- an explicit-nonce signing path for deterministic tests and vectors
So the short-Weierstrass and finite-field signature families line up on the
same caller model even though their underlying groups differ.
Like the other short-Weierstrass key types, `EcdsaPublicKey` supports both
compact SEC 1 point encodings and the self-describing crate-defined key blob.
That matches the Edwards writeup's clearer separation between "wire point" and
"standalone serialized key" forms.
The important practical caveat is the same one called out for the Edwards side:
the arithmetic is generic and variable-time. The implementation is correct and
well tested, but it is not a hardened constant-time signing engine.
### Twisted Edwards schemes
#### Edwards DH
Reference: NIST SP 800-56A Rev. 3 for the DH model (external standard) with
Edwards-group arithmetic and compressed-point conventions matching FIPS 186-5
(`pubs/fips186-5.pdf`).
`EdwardsDh` provides the same core operation on a twisted Edwards curve:
```math
S = d \cdot Q_{\mathrm{peer}}
```
The difference is the wire representation. `EdwardsDhPrivateKey::agree`
returns the compressed Edwards encoding of the shared point, so the output is a
canonical 32-byte value on the built-in Ed25519 curve instead of a bare
x-coordinate. That matches the way the Edwards side of the crate already treats
points as compressed byte strings.
The implementation is generic over `TwistedEdwardsCurve`, but the in-tree named
fixture and benchmark path today is the built-in `ed25519()` domain.
#### Edwards ElGamal
Reference: the ElGamal paper for the encryption law (`pubs/elgamal-1985.pdf`)
with Edwards-curve group and encoding choices matching the Ed25519 / EdDSA
side of the crate (`pubs/fips186-5.pdf`; SEC 2 v2.0 is an external standard
with no local PDF).
`EdwardsElGamal` mirrors the same ElGamal construction on a twisted Edwards
group:
```math
(C_1, C_2) = (k \cdot B,\; M + k \cdot Q)
```
with decryption:
```math
M = C_2 - d \cdot C_1
```
As with the short-Weierstrass variant, the module exposes:
- point encryption
- integer encryption via `m \cdot B`
- homomorphic ciphertext addition
The main distinction is representation: the Edwards wrapper uses compressed
Edwards point encodings throughout, which makes ciphertext serialization more
compact and keeps it aligned with the `Ed25519` / `EdDsa` side of the crate.
As with `EdwardsDh`, the machinery accepts any caller-supplied
`TwistedEdwardsCurve`, but the in-tree deterministic fixtures and benchmarks
currently target the built-in `ed25519()` domain.
#### Ed25519
Reference: FIPS 186-5 for EdDSA (`pubs/fips186-5.pdf`); SEC 2 v2.0 for the
underlying elliptic-curve parameter conventions is an external standard with
no local PDF.
`Ed25519` is the fixed-curve RFC 8032 signature construction built on the
Edwards arithmetic in this crate. Unlike the generic `EdDsa` layer, it follows
the standard seed-hash-and-clamp flow exactly:
```math
h = \mathrm{SHA\text{-}512}(\text{seed})
```
Clamp the lower 32 bytes of `h` to derive the secret scalar `a`, and use the
upper 32 bytes as the deterministic nonce prefix. Signing then computes:
```math
r = H(\text{prefix} \parallel M) \bmod n
```
```math
R = r \cdot B,\qquad
k = H(\mathrm{enc}(R) \parallel \mathrm{enc}(A) \parallel M) \bmod n
```
```math
S = r + ka \bmod n
```
The standard 64-byte signature is:
```math
\sigma = \mathrm{enc}(R) \parallel \mathrm{enc}_{\mathrm{LE}}(S)
```
Verification checks:
```math
S \cdot B = R + kA
```
The API exposes the real RFC shapes directly:
- private key: 32-byte seed
- public key: 32-byte compressed point
- signature: 64-byte `R || S`
So this is the standards-conformant Edwards path, while `EdDsa` remains the
more explicit curve-generic signature layer for callers who want direct scalar
control.
The test coverage for this module now includes the full RFC 8032 section 7.1
Ed25519 vector set, along with strict parsing and rejection checks for malformed
public keys and signatures.
### Curve25519 / Curve448 ECDH (RFC 7748)
Reference: RFC 7748, "Elliptic Curves for Security", §5 (X25519, X448) and
§5.2 (test vectors).
`X25519` and `X448` are the Montgomery-form ECDH primitives:
```math
\text{X25519}: \quad y^2 = x^3 + 486662\,x^2 + x \quad \text{over } \mathrm{GF}(2^{255} - 19)
```
```math
\text{X448}: \quad y^2 = x^3 + 156326\,x^2 + x \quad \text{over } \mathrm{GF}(2^{448} - 2^{224} - 1)
```
The crate ships these as a constant-time exception within `cryptography::vt`.
Unlike the rest of the public-key surface (which uses the variable-time
in-tree `BigUint`), X25519 and X448 use dedicated fixed-radix limb
representations:
- X25519: 5 limbs of radix $2^{51}$, two-pass carry reduction with the
`2^{255} \equiv 19 \pmod p` wrap-around factor.
- X448: 8 limbs of radix $2^{56}$, two-pass carry reduction with the
`2^{448} \equiv 2^{224} + 1 \pmod p` wrap-around factor.
In both cases the Montgomery ladder uses mask-driven `cswap` so the access
pattern depends on the loop index, not on the secret scalar bit. Field
multiply, square, conditional subtract, and final canonicalisation are
written without data-dependent branches or table lookups.
Scalar clamping follows RFC 7748 §5 exactly:
- X25519: `k[0] &= 248; k[31] &= 127; k[31] |= 64`
- X448: `k[0] &= 252; k[55] |= 128`
The encoded `u`-coordinate inputs likewise follow the spec:
- X25519: high bit of `u[31]` is masked off before decoding
- X448: full 448-bit `u`-coordinate, no masking
The shared-secret API (`agree`) returns `Option<[u8; N]>` and rejects the
all-zero output, as RFC 7748 §6 recommends for low-order point detection.
The raw `scalar_mult` function exposes the unconditional RFC 7748 mapping
(useful for KAT validation and protocol layers that prefer to do their own
low-order check).
Example (X25519):
```rust
use cryptography::CtrDrbgAes256;
use cryptography::vt::X25519;
let mut rng = CtrDrbgAes256::new(&[0x33u8; 48]);
let (pub_a, priv_a) = X25519::generate(&mut rng);
let (pub_b, priv_b) = X25519::generate(&mut rng);
let shared_a = priv_a.agree(&pub_b).expect("non-low-order");
let shared_b = priv_b.agree(&pub_a).expect("non-low-order");
assert_eq!(shared_a, shared_b);
```
Test coverage in `cargo test`:
- RFC 7748 §5.2 single-step vectors for X25519 and X448
- iterated tests at 1 and 1000 iterations (run by default)
- iterated tests at 1 000 000 iterations (gated `#[ignore]`; run with
`cargo test --release -- --ignored rfc7748_section5_2_iter_1m`)
- ECDH symmetry round-trip (`A * (B * G) == B * (A * G)`)
- low-order rejection by `agree`
- field-arithmetic sanity (`x * x^{-1} = 1`)
## Byte-Oriented APIs
The public-key wrappers now distinguish clearly between:
- the arithmetic interfaces (`encrypt_raw`, `decrypt_raw`, typed ciphertexts)
- the usable byte-to-byte helpers
Examples:
- `CocksPublicKey::encrypt_bytes` / `CocksPrivateKey::decrypt_bytes`
- `DsaPrivateKey::sign_message_bytes::<H>` / `DsaPublicKey::verify_message_bytes::<H>`
- `EcElGamalPublicKey::encrypt` / `EcElGamalPrivateKey::decrypt` (Koblitz byte layer)
- `EciesPublicKey::encrypt` / `EciesPrivateKey::decrypt` (arbitrary-length bytes)
- `EcdsaPrivateKey::sign_message::<H>` / `EcdsaPublicKey::verify_message::<H>`
- `ElGamalPublicKey::encrypt_bytes` / `ElGamalPrivateKey::decrypt_bytes`
- `PaillierPublicKey::encrypt_bytes` / `PaillierPrivateKey::decrypt_bytes`
- `RabinPublicKey::encrypt_bytes` / `RabinPrivateKey::decrypt_bytes`
- `SchmidtSamoaPublicKey::encrypt_bytes` / `SchmidtSamoaPrivateKey::decrypt_bytes`
For the schemes whose native ciphertext is a bigint or a pair of bigints, these
helpers serialize the ciphertext into the same crate-defined binary framing used
throughout the non-RSA key formats.
## Public-Key Performance
Public-key timing is measured with [pilot-bench](https://github.com/darrelllong/pilot-bench)
driving `pilot_pk` through:
```text
bash scripts/bench_all_pk_full.sh
```
The publication-facing numbers below come from Pilot and report milliseconds
per operation, **90%** confidence-interval half-width, and rounds required to
hit the stop rule. The 2026-05-08 sweep was run with
`PILOT_PRESET=normal --confidence-level 0.90` (10% CI half-width target,
autocorrelation tolerance 0.2, ≥ 50 rounds minimum sample size). The tables
below are parallel runs on:
- Apple M1 Max (`wigner.local`)
- AMD EPYC 7452 (`moore.soe.ucsc.edu`, single-core slice)
- Broadcom BCM2712 / Cortex-A76 (`darby.local`, Raspberry Pi 5)
For RSA specifically, the timing gap between `encrypt`/`verify` and
`decrypt`/`sign` is still expected: the private side now uses CRT, but the
public side continues to benefit from the sparse default exponent
$e = 65{,}537$.
### Finite-field public key (1024-bit)
| `rsa_keygen_1024` | 17.79 | ±0.9056 | 50 | 22.49 | ±0.1048 | 50 | 41.03 | ±0.01044 | 80 |
| `rsa_encrypt_1024` | 0.04317 | ±0.0002373 | 110 | 0.05593 | ±0.0003877 | 50 | 0.09092 | ±0.001591 | 50 |
| `rsa_decrypt_1024` | 0.326 | ±0.001685 | 201 | 0.4188 | ±0.002015 | 57 | 0.7981 | ±0.001612 | 50 |
| `rsa_sign_1024` | 0.3262 | ±0.001666 | 110 | 0.4197 | ±0.001344 | 50 | 0.8024 | ±0.01427 | 50 |
| `rsa_verify_1024` | 0.04322 | ±0.0001499 | 80 | 0.05616 | ±0.0001574 | 110 | 0.09102 | ±0.0001209 | 50 |
| `elgamal_keygen_1024` | 82.42 | ±0.3261 | 117 | 118.5 | ±0.1654 | 50 | 244 | ±0.7526 | 50 |
| `elgamal_encrypt_1024` | 0.4056 | ±0.002128 | 50 | 0.5919 | ±0.002056 | 50 | 1.266 | ±0.001071 | 50 |
| `elgamal_decrypt_1024` | 0.2105 | ±0.003503 | 80 | 0.3051 | ±0.001142 | 80 | 0.6525 | ±0.0004211 | 80 |
| `dsa_keygen_1024` | 59.38 | ±0.1845 | 80 | 85.73 | ±0.1615 | 170 | 176.2 | ±0.4703 | 50 |
| `dsa_sign_1024` | 0.3591 | ±0.006232 | 50 | 0.5384 | ±0.001327 | 80 | 0.9262 | ±0.01647 | 50 |
| `dsa_verify_1024` | 0.5266 | ±0.001681 | 117 | 0.7889 | ±0.006818 | 59 | 1.49 | ±0.00501 | 50 |
| `paillier_keygen_1024` | 18.78 | ±0.03684 | 116 | 24.88 | ±0.2 | 50 | 46.57 | ±0.2831 | 50 |
| `paillier_encrypt_1024` | 7.658 | ±0.005387 | 202 | 11.39 | ±0.02877 | 80 | 19.47 | ±0.00288 | 50 |
| `paillier_decrypt_1024` | 2.853 | ±0.001856 | 170 | 4.118 | ±0.05992 | 50 | 9.44 | ±0.005553 | 50 |
| `paillier_rerandomize_1024` | 4.954 | ±0.01245 | 230 | 7.296 | ±0.01047 | 50 | 13.47 | ±0.05421 | 50 |
| `paillier_add_1024` | 0.01326 | ±1.932e-05 | 140 | 0.01893 | ±6.899e-05 | 50 | 0.04363 | ±1.038e-05 | 84 |
| `cocks_keygen_1024` | 14.65 | ±0.2535 | 50 | 18.78 | ±0.05254 | 80 | 34.66 | ±0.06805 | 50 |
| `cocks_encrypt_1024` | 0.932 | ±0.002291 | 80 | 1.352 | ±0.003914 | 50 | 2.795 | ±0.01873 | 50 |
| `cocks_decrypt_1024` | 0.169 | ±0.001759 | 50 | 0.2144 | ±0.001535 | 50 | 0.403 | ±0.0002062 | 56 |
| `rabin_keygen_1024` | 21.09 | ±0.1053 | 80 | 27.26 | ±0.08139 | 50 | 50.07 | ±0.6358 | 50 |
| `rabin_encrypt_1024` | 0.03663 | ±0.0001557 | 170 | 0.04874 | ±0.0001494 | 50 | 0.07455 | ±5.053e-05 | 54 |
| `rabin_decrypt_1024` | 0.3147 | ±0.001467 | 110 | 0.4074 | ±0.00123 | 52 | 0.7914 | ±0.01408 | 50 |
| `schmidt_samoa_keygen_1024` | 7.594 | ±0.1311 | 81 | 8.758 | ±0.2689 | 50 | 14.73 | ±0.1504 | 115 |
| `schmidt_samoa_encrypt_1024` | 0.9146 | ±0.001267 | 234 | 1.335 | ±0.00233 | 50 | 2.763 | ±0.002222 | 110 |
| `schmidt_samoa_decrypt_1024` | 0.2582 | ±0.01078 | 51 | 0.3853 | ±0.005221 | 50 | 0.8067 | ±0.004842 | 50 |
### RSA (2048-bit)
| `rsa_keygen_2048` | 287 | ±3.338 | 110 | 415 | ±1.251 | 110 | 850.5 | ±2.95 | 52 |
| `rsa_encrypt_2048` | 0.1306 | ±0.0005355 | 110 | 0.186 | ±0.0004 | 145 | 0.3113 | ±0.0001877 | 80 |
| `rsa_decrypt_2048` | 1.784 | ±0.01143 | 50 | 2.555 | ±0.006951 | 50 | 5.307 | ±0.002826 | 52 |
| `rsa_sign_2048` | 1.798 | ±0.05595 | 50 | 2.546 | ±0.006619 | 80 | 5.308 | ±0.01734 | 50 |
| `rsa_verify_2048` | 0.1311 | ±0.0007617 | 50 | 0.1858 | ±0.0005796 | 50 | 0.3128 | ±0.005454 | 50 |
### ECDSA / ECDH (P-256)
| `ecdsa_keygen` | 1.99 | ±0.006535 | 50 | 2.437 | ±0.008313 | 80 | 3.611 | ±0.03197 | 50 |
| `ecdsa_sign` | 2.166 | ±0.005612 | 52 | 2.704 | ±0.00652 | 50 | 3.937 | ±0.01097 | 50 |
| `ecdsa_verify` | 4.027 | ±0.01481 | 50 | 4.977 | ±0.007851 | 50 | 7.31 | ±0.02331 | 50 |
| `ecdh_keygen` | 1.988 | ±0.006667 | 50 | 2.438 | ±0.005295 | 50 | 3.614 | ±0.03119 | 50 |
| `ecdh_agree` | 2.059 | ±0.03056 | 50 | 2.511 | ±0.006395 | 50 | 3.727 | ±0.03323 | 50 |
| `ecdh_serialize` | 0.0001034 | ±2.911e-06 | 50 | 7.726e-05 | ±6.443e-06 | 101 | 0.0001118 | ±3.597e-06 | 58 |
### ECIES / EC ElGamal (P-256)
| `ecies_keygen` | 1.992 | ±0.004656 | 110 | 2.446 | ±0.008863 | 140 | 3.611 | ±0.03745 | 50 |
| `ecies_encrypt` | 3.939 | ±0.0117 | 50 | 4.835 | ±0.008796 | 50 | 7.184 | ±0.09285 | 50 |
| `ecies_decrypt` | 1.954 | ±0.003838 | 80 | 2.39 | ±0.004669 | 85 | 3.569 | ±0.0409 | 50 |
| `ec_elgamal_keygen` | 1.985 | ±0.004116 | 55 | 2.432 | ±0.01028 | 50 | 3.611 | ±0.01945 | 50 |
| `ec_elgamal_encrypt` | 4.067 | ±0.02335 | 55 | 4.973 | ±0.01116 | 50 | 7.394 | ±0.02841 | 50 |
| `ec_elgamal_decrypt` | 1.993 | ±0.005386 | 50 | 2.452 | ±0.008972 | 110 | 3.633 | ±0.02232 | 50 |
### Ed25519 / Edwards DH / Edwards ElGamal
| `ed25519_keygen` | 2.03 | ±0.007771 | 50 | 2.515 | ±0.008311 | 170 | 3.806 | ±0.00763 | 54 |
| `ed25519_sign` | 1.102 | ±0.005711 | 110 | 1.264 | ±0.00618 | 50 | 1.914 | ±0.01796 | 50 |
| `ed25519_verify` | 3.33 | ±0.008169 | 50 | 4.126 | ±0.008502 | 52 | 6.246 | ±0.009423 | 50 |
| `edwards_dh_keygen` | 2.035 | ±0.04036 | 84 | 2.477 | ±0.004674 | 50 | 3.763 | ±0.006105 | 50 |
| `edwards_dh_agree` | 1.002 | ±0.002051 | 80 | 1.237 | ±0.003963 | 142 | 1.878 | ±0.001706 | 52 |
| `edwards_dh_serialize` | 7.426e-05 | ±2.856e-06 | 140 | 7.204e-05 | ±4.252e-06 | 50 | 7.718e-05 | ±1.852e-06 | 50 |
| `edwards_elgamal_keygen` | 2.015 | ±0.009597 | 110 | 2.466 | ±0.003901 | 50 | 3.791 | ±0.04686 | 50 |
| `edwards_elgamal_encrypt` | 2.108 | ±0.01187 | 50 | 2.591 | ±0.00472 | 50 | 3.94 | ±0.0231 | 50 |
| `edwards_elgamal_decrypt` | 1.591 | ±0.002425 | 80 | 1.935 | ±0.005726 | 80 | 3.011 | ±0.01664 | 50 |
### X25519 / X448 (RFC 7748)
| `x25519_keygen` | 0.03499 | ±1.501e-05 | 110 | 0.06482 | ±0.0001863 | 50 | 0.2165 | ±0.002467 | 118 |
| `x25519_agree` | 0.03416 | ±1.434e-05 | 140 | 0.06353 | ±0.0001281 | 51 | 0.2152 | ±0.002508 | 110 |
| `x25519_scalar_mult_base` | 0.03417 | ±1.659e-05 | 110 | 0.06361 | ±0.0001665 | 110 | 0.2152 | ±0.002812 | 50 |
| `x25519_scalar_mult` | 0.03417 | ±2.761e-05 | 110 | 0.06367 | ±0.000157 | 59 | 0.2143 | ±7.422e-05 | 115 |
| `x448_keygen` | 0.2373 | ±0.0008337 | 80 | 0.3623 | ±0.0005169 | 50 | 1.086 | ±0.01023 | 50 |
| `x448_agree` | 0.2362 | ±0.0001385 | 80 | 0.3629 | ±0.005327 | 50 | 1.084 | ±0.01125 | 50 |
| `x448_scalar_mult_base` | 0.2362 | ±0.0001154 | 80 | 0.3613 | ±0.0006512 | 50 | 1.087 | ±0.01268 | 88 |
| `x448_scalar_mult` | 0.2362 | ±0.0001106 | 140 | 0.3612 | ±0.0006672 | 50 | 1.084 | ±0.01125 | 50 |
### ML-KEM (Kyber)
| `mlkem512_keygen` | 0.01703 | ±7.847e-05 | 50 | 0.02536 | ±5.482e-05 | 80 | 0.05291 | ±0.0002066 | 50 |
| `mlkem512_encaps` | 0.01612 | ±2.431e-05 | 50 | 0.02665 | ±0.0006511 | 50 | 0.05284 | ±0.0001552 | 116 |
| `mlkem512_decaps` | 0.01639 | ±1.773e-05 | 80 | 0.02991 | ±0.0003324 | 50 | 0.05606 | ±0.0001612 | 142 |
| `mlkem768_keygen` | 0.02779 | ±7.227e-05 | 50 | 0.04218 | ±0.0003994 | 110 | 0.08631 | ±0.0003145 | 50 |
| `mlkem768_encaps` | 0.02594 | ±2.154e-05 | 296 | 0.0419 | ±0.0001054 | 50 | 0.08728 | ±0.0003385 | 80 |
| `mlkem768_decaps` | 0.02654 | ±9.472e-05 | 110 | 0.04684 | ±9.01e-05 | 50 | 0.0914 | ±0.0002707 | 50 |
| `mlkem1024_keygen` | 0.0439 | ±6.454e-05 | 80 | 0.06593 | ±0.0007346 | 50 | 0.137 | ±0.0004361 | 50 |
| `mlkem1024_encaps` | 0.03975 | ±6.822e-05 | 80 | 0.06373 | ±0.0002676 | 50 | 0.1364 | ±0.0004771 | 50 |
| `mlkem1024_decaps` | 0.04065 | ±3.935e-05 | 110 | 0.07061 | ±0.0001176 | 56 | 0.1428 | ±0.0003117 | 110 |
### ML-DSA (Dilithium)
| `mldsa44_keygen` | 0.06385 | ±0.0001583 | 50 | 0.09451 | ±0.0001812 | 110 | 0.2791 | ±0.0003247 | 320 |
| `mldsa44_sign` | 0.1572 | ±7.018e-05 | 50 | 0.3812 | ±0.001324 | 50 | 0.5605 | ±0.0003453 | 80 |
| `mldsa44_verify` | 0.01678 | ±4.972e-05 | 111 | 0.03896 | ±0.000121 | 80 | 0.06158 | ±0.0008716 | 50 |
| `mldsa65_keygen` | 0.1179 | ±0.0002556 | 80 | 0.1692 | ±0.001079 | 110 | 0.3735 | ±0.0008862 | 50 |
| `mldsa65_sign` | 0.2659 | ±0.0004532 | 50 | 0.6691 | ±0.001186 | 80 | 0.9478 | ±0.003496 | 50 |
| `mldsa65_verify` | 0.02498 | ±0.0003743 | 50 | 0.05598 | ±0.0001238 | 591 | 0.08762 | ±0.000738 | 140 |
| `mldsa87_keygen` | 0.1726 | ±0.0002285 | 410 | 0.2448 | ±0.0007543 | 50 | 0.596 | ±0.0009311 | 52 |
| `mldsa87_sign` | 0.168 | ±0.0001701 | 170 | 0.4168 | ±0.001193 | 52 | 0.5985 | ±0.0009905 | 170 |
| `mldsa87_verify` | 0.03707 | ±0.000108 | 110 | 0.08329 | ±0.0002082 | 80 | 0.1371 | ±0.000586 | 110 |
### NTRU (NIST PQC round 3)
| `ntruhps509_keygen` | 1.002 | ±0.04583 | 50 | 1.278 | ±0.002711 | 58 | 2.296 | ±0.002031 | 50 |
| `ntruhps509_encaps` | 0.08268 | ±0.003083 | 50 | 0.1069 | ±0.0004561 | 50 | 0.1729 | ±0.0003729 | 80 |
| `ntruhps509_decaps` | 0.1455 | ±0.0106 | 50 | 0.156 | ±0.001382 | 110 | 0.2853 | ±0.001599 | 50 |
| `ntruhps677_keygen` | 1.219 | ±0.01126 | 320 | 1.798 | ±0.007316 | 53 | 3.042 | ±0.004702 | 87 |
| `ntruhps677_encaps` | 0.08707 | ±0.0007503 | 50 | 0.1333 | ±0.001523 | 50 | 0.2079 | ±0.0009669 | 50 |
| `ntruhps677_decaps` | 0.1052 | ±0.002519 | 80 | 0.1591 | ±0.000419 | 260 | 0.2803 | ±0.0004506 | 80 |
| `ntruhps821_keygen` | 2.366 | ±0.07834 | 50 | 2.814 | ±0.01177 | 50 | 5.117 | ±0.008321 | 80 |
| `ntruhps821_encaps` | 0.1762 | ±0.009012 | 80 | 0.1937 | ±0.0004173 | 80 | 0.3065 | ±0.0007033 | 50 |
| `ntruhps821_decaps` | 0.3096 | ±0.01153 | 52 | 0.279 | ±0.0006073 | 80 | 0.4914 | ±0.001814 | 140 |
| `ntruhrss701_keygen` | 1.28 | ±0.09556 | 50 | 1.904 | ±0.01062 | 54 | 3.57 | ±0.007388 | 50 |
| `ntruhrss701_encaps` | 0.04845 | ±0.001142 | 50 | 0.06901 | ±0.004323 | 50 | 0.1175 | ±0.0003305 | 50 |
| `ntruhrss701_decaps` | 0.1234 | ±0.002806 | 54 | 0.1691 | ±0.0004149 | 112 | 0.3118 | ±0.0004736 | 85 |
### NTRUEncrypt (IEEE Std 1363.1-2008)
| `ntruees401ep1_keygen` | 0.7645 | ±0.005618 | 140 | 0.9366 | ±0.002872 | 50 | 1.343 | ±0.02541 | 53 |
| `ntruees401ep1_encrypt` | 0.1008 | ±0.001312 | 171 | 0.1091 | ±0.004532 | 80 | 0.2898 | ±0.0009549 | 80 |
| `ntruees401ep1_decrypt` | 0.1385 | ±0.0003355 | 177 | 0.1582 | ±0.00105 | 81 | 0.4851 | ±0.002529 | 170 |
| `ntruees443ep1_keygen` | 0.7524 | ±0.02073 | 80 | 0.862 | ±0.02262 | 50 | 1.308 | ±0.002847 | 110 |
| `ntruees443ep1_encrypt` | 0.04354 | ±0.0005677 | 110 | 0.04403 | ±0.0003447 | 80 | 0.07591 | ±0.0006669 | 260 |
| `ntruees443ep1_decrypt` | 0.04295 | ±0.0004111 | 57 | 0.04917 | ±0.0003307 | 50 | 0.105 | ±0.0005188 | 50 |
| `ntruees449ep1_keygen` | 0.9509 | ±0.01351 | 50 | 1.113 | ±0.00314 | 50 | 1.624 | ±0.001674 | 50 |
| `ntruees449ep1_encrypt` | 0.1449 | ±0.0002934 | 140 | 0.1545 | ±0.003887 | 50 | 0.3414 | ±0.0007413 | 50 |
| `ntruees449ep1_decrypt` | 0.1771 | ±0.002304 | 50 | 0.1991 | ±0.001424 | 382 | 0.4925 | ±0.0007877 | 50 |
| `ntruees541ep1_keygen` | 0.7433 | ±0.002493 | 53 | 1.044 | ±0.02334 | 50 | 1.375 | ±0.002495 | 50 |
| `ntruees541ep1_encrypt` | 0.07745 | ±0.000181 | 50 | 0.07411 | ±0.0007479 | 112 | 0.1992 | ±0.0009162 | 561 |
| `ntruees541ep1_decrypt` | 0.09241 | ±0.001237 | 52 | 0.0998 | ±0.0008372 | 140 | 0.2991 | ±0.0006596 | 50 |
| `ntruees677ep1_keygen` | 1.203 | ±0.01825 | 50 | 1.585 | ±0.009279 | 50 | 2.373 | ±0.001855 | 140 |
| `ntruees677ep1_encrypt` | 0.1819 | ±0.002014 | 140 | 0.2004 | ±0.002973 | 55 | 0.464 | ±0.003442 | 50 |
| `ntruees677ep1_decrypt` | 0.2825 | ±0.002102 | 80 | 0.3248 | ±0.003849 | 50 | 0.828 | ±0.0008432 | 50 |
| `ntruees1087ep1_keygen` | 1.76 | ±0.005377 | 176 | 2.649 | ±0.06399 | 50 | 3.701 | ±0.002887 | 80 |
| `ntruees1087ep1_encrypt` | 0.141 | ±0.0002315 | 260 | 0.1547 | ±0.0009354 | 50 | 0.3323 | ±0.0005843 | 110 |
| `ntruees1087ep1_decrypt` | 0.1835 | ±0.0003071 | 230 | 0.2254 | ±0.0009941 | 80 | 0.564 | ±0.00616 | 80 |
| `ntruees1087ep2_keygen` | 1.869 | ±0.007425 | 110 | 2.764 | ±0.007286 | 56 | 3.843 | ±0.002942 | 140 |
| `ntruees1087ep2_encrypt` | 0.2148 | ±0.0006427 | 50 | 0.244 | ±0.0009286 | 50 | 0.5671 | ±0.001289 | 201 |
| `ntruees1087ep2_decrypt` | 0.3325 | ±0.001221 | 209 | 0.3946 | ±0.001984 | 80 | 1.014 | ±0.001434 | 80 |
| `ntruees1499ep1_keygen` | 3.114 | ±0.01771 | 170 | 4.314 | ±0.01051 | 80 | 7.366 | ±0.004614 | 147 |
| `ntruees1499ep1_encrypt` | 0.211 | ±0.000396 | 233 | 0.2415 | ±0.001777 | 80 | 0.5393 | ±0.001081 | 50 |
| `ntruees1499ep1_decrypt` | 0.3017 | ±0.0002866 | 50 | 0.3725 | ±0.002614 | 50 | 0.9457 | ±0.008248 | 50 |
Cross-platform summary Kiviat diagrams (radar charts; log-radial ops/sec
axis, outer ring = faster):


The integer-arithmetic chart above plots ops/sec for the mixed integer-based
public-key schemes (RSA, DSA, ECDSA, ECDH, Ed25519, X25519, X448).
Signature-only and rerandomization/addition rows stay in the tables because
they do not have matching encrypt/decrypt axes.
The post-quantum chart blends representative axes from ML-KEM, ML-DSA, and
NTRU. Per-scheme breakdown radars live in
[POSTQUANTUM.md](POSTQUANTUM.md).
## Practical Guidance
- Use `RSA` when you need standards-backed encryption or signatures.
- Use `DSA`, `ECDSA`, or `Ed25519` when you need a standards-backed digital signature.
- Use `ECIES` when you need public-key encryption over an elliptic curve.
- Use `ECDH` or `DH` when you need key agreement without a full encryption layer.
- Use the other implemented schemes when you explicitly want those primitives
and understand their wrapper model.
- Use `CtrDrbgAes256` (or another strong `Csprng`) for all randomized public-key
operations.
- Keep an eye on 2048-bit and larger timings; the in-tree bigint backend is
respectable but not a tuned industrial multiprecision library. The crate-wide
policy is to keep the arithmetic kernels pure Rust and in-tree.
## References
The primary public-key papers and standards are stored in `pubs/`. The BibTeX
index is in [README.md](README.md).