A pure Erlang library containing elliptic curve cryptography utilities for Ed25519, Ed448, and secp256k1.
This library is intended for learning, experimentation, and as a portable
pure-Erlang fallback when native C/Rust NIFs (such as libsodium/enacl or
native secp256k1 bindings) are unavailable.
# Compile
$ rebar3 compile
# EUnit test suite
$ rebar3 eunit
# Dialyzer static analysis
$ rebar3 dialyzer
# QuickCheck property-based testing
$ rebar3 as test eqcType specifications across all source modules are validated using eqWAlizer via the Erlang Language Platform (ELP):
# Type-check all source modules
$ elp eqwalize-all --as default
# Or using mise
$ mise run eqwalize
# Or type-check a specific module
$ elp eqwalize ecu_secp256k1%% Keypair generation, signing, and detached verification
{SecretKey, PublicKey} = ecu_eddsa:sign_keypair(),
Signature = ecu_eddsa:sign_detached(<<"Hello, world!">>, SecretKey),
true = ecu_eddsa:sign_verify_detached(Signature, <<"Hello, world!">>, PublicKey).
%% Batch verification of multiple signatures in a single pass (RFC 8032 §5.1.7)
Items = [
{Sig1, Msg1, PK1},
{Sig2, Msg2, PK2},
{Sig3, Msg3, PK3}
],
true = ecu_eddsa:sign_verify_batch(Items).%% Keypair generation with 57-byte public key
{SecretKey, PublicKey} = ecu_eddsa448:sign_keypair(),
Msg = <<"High-security payload">>,
%% Sign and verify 114-byte signature with optional context string
Context = <<"custom-app-v1">>,
Signature = ecu_eddsa448:sign_detached(Msg, SecretKey, Context),
true = ecu_eddsa448:sign_verify_detached(Signature, Msg, PublicKey, Context).
%% Batch verification of multiple Ed448 signatures in a single pass
Batch = [
{SigA, MsgA, PKA, ContextA},
{SigB, MsgB, PKB, ContextB}
],
true = ecu_eddsa448:sign_verify_batch(Batch).%% Generate keypair with 32-byte x-only public key
{SecretKey, PublicKey} = ecu_schnorr:sign_keypair(),
Msg = <<"Bitcoin Taproot transaction">>,
%% Sign and verify 64-byte signature
Signature = ecu_schnorr:sign(SecretKey, Msg),
true = ecu_schnorr:verify(Signature, Msg, PublicKey).%% Trusted dealer keygen for a 2-of-3 BIP-340 threshold group
{Shares, GroupPK, _VSS} = ecu_frost:trusted_dealer_keygen(bip340, 2, 3),
Msg = <<"Threshold signed payload">>,
%% Round 1: Any 2 participants commit nonces
[{1, S1}, {2, S2} | _] = Shares,
{N1, {HC1, BC1}} = ecu_frost:commit(bip340, S1),
{N2, {HC2, BC2}} = ecu_frost:commit(bip340, S2),
Commitments = [{1, HC1, BC1}, {2, HC2, BC2}],
%% Round 2: Participants compute partial signature shares
Z1 = ecu_frost:sign(bip340, 1, S1, GroupPK, N1, Msg, Commitments),
Z2 = ecu_frost:sign(bip340, 2, S2, GroupPK, N2, Msg, Commitments),
%% Aggregate shares into a standard 64-byte BIP-340 Taproot signature
Signature = ecu_frost:aggregate(bip340, Commitments, Msg, GroupPK, [Z1, Z2]),
true = ecu_schnorr:verify(Signature, Msg, GroupPK).%% X25519 (RFC 7748)
{PubA, PrivA} = ecu_ecdh:keypair(x25519),
{PubB, PrivB} = ecu_ecdh:keypair(x25519),
SharedSecret = ecu_ecdh:compute_key(x25519, PubB, PrivA),
SharedSecret = ecu_ecdh:compute_key(x25519, PubA, PrivB).
%% X448 (RFC 7748 Goldilocks)
{PubA448, PrivA448} = ecu_ecdh:keypair(x448),
{PubB448, PrivB448} = ecu_ecdh:keypair(x448),
Shared448 = ecu_ecdh:compute_key(x448, PubB448, PrivA448).
%% Ed25519 to X25519 conversion (birational equivalence)
XPub = ecu_ecdh:ed25519_public_to_x25519(Ed25519PubKey),
XPriv = ecu_ecdh:ed25519_secret_to_x25519(Ed25519SecretKey).ecu_eddsa: High-level Ed25519 digital signature API matching standardlibsodiumconventions:sign_keypair/0: Generate a fresh{secret, public}keypair.sign_keypair_from_seed/1: Deterministically derive a keypair from a 32-byte seed.sign/2,sign_open/2: Create and open combined signed messages.sign_detached/2,sign_verify_detached/3: Create and verify detached signatures.sign_verify_batch/1: Batch signature verification verifying a list of{Sig, Msg, PK}tuples in a single pass (RFC 8032 §5.1.7).
ecu_ed25519: Curve point arithmetic and group operations using extended homogeneous coordinates ({X, Y, Z, T}):- Point addition (
p_add/2), subtraction (p_sub/2), doubling (p_dbl/1), and negation (p_neg/1). - Base-point scalar multiplication (
scalar_mul_base/1,scalar_mul_base_noclamp/1) using a precomputed comb table. - Variable-base scalar multiplication (
scalar_mul/2,scalar_mul_noclamp/2) using a fixed 4-bit window method. - Dual-scalar multiplication (
scalar_mul_dual_base/3) for verification. - Multi-scalar multiplication (
multi_scalar_mul/1) using interleaved windowed Straus accumulation for accelerated batch operations. - Point compression (
compress/1) and decompression (decompress/1) with canonical encoding checks and small-order point detection (is_small_order/1).
- Point addition (
-
ecu_eddsa448: High-level Ed448 digital signature API (RFC 8032) using SHAKE256:-
sign_keypair/0: Generate a fresh 57-byte{secret, public}keypair. -
sign_seed_keypair/1: Deterministically derive a keypair from a 57-byte seed. -
sign/2,3,sign_open/2,3: Create and open combined signed messages with optional context string ($0 \le |C| \le 255$ ). -
sign_detached/2,3,sign_verify_detached/3,4: Create and verify 114-byte detached signatures with optional context string. -
sign_verify_batch/1,2: Batch signature verification verifying a list of{Sig, Msg, PK}or{Sig, Msg, PK, Context}tuples in a single pass.
-
-
ecu_ed448: Curve point arithmetic and group operations using projective coordinates ({X, Y, Z}with$x = X/Z, y = Y/Z$ ):- Point addition (
p_add/2), subtraction (p_sub/2), doubling (p_dbl/1), and negation (p_neg/1) using complete Edwards formulas. - Base-point scalar multiplication (
scalar_mul_base/1,scalar_mul_base_noclamp/1) using a 114-window precomputed comb table. - Variable-base scalar multiplication (
scalar_mul/2,scalar_mul_noclamp/2) using a fixed 4-bit window method. - Dual-scalar multiplication (
scalar_mul_dual_base/3) for signature verification. - Multi-scalar multiplication (
multi_scalar_mul/1) using interleaved windowed Straus accumulation. - Point compression (
compress/1) and decompression (decompress/1) with canonical encoding checks and small-order detection (is_small_order/1).
- Point addition (
ecu_ecdsa: ECDSA key derivation, signing, and verification on secp256k1:private_to_public/2: Derive compressed public key from a private key with strict range checking (1 <= Priv < N).sign/3: Sign message hash with a private key.verify/4: Verify signature against message hash and public key.
ecu_secp256k1: Curve arithmetic in prime fieldPand group orderN:- Point operations internally accelerated using Jacobian coordinates
(
{X, Y, Z}withx = X/Z^2, y = Y/Z^3). - Scalar multiplication (
scalar_mul/2,scalar_mul_base/1). - Double-scalar multiplication (
scalar_mul_dual/4) using Shamir's trick. - Compressed point encoding (
compress/1,decompress/1).
- Point operations internally accelerated using Jacobian coordinates
(
ecu_crypto: High-level cryptocurrency utilities:- Ethereum message signing and address recovery (
eth_msg_sign/2,eth_msg_recover/2,ec_recover/2). - Bitcoin address derivation (
private_to_short/2).
- Ethereum message signing and address recovery (
-
ecu_schnorr: Complete BIP-340 64-byte Schnorr signature implementation:-
sign_keypair/0: Generate a fresh{secret, public}keypair with 32-byte$x$ -only public key. -
sign_keypair_from_secret/1: Derive 32-byte$x$ -only public key from a 32-byte secret key (negating the scalar if$Y$ is odd). -
sign/2,sign/3: Sign arbitrary-length message with deterministic or auxiliary randomness. -
verify/3: Verify 64-byte signature against message and 32-byte$x$ -only public key. -
lift_x/1: Lift 32-byte$X$ coordinate to secp256k1 curve point with even$Y$ . -
tagged_hash/2: Compute BIP-340 tagged hashes (BIP0340/challenge,BIP0340/aux,BIP0340/nonce).
-
-
ecu_frost: Two-round FROST threshold signature scheme supporting ciphersuites:-
ed25519:FROST(Ed25519, SHA-512)(default) producing standard 64-byte Ed25519 signatures. -
ed448:FROST(Ed448, SHAKE256)producing standard 114-byte Ed448 signatures ($R \parallel z$ ) verified withecu_eddsa448:sign_verify_detached/4. -
secp256k1:FROST(secp256k1, SHA-256)using RFC 9380expand_message_xmdproducing 65-byte signatures ($R \parallel z$ ). -
bip340: BIP-340 Schnorr threshold signatures on secp256k1 producing 64-byte$x$ -only signatures ($r \parallel s$ ) with tagged hashes, verified viaecu_schnorr:verify/3. - API functions accept an optional
ciphersuite()prefix (ed25519 | secp256k1 | bip340 | ed448), defaulting toed25519:-
commit/1,2: Round 1 preprocessing — generate hiding & binding nonces and commitments. -
sign/6,7: Round 2 signing — compute partial signature share$z_i$ bound to group public key and message. -
verify_signature_share/7,8: Coordinator verification of individual signature shares. -
aggregate/4,5: Aggregate threshold signature shares into a group signature. -
verify/3,4: Verify aggregated FROST signatures (detached Ed25519, Ed448, secp256k1 prime-order verify, or BIP-340). -
trusted_dealer_keygen/2,3,4: Trusted dealer key generation ($t$ -of-$n$) with Feldman VSS commitments. -
vss_verify/3,4: Verifiable Secret Sharing share verification. -
secret_share_combine/1,2: Lagrange polynomial secret reconstruction from$t$ shares.
-
-
-
ecu_frost_dkg: Two-round Distributed Key Generation (DKG):-
round1/3,4,round1_check/4,5: Coefficient commitment and zero-knowledge Proof of Knowledge (PoK). -
round2/2,3,round2_check/3,4: Share distribution and verification against public commitments. -
compute_private_sign_share/1,2,3,compute_public_verification_share/1,2,3,compute_public_key/1,2: Joint key derivation.
-
-
ecu_ecdh: Diffie-Hellman key exchange and Montgomery curve functions:-
keypair/1: Fresh{PublicKey, PrivateKey}keypair generation (x25519,x448,secp256k1). -
keypair_from_seed/2: Deterministic keypair generation from seed bytes. -
compute_key/3: Key agreement returning shared secret binary (32bytes for X25519 and secp256k1,56bytes for X448). -
x25519/2: RFC 7748 §5 scalar multiplication over Montgomery Curve25519 ($u$ -coordinate). -
x448/2: RFC 7748 §5 scalar multiplication over Montgomery Curve448 ($u$ -coordinate). -
clamp/2: RFC 7748 scalar pruning/clamping. -
ed25519_public_to_x25519/1: Twisted Edwards public key conversion to Montgomery$u$ -coordinate via birational map$u = (1 + y) / (1 - y) \pmod p$ . -
ed25519_secret_to_x25519/1: Ed25519 secret key conversion via SHA-512 and clamping.
-
ecu_keccak: High-speed unrolled 64-bit Keccak-f[1600] permutation and sponge construction (zero external C dependencies):keccak256/1: Ethereum Keccak-256 hash function (zero padding delimiter).sha3_224/1,sha3_256/1,sha3_384/1,sha3_512/1: Standard NIST FIPS 202 SHA-3 hash functions.shake128/2,shake256/2: Keccak extendable-output functions (XOF).
Note
ecu_keccak exists to keep ec_utils fully self-contained with zero external C dependencies for Ethereum message signing and recovery (eth_msg_recover/2), address derivation, and Ed448/SHAKE256.
Do not use this library for standalone hash functions alone! If your application only requires high-throughput hashing or processes large volumes of data, use OTP's built-in crypto:hash/2 (for NIST SHA-3) or a dedicated C/Rust NIF (such as sha3), which are ~40×–70× faster.
Pure Erlang bignum arithmetic cannot match native assembly or C/Rust NIFs, but the library incorporates key algorithmic optimizations:
- Jacobian projective coordinates (secp256k1): Doubling and mixed additions require zero modular inversions; a single field inversion is computed at the end of scalar multiplication.
- Precomputed comb table (Ed25519 & secp256k1): Base-point scalar multiplication uses a
precomputed table of 1,024 points stored in
persistent_term(loaded via-on_load), taking at most 64 additions and 0 doublings. - Fixed 4-bit window (Ed25519): Variable-base scalar multiplication processes nibbles left-to-right with binary pattern matching, cutting additions roughly in half.
- Shamir's trick / Straus double-scalar multiplication: Verification and address recovery interleave two scalar multiplications into a single doubling chain, eliminating process spawning and message passing overhead.
| Operation | Baseline | Optimized | Speedup | Reference (Native NIF / OpenSSL) | vs Reference |
|---|---|---|---|---|---|
| secp256k1 scalar mul base | 5,377 µs | 166 µs | ~32× | 225 µs (crypto / OpenSSL) | 1.35× faster |
| secp256k1 scalar mul | — | 760 µs | — | 243 µs (crypto / OpenSSL) | 3.1× |
| secp256k1 eth_recover | 7,810 µs | 893 µs | ~8.7× | 54.1 µs (Rust NIF) | 16.5× |
| secp256k1 bip340 sign | — | 370 µs | — | 54.2 µs (k256 Rust NIF) | 6.8× |
| secp256k1 bip340 verify | — | 996 µs | — | 49.6 µs (k256 Rust NIF) | 19.9× |
| ed25519 scalar mul base | 746 µs | 122 µs | ~6.1× | 14.9 µs (enacl) | 8.2× |
| ed25519 scalar mul | 741 µs | 614 µs | ~1.2× | 71.8 µs (enacl) | 8.6× |
| ed25519 sign | 739 µs | 272 µs | ~2.7× | 16.1 µs (enacl) | 16.9× |
| ed25519 verify | 1,496 µs | 769 µs | ~1.9× | 44.2 µs (enacl) | 17.4× |
| ed25519 verify (batch of 32) | 846 µs/sig | 368 µs/sig | ~2.3× | — | — |
| ed25519 point add | 1.9 µs | 2.3 µs | ~1.0× | 12.6 µs (enacl) | 5.5× faster |
| ed448 scalar mul base | — | 492 µs | — | 61 µs (crypto / OpenSSL) | 8.1× |
| ed448 scalar mul | — | 1.80 ms | — | — | — |
| ed448 sign | — | 1.06 ms | — | 128 µs (crypto / OpenSSL) | 8.3× |
| ed448 verify | — | 2.46 ms | — | 154 µs (crypto / OpenSSL) | 16.0× |
| x25519 compute_key | — | 575 µs | — | 47.6 µs (enacl) / 63.2 µs (crypto) | 12.1× |
| x448 compute_key | — | 1.69 ms | — | 205 µs (crypto / OpenSSL) | 8.2× |
| frost ed25519 commit | — | 329 µs | — | 63.5 µs (frost-ed25519 Rust NIF) | 5.2× |
| frost ed25519 part sign | — | 2.12 ms | — | 102 µs (frost-ed25519 Rust NIF) | 20.8× |
| frost ed25519 aggregate | — | 2.32 ms | — | 183 µs (frost-ed25519 Rust NIF) | 12.7× |
| frost ed25519 verify | — | 929 µs | — | 99.9 µs (frost-ed25519 Rust NIF) | 9.3× |
| frost secp256k1 commit | — | 331 µs | — | 75.1 µs (frost-secp256k1 Rust NIF) | 4.4× |
| frost secp256k1 part sign | — | 2.50 ms | — | 150 µs (frost-secp256k1 Rust NIF) | 16.7× |
| frost secp256k1 aggregate | — | 2.64 ms | — | 245 µs (frost-secp256k1 Rust NIF) | 10.8× |
| frost secp256k1 verify | — | 1.02 ms | — | 117 µs (frost-secp256k1 Rust NIF) | 8.7× |
| frost bip340 commit | — | 351 µs | — | 75.7 µs (frost-secp256k1-tr Rust NIF) | 4.6× |
| frost bip340 part sign | — | 2.52 ms | — | 165 µs (frost-secp256k1-tr Rust NIF) | 15.3× |
| frost bip340 aggregate | — | 2.59 ms | — | 262 µs (frost-secp256k1-tr Rust NIF) | 9.9× |
| frost bip340 verify | — | 1.02 ms | — | 133 µs (frost-secp256k1-tr Rust NIF) | 7.7× |
| frost ed448 commit | — | 1.11 ms | — | 441 µs (frost-ed448 Rust NIF) | 2.5× |
| frost ed448 part sign | — | 5.95 ms | — | 560 µs (frost-ed448 Rust NIF) | 10.6× |
| frost ed448 aggregate | — | 6.00 ms | — | 1.10 ms (frost-ed448 Rust NIF) | 5.5× |
| frost ed448 verify | — | 2.38 ms | — | 691 µs (frost-ed448 Rust NIF) | 3.4× |
| keccak256 (Ethereum) | — | 81.9 µs | — | 0.35 µs (rustcrypto NIF) | 234× |
| sha3_256 (NIST) | — | 81.5 µs | — | 0.44 µs (crypto / OpenSSL) | 185× |
Notes:
- Pure Erlang point addition is faster than
enaclbecause calling the NIF incurs marshalling and point decompression overhead. - Pure Erlang
secp256k1base-point scalar multiplication is faster than OpenSSL due to the precomputed 1,024-entry comb table. - BIP-340 Schnorr and Keccak-256 reference benchmarks use an optional test NIF (
k256andsha3) buildable viamake rustcrypto_reference. - FROST threshold reference benchmarks use optional RFC 9591 test NIFs (Zcash Foundation
frost-corecrates) buildable viamake frost_reference. - Both reference NIFs can be built together using
make reference_nifs.
See ROADMAP.md for details on completed milestones and planned cryptographic extensions, including:
- Diffie-Hellman Key Exchange (X25519 / RFC 7748): Pure-Erlang Montgomery ladder on Curve25519.
- Ed448 Goldilocks Curve (RFC 8032 & RFC 7748): 448-bit Edwards curve arithmetic and SHAKE256
[Completed]. - FROST(Ed448, SHAKE256): Threshold signing for Ed448
[Completed]. - Pairing-Friendly Cryptography (BLS12-381): Primitives for threshold BLS signatures and ZK-SNARK verifiers.
Warning: Not Constant-Time
All arithmetic in this library is implemented in pure Erlang on top of the BEAM virtual machine. Point operations and scalar multiplications branch on secret scalar bits, and BEAM bignum operations take time that depends on the magnitude and values of operands.
Do NOT use this library for signing with high-value secret keys in environments where an attacker can observe precise execution timing or side-channels. In production environments handling sensitive private keys, use constant-time native implementations (such as
enacl/libsodiumor nativesecp256k1NIFs).