Files
skills/docs/security-stack/crypto/crypto-curve/src/primitives/ed25519/ed25519.md
2026-05-29 08:20:56 +01:00

6.3 KiB

Ed25519

(no submodules)

Purpose

Ed25519 is the Edwards-curve Digital Signature Algorithm defined in RFC 8032, the recommended signature scheme for new protocols. Compared with ECDSA it has deterministic signatures (no per-signature randomness needed), constant-time verification of well-formed inputs, small 32-byte public keys and 64-byte signatures, and formal soundness properties that make it the baseline choice for TLS, SSH, JWT signing, and peer-to-peer identity. This module exposes the primitive directly — key generation, signing, verification, and public-key derivation — as thin wrappers over @noble/curves/ed25519, with a paired native stub that throws PrimitiveNotImplemented. Selection between the two is controlled by crypto.config.ts at build time.

Features

  • getPublicKey(privateKey) → 32-byte compressed Edwards public key.
  • sign(message, privateKey) → 64-byte deterministic signature.
  • verify(signature, message, publicKey) → boolean that never throws for malformed inputs.
  • randomPrivateKey() → 32 random bytes (every 32-byte value is a valid Ed25519 secret; no retry loop needed).
  • RFC 8032 §7.1 vector 1 is covered in tests.
  • Native stub throws PrimitiveNotImplemented('ed25519') — flip to 'native' only after per-primitive audit (Phase 3).

Usage: Basic Example

import * as ed25519 from "@suaveplan/crypto-curve/ed25519";

const sk = ed25519.randomPrivateKey();
const pk = ed25519.getPublicKey(sk);
const msg = new TextEncoder().encode("hello");
const sig = ed25519.sign(msg, sk);
console.log(ed25519.verify(sig, msg, pk)); // true

Usage: Advanced Example

import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
import { zeroize } from "@suaveplan/crypto-utils/zeroize";

function signAndDiscardKey(msg: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
    const sk = ed25519.randomPrivateKey();
    try {
        const pk = ed25519.getPublicKey(sk);
        const sig = ed25519.sign(msg, sk);
        return { pk, sig };
    } finally {
        zeroize(sk);
    }
}

const { pk, sig } = signAndDiscardKey(new Uint8Array([1, 2, 3]));
console.log(pk.length, sig.length);

API Reference

  • getPublicKey(privateKey: Uint8Array): Uint8Array — 32-byte public key. Throws if privateKey.length !== 32.
  • sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array — 64-byte signature.
  • verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean — returns false instead of throwing on malformed inputs.
  • randomPrivateKey(): Uint8Array — 32 random bytes via @suaveplan/crypto-utils/random.

Implementation Notes

Noble's verify throws on some malformed inputs (bad-length public key, non-canonical signature encodings) rather than returning false. The noble impl wraps the call in a try/catch so every flavour exposes a boolean-only contract — CI's differential fuzz harness depends on that uniformity. Signing is deterministic, so the same (msg, sk) pair always produces the same signature: this is a deliberate property that makes replay/caching sound and prevents fault-attack reuse. For signing data larger than a few megabytes, streaming APIs are not offered here on purpose — callers should compose over the hash-then-sign idiom instead.