Skip to content
ecdsa.com
C

libsodium

Frank Denis's libsodium, the portable fork of NaCl: Ed25519 signatures with strict verification, X25519, authenticated encryption, and ML-KEM-768 plus the X-Wing hybrid KEM — but no ECDSA at all.
License
ISC
Checked version
1.0.22 ·
Source
github.com/jedisct1/libsodium
Documentation
doc.libsodium.org/

Compatibility at a glance

EdDSAPost-quantum

EdDSA
Ed25519Ed25519ph
Private key formats
seed-32expanded-64

libsodium offers a small, opinionated set of primitives behind hard-to-misuse functions, and it is bundled into PHP, used by many language bindings and embedded in messaging apps. Signatures are Ed25519 only: the headers of 1.0.22 (checked on the Alpine 3.24.2 package) declare no ECDSA function, so anything that speaks ES256 or secp256k1 needs another library.

Ed25519 signing is deterministic, as RFC 8032 specifies — two signatures of the same message were identical — and crypto_sign_*_ph functions implement Ed25519ph, whose signatures are not interchangeable with plain Ed25519: the plain verifier rejected one in the check. The secret key is 64 bytes, the 32-byte seed followed by the public key, while most other libraries store only the seed; crypto_sign_ed25519_sk_to_seed and crypto_sign_seed_keypair convert between the two, as the sample shows.

Verification is strict where other implementations differ. A signature with S replaced by S + L — the same value modulo the group order, forbidden by RFC 8032 — was rejected, and so was a signature checked against the small-order identity point as public key. Libraries that accept those inputs disagree with libsodium on edge cases, which matters for consensus systems that must agree on validity. crypto_sign_verify_detached takes no signature length and reads exactly 64 bytes, so check the length before calling it.

Post-quantum support in 1.0.22 is key encapsulation only: crypto_kem_mlkem768_* (1184-byte public keys, 1088-byte ciphertexts) and the hybrid X-Wing (crypto_kem_xwing_*, 1216 and 1120 bytes), which is also what the generic crypto_kem_* functions use by default. There is no ML-DSA or other post-quantum signature. Keys are raw bytes throughout — 32-byte public keys and seeds — with no PEM, DER or JWK support. Compare KEM sizes in the post-quantum KEM playground.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/libsodium/run.sh ran libsodium 1.0.22 in the container image alpine:3.24.2 on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA curves

    none

    No ECDSA: crypto_sign_PRIMITIVE is ed25519 and no header declares an ECDSA function.

    Recorded by the check
    fact ecdsa-curves = none
    evidence ecdsa-curves: crypto_sign_PRIMITIVE is "ed25519"; the header set declares no ECDSA functions
  • EdDSA

    Ed25519Ed25519ph

    Ed25519 through crypto_sign_* and Ed25519ph through crypto_sign_init, _update and _final_*; the plain verifier rejects Ed25519ph signatures.

    Recorded by the check
    fact eddsa = Ed25519, Ed25519ph
    evidence eddsa: crypto_sign_final_verify (Ed25519ph) accepts its signature: yes; the plain verifier rejects it: yes
  • Deterministic Ed25519 signatures

    yes

    Two signatures of the same message with the same key were identical.

    Recorded by the check
    fact ed25519-deterministic = yes
  • Ed25519 private key layout

    expanded-64

    crypto_sign_SECRETKEYBYTES is 64: the seed followed by the public key.

    Recorded by the check
    fact ed25519-private-key = expanded-64
    evidence ed25519-private-key: crypto_sign_SECRETKEYBYTES = 64: the 32-byte seed followed by the 32-byte public key (seed matches: yes)
  • Ed25519 signature with non-canonical S

    rejected

    A signature with S replaced by S + L was rejected.

    Recorded by the check
    fact ed25519-non-canonical-s = rejected
  • Ed25519 small-order public key

    rejected

    Verification against the identity point as public key was rejected.

    Recorded by the check
    fact ed25519-small-order-key = rejected
    evidence ed25519-small-order-key: the identity point as public key with R = identity, S = 0
  • Malformed Ed25519 signature

    rejected

    64 bytes of garbage made crypto_sign_verify_detached return -1.

    Recorded by the check
    fact ed25519-malformed-signature = rejected
    evidence ed25519-malformed-signature: 64 bytes of 0xab: crypto_sign_verify_detached returned -1; the function takes no length and reads exactly crypto_sign_BYTES = 64 bytes
  • Signature modes

    detachedcombined

    Detached signatures (64 bytes) and combined signed messages (signature prepended to the message) are both available.

    Recorded by the check
    fact signature-modes = detached, combined
    evidence signature-modes: crypto_sign prepends the 64-byte signature to the message: 82 bytes for a 18-byte message
  • Post-quantum algorithms

    ML-KEMX-Wing

    ML-KEM-768 (crypto_kem_mlkem768_*) and X-Wing (crypto_kem_xwing_*, the default of the generic crypto_kem_*) encapsulated and decapsulated; there are no post-quantum signatures.

    Recorded by the check
    fact pqc = ML-KEM, X-Wing
    evidence pqc: crypto_kem_mlkem768: public key 1184 bytes, ciphertext 1088 bytes
    evidence pqc: crypto_kem_xwing: public key 1216 bytes, ciphertext 1120 bytes; generic crypto_kem_primitive() is "xwing"
  • Public key formats

    raw-32

    Public keys are 32 raw bytes; there is no PEM, DER or JWK support.

    Recorded by the check
    fact public-key-formats = raw-32
  • Private key formats

    seed-32expanded-64

    Keys derive from a 32-byte seed (crypto_sign_seed_keypair) and are stored as 64 bytes; crypto_sign_ed25519_sk_to_seed recovers the seed.

    Recorded by the check
    fact private-key-formats = seed-32, expanded-64
    evidence private-key-formats: crypto_sign_seed_keypair reproduces the key from the seed: yes; crypto_sign_ed25519_sk_to_seed recovers it: yes

Usage

Generate an Ed25519 key, extract the 32-byte seed other libraries expect, sign and verify a detached signature.
#include <sodium.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    if (sodium_init() < 0) return 1;
    const unsigned char msg[] = "order #1042 approved";
    unsigned char pk[crypto_sign_PUBLICKEYBYTES], sk[crypto_sign_SECRETKEYBYTES];
    unsigned char sig[crypto_sign_BYTES], seed[32];

    crypto_sign_keypair(pk, sk);
    /* Other libraries store the 32-byte seed; libsodium's secret key is seed || public key. */
    crypto_sign_ed25519_sk_to_seed(seed, sk);

    /* Detached signature: 64 bytes, deterministic per RFC 8032. */
    crypto_sign_detached(sig, NULL, msg, sizeof msg - 1, sk);
    int ok = crypto_sign_verify_detached(sig, msg, sizeof msg - 1, pk) == 0;

    printf("secret key %d bytes, seed %zu bytes, signature %d bytes, verified: %d\n",
           crypto_sign_SECRETKEYBYTES, sizeof seed, crypto_sign_BYTES, ok);
    return 0;
}

The snippet harness runs every sample verbatim in the container image alpine:3.24.2, and the run fails unless the output matches.

Setup
apk add --no-cache -q build-base libsodium-dev=1.0.22-r0 >/dev/null
Command
cc -o usage usage.c -lsodium && ./usage
Output must match (regular expression)
secret key 64 bytes, seed 32 bytes, signature 64 bytes, verified: 1

Libraries