✍️ Ed25519 Edwards-curve Digital Signature
📋 Quick Navigation
📖 Overview
Ed25519 is a high-performance public-key signature system based on elliptic curve cryptography. It uses the Edwards-curve Digital Signature Algorithm (EdDSA) over the twisted Edwards curve edwards25519, designed to provide high security while avoiding common implementation pitfalls and offering excellent performance characteristics.
✨ Key Features
Fast Operations
Among the fastest signature algorithms available
Compact Keys
32-byte public and private keys, 64-byte signatures
Deterministic
No random number generator required for signing
High Security
128-bit security level against best known attacks
Side-channel Resistant
Designed to resist timing and cache attacks
Standardized
RFC 8032 and NIST SP 800-186 approved
Widely Adopted
Used in SSH, TLS, cryptocurrencies, and many protocols
Implementation Safe
Designed to avoid common cryptographic pitfalls
🎯 Common Use Cases
🔐 Protocol Integration
- SSH: ssh-ed25519 host and user keys
- TLS 1.3: Certificate and handshake signatures
- Signal Protocol: Identity key signatures
- Git: Commit and tag signing
💰 Blockchain & Identity
- Cryptocurrencies: Transaction signing (Monero, Stellar)
- Digital Identity: Self-sovereign identity systems
- Code Signing: Software package authentication
- Document Signing: Digital document authentication
Algorithm Parameters
| Parameter | Value |
|---|---|
| Security Level | 128 bits |
| Public Key Size | 32 bytes |
| Private Key Size | 32 bytes |
| Signature Size | 64 bytes |
| Hash Function | SHA-512 |
Usage Examples
Basic Signing and Verification
from metamui_crypto import Ed25519
# Generate a new keypair
keypair = Ed25519.generate_keypair()
# Sign a message
message = b"Hello, Ed25519!"
signature = Ed25519.sign(message, keypair.private_key)
# Verify the signature
is_valid = Ed25519.verify(signature, message, keypair.public_key)
print(f"Signature valid: {is_valid}")
Key Generation from Seed
from metamui_crypto import Ed25519
import hashlib
# Deterministic key generation from seed
seed = b"this is a 32 byte seed phrase!!!" # Must be 32 bytes
keypair = Ed25519.from_seed(seed)
# Same seed always produces same keypair
keypair2 = Ed25519.from_seed(seed)
assert keypair.public_key == keypair2.public_key
assert keypair.private_key == keypair2.private_key
# Derive seed from passphrase
passphrase = "my secure passphrase"
salt = b"application-specific-salt"
seed = hashlib.pbkdf2_hmac('sha256', passphrase.encode(), salt, 100000, 32)
keypair = Ed25519.from_seed(seed)
Batch Verification
from metamui_crypto import Ed25519
# Generate multiple signatures
messages = [f"Message {i}".encode() for i in range(100)]
keypairs = [Ed25519.generate_keypair() for _ in range(100)]
signatures = [
Ed25519.sign(msg, kp.private_key)
for msg, kp in zip(messages, keypairs)
]
# Batch verify (more efficient than individual verification)
public_keys = [kp.public_key for kp in keypairs]
all_valid = Ed25519.batch_verify(signatures, messages, public_keys)
print(f"All signatures valid: {all_valid}")
# Benchmark difference
import time
# Individual verification
start = time.time()
for sig, msg, pk in zip(signatures, messages, public_keys):
Ed25519.verify(sig, msg, pk)
individual_time = time.time() - start
# Batch verification
start = time.time()
Ed25519.batch_verify(signatures, messages, public_keys)
batch_time = time.time() - start
print(f"Individual: {individual_time:.3f}s, Batch: {batch_time:.3f}s")
print(f"Speedup: {individual_time / batch_time:.1f}x")
Message Signing with Context
from metamui_crypto import Ed25519
import json
import time
class MessageSigner:
def __init__(self, private_key):
self.private_key = private_key
def sign_with_metadata(self, message, metadata=None):
"""Sign message with additional context"""
# Create canonical signing structure
signing_data = {
"message": message.decode('utf-8') if isinstance(message, bytes) else message,
"timestamp": int(time.time()),
"version": "1.0"
}
if metadata:
signing_data["metadata"] = metadata
# Canonical JSON serialization
canonical = json.dumps(signing_data, sort_keys=True, separators=(',', ':'))
# Sign the canonical form
signature = Ed25519.sign(canonical.encode('utf-8'), self.private_key)
return {
"data": signing_data,
"signature": signature.hex()
}
@staticmethod
def verify_with_metadata(signed_message, public_key):
"""Verify signed message with metadata"""
# Reconstruct canonical form
canonical = json.dumps(
signed_message["data"],
sort_keys=True,
separators=(',', ':')
)
# Verify signature
signature = bytes.fromhex(signed_message["signature"])
return Ed25519.verify(
signature,
canonical.encode('utf-8'),
public_key
)
Implementation Details
Curve Operations
Ed25519 uses the twisted Edwards curve:
-x² + y² = 1 + d·x²·y²
where d = -121665/121666
Key advantages:
- Complete addition formulas (no special cases)
- Fast arithmetic operations
- Resistance to timing attacks
Signature Algorithm
- Key Generation:
- Generate 32-byte seed
- Hash seed with SHA-512
- Clamp first 32 bytes for private scalar
- Compute public key: A = [s]B
- Signing:
- Hash private key to get (s, prefix)
-
r = SHA-512(prefix message) - R = [r]B
-
h = SHA-512(R A message) - S = r + h·s mod ℓ
- Verification:
- Check 0 ≤ S < ℓ
-
h = SHA-512(R A message) - Check [S]B = R + [h]A
Security Properties
- Deterministic Signatures: Same message always produces same signature
- SUF-CMA Security: Strongly unforgeable under chosen message attack
- No Malleability: Signatures cannot be modified
- Collision Resistance: Based on SHA-512 security
Performance Characteristics
Operation Timings
| Operation | Time (μs) | Cycles (3GHz) |
|---|---|---|
| Key Generation | 15 | 45,000 |
| Sign | 25 | 75,000 |
| Verify | 50 | 150,000 |
| Batch Verify (64) | 1,200 | 3,600,000 |
Comparison with Other Algorithms
| Algorithm | Sign | Verify | Signature Size | Security |
|---|---|---|---|---|
| Ed25519 | 25 μs | 50 μs | 64 bytes | 128-bit |
| ECDSA-P256 | 300 μs | 600 μs | 64 bytes | 128-bit |
| RSA-2048 | 5 ms | 0.2 ms | 256 bytes | 112-bit |
| RSA-3072 | 15 ms | 0.3 ms | 384 bytes | 128-bit |
Advanced Usage
Hierarchical Deterministic Keys
from metamui_crypto import Ed25519, HMAC
import struct
class HDEd25519:
"""Hierarchical Deterministic Ed25519 keys"""
def __init__(self, master_seed):
self.master_seed = master_seed
self.master_keypair = Ed25519.from_seed(master_seed)
def derive_child(self, index):
"""Derive child keypair at index"""
# Use HMAC to derive child seed
hmac = HMAC(self.master_seed, 'sha512')
hmac.update(b"Ed25519-HD")
hmac.update(struct.pack('>I', index))
child_seed = hmac.finalize()[:32]
return Ed25519.from_seed(child_seed)
def derive_path(self, path):
"""Derive key at path like m/0/1/2"""
current_seed = self.master_seed
for index in path:
hmac = HMAC(current_seed, 'sha512')
hmac.update(b"Ed25519-HD")
hmac.update(struct.pack('>I', index))
current_seed = hmac.finalize()[:32]
return Ed25519.from_seed(current_seed)
Threshold Signatures
from metamui_crypto import Ed25519
import secrets
class ThresholdEd25519:
"""Simplified threshold signature scheme"""
def __init__(self, threshold, total_parties):
self.threshold = threshold
self.total_parties = total_parties
self.shares = self._generate_shares()
def _generate_shares(self):
"""Generate key shares using Shamir's Secret Sharing"""
# Generate master key
master_seed = secrets.token_bytes(32)
master_keypair = Ed25519.from_seed(master_seed)
# For demonstration - in practice use proper Shamir's
shares = []
for i in range(self.total_parties):
share_seed = HMAC(master_seed, 'sha256').update(
f"share-{i}".encode()
).finalize()
share_keypair = Ed25519.from_seed(share_seed)
shares.append({
'id': i,
'keypair': share_keypair,
'master_public': master_keypair.public_key
})
return shares
def sign_partial(self, message, share_id):
"""Create partial signature"""
share = self.shares[share_id]
return Ed25519.sign(message, share['keypair'].private_key)
def combine_signatures(self, partial_signatures, message):
"""Combine threshold signatures"""
if len(partial_signatures) < self.threshold:
raise ValueError("Insufficient signatures")
# Simplified - in practice use proper combination
# This just returns the first valid signature
return partial_signatures[0]
VRF (Verifiable Random Function)
from metamui_crypto import Ed25519, SHA256
class Ed25519VRF:
"""Simple VRF construction using Ed25519"""
def __init__(self, private_key):
self.private_key = private_key
self.public_key = Ed25519.get_public_key(private_key)
def prove(self, input_data):
"""Generate VRF proof"""
# Hash input with private key
to_sign = SHA256.hash(b"VRF" + input_data)
# Create proof (signature)
proof = Ed25519.sign(to_sign, self.private_key)
# Derive output
output = SHA256.hash(proof + b"output")
return output, proof
def verify(self, input_data, output, proof, public_key):
"""Verify VRF proof"""
# Verify the proof
to_sign = SHA256.hash(b"VRF" + input_data)
if not Ed25519.verify(proof, to_sign, public_key):
return False
# Check output matches
expected_output = SHA256.hash(proof + b"output")
return output == expected_output
Common Pitfalls
1. Private Key Exposure
# Bad: Logging or printing private keys
# print(f"Private key: {keypair.private_key.hex()}") # Never do this!
# Good: Only log public keys
print(f"Public key: {keypair.public_key.hex()}")
2. Signature Malleability
# Ed25519 signatures are malleable in some implementations
# Always use canonical signature verification
# Bad: Custom signature parsing
# r = signature[:32]
# s = signature[32:]
# # Manual verification...
# Good: Use library verification
is_valid = Ed25519.verify(signature, message, public_key)
3. Weak Random Seeds
# Bad: Predictable seeds
# seed = b"0" * 32 # Predictable!
# seed = hashlib.sha256(b"password").digest() # Weak!
# Good: Cryptographically secure random
import secrets
seed = secrets.token_bytes(32)
# Or derive from strong passphrase with salt
import hashlib
passphrase = "strong passphrase with high entropy"
salt = secrets.token_bytes(16) # Random salt
seed = hashlib.pbkdf2_hmac('sha256', passphrase.encode(), salt, 100000, 32)
Integration Examples
JWT with Ed25519
import json
import base64
from metamui_crypto import Ed25519
class Ed25519JWT:
@staticmethod
def create_token(payload, private_key):
"""Create JWT signed with Ed25519"""
# Header
header = {
"alg": "EdDSA",
"typ": "JWT"
}
# Encode header and payload
header_b64 = base64.urlsafe_b64encode(
json.dumps(header).encode()
).rstrip(b'=')
payload_b64 = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).rstrip(b'=')
# Create signature
message = header_b64 + b'.' + payload_b64
signature = Ed25519.sign(message, private_key)
signature_b64 = base64.urlsafe_b64encode(signature).rstrip(b'=')
# Combine
return (header_b64 + b'.' + payload_b64 + b'.' + signature_b64).decode()
@staticmethod
def verify_token(token, public_key):
"""Verify JWT signed with Ed25519"""
parts = token.encode().split(b'.')
if len(parts) != 3:
return False, None
# Verify signature
message = parts[0] + b'.' + parts[1]
signature = base64.urlsafe_b64decode(parts[2] + b'==')
if not Ed25519.verify(signature, message, public_key):
return False, None
# Decode payload
payload = json.loads(base64.urlsafe_b64decode(parts[1] + b'=='))
return True, payload
SSH Key Format
import struct
import base64
def ed25519_to_openssh(public_key):
"""Convert Ed25519 public key to OpenSSH format"""
key_type = b"ssh-ed25519"
# Build the public key blob
blob = b""
blob += struct.pack(">I", len(key_type))
blob += key_type
blob += struct.pack(">I", len(public_key))
blob += public_key
# Base64 encode
b64_key = base64.b64encode(blob).decode('ascii')
return f"ssh-ed25519 {b64_key}"
def openssh_to_ed25519(ssh_public_key):
"""Parse OpenSSH format Ed25519 public key"""
parts = ssh_public_key.strip().split()
if parts[0] != "ssh-ed25519":
raise ValueError("Not an Ed25519 SSH key")
# Decode base64
blob = base64.b64decode(parts[1])
# Parse blob
offset = 0
# Read key type
key_type_len = struct.unpack(">I", blob[offset:offset+4])[0]
offset += 4
key_type = blob[offset:offset+key_type_len]
offset += key_type_len
# Read public key
key_len = struct.unpack(">I", blob[offset:offset+4])[0]
offset += 4
public_key = blob[offset:offset+key_len]
return public_key
Migration Guide
From ECDSA to Ed25519
# Old ECDSA code
# from ecdsa import SigningKey, SECP256k1
# sk = SigningKey.generate(curve=SECP256k1)
# signature = sk.sign(message)
# New Ed25519 code
from metamui_crypto import Ed25519
keypair = Ed25519.generate_keypair()
signature = Ed25519.sign(message, keypair.private_key)
# Benefits:
# - Faster operations
# - Smaller keys
# - Deterministic signatures
# - No random number issues
From RSA to Ed25519
# Consider hybrid approach for compatibility
class HybridSigner:
def __init__(self):
self.ed25519_key = Ed25519.generate_keypair()
# Keep RSA for legacy systems
self.rsa_key = existing_rsa_key
def sign(self, message, use_legacy=False):
if use_legacy:
return rsa_sign(message, self.rsa_key)
return Ed25519.sign(message, self.ed25519_key.private_key)
Resources
- RFC 8032 - EdDSA Specification
- Ed25519 Paper - Original paper
- Security Analysis - Security properties
- Implementation Guide - Implementation details
- SafeCurves - Curve security analysis