Crypto recipe

Digital signatures with Ed25519

Sign and verify data with Ed25519 — the modern, footgun-free signature scheme.

A digital signature proves a message came from the holder of a private key, and anyone with the matching public key can verify it. Unlike an HMAC, the verifier needs no secret — which makes signatures the tool for software releases, tokens (JWT), certificates, and any public verification.

Ed25519 is the modern default: small keys, fast, and free of the parameter footguns of RSA and ECDSA. Always verify before you trust signed data.

Get it right

  • Prefer Ed25519 (EdDSA). If you must use RSA, use RSA-PSS, not PKCS#1 v1.5.
  • Keep the private (signing) key secret; distribute only the public key.
  • A signature proves authenticity to anyone — unlike a shared-key MAC.
  • Always check the verify result before acting on signed data.

Implementation

Setup Standard library (crypto/ed25519).

Go
package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"fmt"
)

func main() {
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}

	message := []byte("ship release v2.0")
	signature := ed25519.Sign(priv, message) // 64 bytes

	valid := ed25519.Verify(pub, message, signature)
	fmt.Println("valid:", valid)
}

Setup pip install cryptography.

Python
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.exceptions import InvalidSignature

private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()

message = b"ship release v2.0"
signature = private_key.sign(message)

try:
    public_key.verify(signature, message)  # raises on failure
    print("valid")
except InvalidSignature:
    print("invalid")

Setup Built in (node:crypto; Ed25519 since Node 12).

Node.js
import { generateKeyPairSync, sign, verify } from 'node:crypto';

const { publicKey, privateKey } = generateKeyPairSync('ed25519');

const message = Buffer.from('ship release v2.0');
// For Ed25519 the algorithm argument is null (the curve fixes the hash).
const signature = sign(null, message, privateKey); // 64 bytes

// verify(algorithm, data, key, signature) — the public key comes before the signature.
const valid = verify(null, message, publicKey, signature);

Setup dotnet add package NSec.Cryptography (libsodium-based). .NET has no built-in Ed25519; BouncyCastle is the other common choice.

.NET
using NSec.Cryptography;
using System.Text;

var algorithm = SignatureAlgorithm.Ed25519;
using Key key = Key.Create(algorithm);

byte[] message = Encoding.UTF8.GetBytes("ship release v2.0");
byte[] signature = algorithm.Sign(key, message); // 64 bytes

bool valid = algorithm.Verify(key.PublicKey, message, signature);

Setup libsodium (crypto_sign is Ed25519).

C++
#include <sodium.h>
#include <string>

int main() {
    if (sodium_init() < 0) return 1;

    unsigned char pk[crypto_sign_PUBLICKEYBYTES]; // 32
    unsigned char sk[crypto_sign_SECRETKEYBYTES]; // 64
    crypto_sign_keypair(pk, sk);

    std::string msg = "ship release v2.0";
    unsigned char sig[crypto_sign_BYTES];         // 64
    crypto_sign_detached(sig, nullptr,
        reinterpret_cast<const unsigned char *>(msg.data()), msg.size(), sk);

    bool valid = crypto_sign_verify_detached(sig,
        reinterpret_cast<const unsigned char *>(msg.data()), msg.size(), pk) == 0;
}

Setup Built in since Java 15 (EdDSA, JEP 339).

Java
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;

KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
KeyPair pair = kpg.generateKeyPair();

byte[] message = "ship release v2.0".getBytes();

Signature signer = Signature.getInstance("Ed25519");
signer.initSign(pair.getPrivate());
signer.update(message);
byte[] signature = signer.sign();

Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(pair.getPublic());
verifier.update(message);
boolean valid = verifier.verify(signature);

Setup Cargo.toml: ed25519-dalek = { version = "2", features = ["rand_core"] }, rand = "0.8".

Rust
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand::rngs::OsRng;

fn main() {
    let signing_key = SigningKey::generate(&mut OsRng);
    let verifying_key: VerifyingKey = signing_key.verifying_key();

    let message = b"ship release v2.0";
    let signature: Signature = signing_key.sign(message);

    let valid = verifying_key.verify(message, &signature).is_ok();
}