Skip to content

Ed25519 Signatures

Deep dive into Ed25519 cryptographic signatures.

What is Ed25519?

Ed25519 is a high-speed, high-security digital signature scheme using:

  • Curve: Edwards curve (Curve25519)
  • Hash: SHA-512
  • Key size: 32 bytes (256 bits)
  • Signature size: 64 bytes

Why Ed25519?

PropertyEd25519RSA-2048ECDSA-P256
Public key size32 bytes256 bytes64 bytes
Signature size64 bytes256 bytes64 bytes
Verification speed~70μs~30μs~150μs
Signing speed~50μs~1ms~150μs
Security level128-bit112-bit128-bit

Ed25519 offers the best balance of security, speed, and key/signature size.

Key Generation

JavaScript (tweetnacl)

javascript
import nacl from 'tweetnacl';

// Generate a random keypair
const keypair = nacl.sign.keyPair();

// Public key: 32 bytes → 64 hex characters
const publicKey = Buffer.from(keypair.publicKey).toString('hex');

// Secret key: 64 bytes (includes public key)
const secretKey = keypair.secretKey;

console.log('Public Key:', publicKey);
console.log('Public Key length:', publicKey.length); // 64

Python (PyNaCl)

python
from nacl.signing import SigningKey

# Generate a random keypair
key = SigningKey.generate()

# Public key: 32 bytes → 64 hex characters
public_key = key.verify_key.encode().hex()

print(f"Public Key: {public_key}")
print(f"Public Key length: {len(public_key)}")  # 64

Signing Process

Step 1: Create the Message

The message is a JSON object specific to the operation:

javascript
const message = {
  purpose: "registration",
  public_key: publicKey,
  profile: { /* ... */ },
  timestamp: Date.now()
};

Step 2: Canonical JSON

Convert to canonical form (keys sorted alphabetically, no whitespace):

javascript
function canonicalJson(obj) {
  if (obj === null) return 'null';
  if (typeof obj === 'undefined') return undefined;
  if (Array.isArray(obj)) {
    return '[' + obj.map(canonicalJson).join(',') + ']';
  }
  if (typeof obj === 'object') {
    const keys = Object.keys(obj).sort();
    const pairs = keys
      .map(k => {
        const v = canonicalJson(obj[k]);
        return v !== undefined ? `"${k}":${v}` : undefined;
      })
      .filter(p => p !== undefined);
    return '{' + pairs.join(',') + '}';
  }
  return JSON.stringify(obj);
}

Example transformation:

javascript
// Input
{ "name": "Alice", "age": 30, "city": null }

// Output (canonical)
{"age":30,"city":null,"name":"Alice"}

Step 3: Sign the Bytes

Sign the UTF-8 encoded canonical JSON:

javascript
const messageBytes = Buffer.from(canonicalJson(message));
const signatureBytes = nacl.sign.detached(messageBytes, secretKey);
const signature = Buffer.from(signatureBytes).toString('hex');

The signature is 64 bytes → 128 hex characters.

Verification Process

The server verifies signatures by:

  1. Reconstructing the expected message
  2. Converting to canonical JSON
  3. Verifying the signature with the public key
javascript
// Server-side verification
const messageBytes = Buffer.from(canonicalJson(expectedMessage));
const signatureBytes = Buffer.from(signature, 'hex');
const publicKeyBytes = Buffer.from(publicKey, 'hex');

const isValid = nacl.sign.detached.verify(
  messageBytes,
  signatureBytes,
  publicKeyBytes
);

Test Vectors

Use these to verify your implementation:

Test Vector 1: Simple Object

json
{
  "input": { "a": 1, "b": 2 },
  "canonical": "{\"a\":1,\"b\":2}",
  "public_key": "5f3d93f26e0cf7cf06b81d7fc7fb1e3b79d15c7b0d0c7f7c0d7c7f7c0d7c7f7c",
  "signature": "..."
}

Test Vector 2: Nested Object

json
{
  "input": { "z": { "b": 2, "a": 1 }, "a": "test" },
  "canonical": "{\"a\":\"test\",\"z\":{\"a\":1,\"b\":2}}",
  "public_key": "...",
  "signature": "..."
}

Common Mistakes

1. Non-canonical JSON

javascript
// Wrong: keys not sorted
JSON.stringify({ name: "Alice", age: 30 })
// Produces: {"name":"Alice","age":30}

// Correct: keys sorted
canonicalJson({ name: "Alice", age: 30 })
// Produces: {"age":30,"name":"Alice"}

2. Wrong Encoding

javascript
// Wrong: signing the string directly
nacl.sign.detached(message, secretKey);

// Correct: signing UTF-8 bytes
nacl.sign.detached(Buffer.from(message), secretKey);

3. Including undefined Values

javascript
// Wrong: undefined becomes "undefined" string
JSON.stringify({ a: undefined })

// Correct: undefined values should be omitted
canonicalJson({ a: undefined }) // Returns "{}"

Libraries

JavaScript/TypeScript

bash
npm install tweetnacl
javascript
import nacl from 'tweetnacl';

Python

bash
pip install pynacl
python
from nacl.signing import SigningKey, VerifyKey

Rust

toml
[dependencies]
ed25519-dalek = "2.0"
rust
use ed25519_dalek::{SigningKey, Signature, Verifier};

Go

bash
go get golang.org/x/crypto/ed25519
go
import "golang.org/x/crypto/ed25519"

The Registry Protocol for AI Agents