Ed25519-ZIP215 Security-Focused API Documentation
Algorithm: Ed25519 with ZIP-215 validation rules
Type: Digital Signature Algorithm
Security Level: ~128-bit
Key Size: 256-bit private, 256-bit public
Signature Size: 512 bits (64 bytes)
Standard: RFC 8032 + ZIP-215 (Zcash Improvement Proposal)
Table of Contents
- Security Contract
- Attack Resistance Matrix
- Secure Usage Examples
- Common Mistakes
- Performance vs Security
- Platform-Specific Notes
- Compliance Information
Security Contract
generateKeyPair(): { privateKey: Uint8Array, publicKey: Uint8Array }
Preconditions:
- System MUST have cryptographically secure random source
- No input parameters required
Postconditions:
- Returns 32-byte private key (random scalar)
- Returns 32-byte public key (curve point)
- Private key has proper bit clamping applied
- Public key is valid curve point
- Keys are suitable for ZIP-215 signatures
Side Effects:
- Consumes 32 bytes of system entropy
- No timing variations
sign(privateKey: Uint8Array, message: Uint8Array): Uint8Array
Preconditions:
privateKeyMUST be exactly 32 bytesprivateKeyMUST be valid Ed25519 private keymessagecan be 0 to 2^64 - 1 bytes
Postconditions:
- Returns 64-byte signature
- Signature is deterministic (same message = same signature)
- Signature verifiable with corresponding public key
- ZIP-215 compliant encoding
Side Effects:
- No secret data leaked through timing
- Deterministic nonce generation (no RNG needed)
verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean
Preconditions:
publicKeyMUST be exactly 32 bytessignatureMUST be exactly 64 bytesmessagecan be 0 to 2^64 - 1 bytes
Postconditions:
- Returns
trueif signature valid under ZIP-215 rules - Returns
falseif signature invalid - Accepts all signatures that RFC 8032 accepts
- Accepts additional signatures per ZIP-215
- Constant-time verification
Side Effects:
- No timing variations based on signature validity
verifyBatch(entries: Array<{publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array}>): boolean
Preconditions:
- All entries must have valid format
- Batch size should be reasonable (< 1000)
Postconditions:
- Returns
trueif ALL signatures valid - Returns
falseif ANY signature invalid - ~2x faster than individual verification
- Same validation rules as single verify
Side Effects:
- Uses randomized verification internally
- Still maintains security guarantees
Attack Resistance Matrix
✅ Attacks Prevented
| Attack Type | Protection Mechanism | Security Level |
|---|---|---|
| Forgery | Discrete log hardness | 2^128 operations |
| Key Recovery | Curve25519 security | 2^128 operations |
| Malleability (ZIP-215) | Canonical acceptance | Fully prevented |
| Timing Attacks | Constant-time operations | Complete protection |
| Fault Injection | Verification checks | Signature rejected |
| Small Subgroup | Point validation | Not vulnerable |
| Invalid Curve | ZIP-215 validation | Rejected |
| Batch Poisoning | Randomized coefficients | 2^-128 probability |
❌ Attacks NOT Prevented
| Attack Type | Reason | Mitigation Required |
|---|---|---|
| Replay Attacks | No built-in freshness | Add nonce/timestamp |
| Signer Compromise | Deterministic signatures | Protect private keys |
| Quantum Attacks | Not quantum-resistant | Use post-quantum algo |
| Side-Channel (Power) | Implementation dependent | Hardware protection |
⚠️ Security Limitations
- Deterministic Signatures: Same message always produces same signature
- No Forward Secrecy: Old signatures remain valid forever
- Quantum Vulnerable: ~64-bit security against quantum computers
- ZIP-215 Compatibility: Accepts more signatures than strict RFC 8032
Secure Usage Examples
Basic Signature Generation and Verification
import { Ed25519ZIP215 } from '@metamui/ed25519-zip215';
// Generate a new key pair
const { privateKey, publicKey } = Ed25519ZIP215.generateKeyPair();
// Sign a message
const message = new TextEncoder().encode('Transfer 100 tokens to Alice');
const signature = Ed25519ZIP215.sign(privateKey, message);
// Verify the signature
const isValid = Ed25519ZIP215.verify(publicKey, message, signature);
console.log('Signature valid:', isValid); // true
// ZIP-215 accepts non-canonical signatures that RFC 8032 rejects
// This ensures compatibility with all Ed25519 implementations
Secure Key Storage
import { secureErase } from '@metamui/secure-memory';
class SecureKeyManager {
private privateKey: Uint8Array | null = null;
public readonly publicKey: Uint8Array;
constructor() {
const keyPair = Ed25519ZIP215.generateKeyPair();
this.privateKey = keyPair.privateKey;
this.publicKey = keyPair.publicKey;
}
sign(message: Uint8Array): Uint8Array {
if (!this.privateKey) {
throw new Error('Private key has been destroyed');
}
return Ed25519ZIP215.sign(this.privateKey, message);
}
destroy(): void {
if (this.privateKey) {
secureErase(this.privateKey);
this.privateKey = null;
}
}
// Ensure cleanup on garbage collection
[Symbol.dispose](): void {
this.destroy();
}
}
// Usage with automatic cleanup
{
using signer = new SecureKeyManager();
const signature = signer.sign(message);
// Private key automatically erased when scope exits
}
Batch Verification for Performance
class SignatureValidator {
static async validateTransactions(
transactions: Array<{
from: Uint8Array; // public key
data: Uint8Array; // transaction data
signature: Uint8Array;
}>
): Promise<boolean> {
// Prepare batch entries
const entries = transactions.map(tx => ({
publicKey: tx.from,
message: tx.data,
signature: tx.signature
}));
// Batch verification is ~2x faster
const allValid = Ed25519ZIP215.verifyBatch(entries);
if (!allValid) {
// Find which signatures are invalid
const results = await Promise.all(
entries.map(async (entry, index) => ({
index,
valid: Ed25519ZIP215.verify(
entry.publicKey,
entry.message,
entry.signature
)
}))
);
const invalid = results.filter(r => !r.valid);
console.error(`Invalid signatures at indices: ${invalid.map(r => r.index)}`);
}
return allValid;
}
}
Timestamped Signatures for Replay Protection
class TimestampedSigner {
constructor(
private readonly privateKey: Uint8Array,
public readonly publicKey: Uint8Array
) {}
signWithTimestamp(
message: Uint8Array,
validitySeconds: number = 300 // 5 minutes
): {
signature: Uint8Array;
timestamp: number;
expiry: number;
} {
const timestamp = Math.floor(Date.now() / 1000);
const expiry = timestamp + validitySeconds;
// Create message with timestamp
const timestampBytes = new Uint8Array(8);
new DataView(timestampBytes.buffer).setBigUint64(0, BigInt(timestamp));
const expiryBytes = new Uint8Array(8);
new DataView(expiryBytes.buffer).setBigUint64(0, BigInt(expiry));
// Concatenate: timestamp || expiry || message
const fullMessage = new Uint8Array(
timestampBytes.length + expiryBytes.length + message.length
);
fullMessage.set(timestampBytes, 0);
fullMessage.set(expiryBytes, timestampBytes.length);
fullMessage.set(message, timestampBytes.length + expiryBytes.length);
const signature = Ed25519ZIP215.sign(this.privateKey, fullMessage);
return { signature, timestamp, expiry };
}
static verifyWithTimestamp(
publicKey: Uint8Array,
message: Uint8Array,
signature: Uint8Array,
timestamp: number,
expiry: number
): boolean {
// Check if signature has expired
const now = Math.floor(Date.now() / 1000);
if (now > expiry) {
return false; // Signature expired
}
// Reconstruct full message
const timestampBytes = new Uint8Array(8);
new DataView(timestampBytes.buffer).setBigUint64(0, BigInt(timestamp));
const expiryBytes = new Uint8Array(8);
new DataView(expiryBytes.buffer).setBigUint64(0, BigInt(expiry));
const fullMessage = new Uint8Array(
timestampBytes.length + expiryBytes.length + message.length
);
fullMessage.set(timestampBytes, 0);
fullMessage.set(expiryBytes, timestampBytes.length);
fullMessage.set(message, timestampBytes.length + expiryBytes.length);
return Ed25519ZIP215.verify(publicKey, fullMessage, signature);
}
}
Common Mistakes
❌ Using RFC 8032 Strict Validation
// WRONG: Strict RFC 8032 validation rejects valid signatures
function strictVerify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean {
// This rejects non-canonical signatures that ZIP-215 accepts
if (!isCanonicalEncoding(signature)) {
return false; // TOO STRICT!
}
return ed25519Verify(publicKey, message, signature);
}
// CORRECT: ZIP-215 accepts all valid signatures
function zip215Verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean {
// Accepts both canonical and non-canonical encodings
return Ed25519ZIP215.verify(publicKey, message, signature);
}
❌ Reusing Signatures Across Contexts
// BAD: Same signature used for different purposes
const signature = Ed25519ZIP215.sign(privateKey, userId);
// WRONG: Signature can be replayed in different context!
const loginToken = { userId, signature };
const transferAuth = { userId, amount: 1000, signature }; // REPLAY ATTACK!
// GOOD: Include context in signed message
const loginMessage = new TextEncoder().encode(`login:${userId}:${timestamp}`);
const loginSignature = Ed25519ZIP215.sign(privateKey, loginMessage);
const transferMessage = new TextEncoder().encode(`transfer:${userId}:${amount}:${nonce}`);
const transferSignature = Ed25519ZIP215.sign(privateKey, transferMessage);
❌ Not Protecting Private Keys
// TERRIBLE: Private key in code
const privateKey = new Uint8Array([
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60,
// ... rest of key
]);
// BAD: Private key in environment variable
const privateKey = Buffer.from(process.env.PRIVATE_KEY, 'hex');
// BETTER: Encrypted storage with hardware protection
import { KeyVault } from '@cloud/key-vault';
async function signSecurely(message: Uint8Array): Promise<Uint8Array> {
// Key never exposed to application
return await KeyVault.sign('key-id', message);
}
❌ Signature Malleability Confusion
// ZIP-215 prevents signature malleability attacks
// But some systems might still be vulnerable
// Example of why ZIP-215 matters:
const signature1 = Ed25519ZIP215.sign(privateKey, message);
// An attacker might create non-canonical form
const signature2 = makeNonCanonical(signature1);
// RFC 8032 strict: signature2 rejected (good but breaks compatibility)
// RFC 8032 loose: signature2 accepted (malleability attack!)
// ZIP-215: signature2 accepted but treated as equivalent to signature1
// With ZIP-215, both signatures are valid and considered identical
const valid1 = Ed25519ZIP215.verify(publicKey, message, signature1);
const valid2 = Ed25519ZIP215.verify(publicKey, message, signature2);
// Both are true, preventing malleability attacks
Performance vs Security
Performance Characteristics
| Operation | Time (typical) | Security Impact |
|---|---|---|
| Key Generation | ~50 μs | Full security |
| Sign | ~70 μs | Deterministic |
| Verify | ~150 μs | Constant-time |
| Batch Verify (n=100) | ~8 ms | Randomized |
Optimization Guidelines
// Batch verification for multiple signatures
const signatures = transactions.map(tx => ({
publicKey: tx.signer,
message: tx.data,
signature: tx.signature
}));
// ~2x faster than individual verification
if (Ed25519ZIP215.verifyBatch(signatures)) {
// All signatures valid
} else {
// At least one invalid - verify individually to find which
}
Platform-Specific Notes
TypeScript/JavaScript
- Use
crypto.getRandomValues()for key generation - BigInt operations for curve arithmetic
- WASM implementation available for performance
Python
secrets.token_bytes(32)for key generation- Native int supports large numbers
- Consider
PyNaClfor comparison
Rust
getrandomcrate for randomnesscurve25519-dalekfor operations- Compile with
--releasefor performance
Swift
SecRandomCopyBytesfor key generation- Use
CryptoKitfor comparison - Avoid unnecessary data conversions
Kotlin/JVM
SecureRandomfor key generation- BigInteger for curve operations
- JNI bindings available for performance
Compliance Information
Standards Compliance
- RFC 8032: Ed25519 signature standard
- ZIP-215: Zcash Improvement Proposal for validation rules
- FIPS 186-5: Included in draft standard
- NIST SP 800-186: Recommended parameters
ZIP-215 Specific Rules
- Accept Non-Canonical S: S values ≥ L are accepted
- Accept Non-Canonical A: Any 32-byte encoding accepted
- Accept Identity Points: Including point at infinity
- Consistent Validation: All implementations accept same signatures
Security Properties
- EUF-CMA: Existentially unforgeable under chosen message attack
- SUF-CMA: Strongly unforgeable (with ZIP-215)
- Deterministic: No random number generator failures
- Side-Channel Resistant: Constant-time implementation
Use Cases
✅ Ideal for:
- Cryptocurrency transactions
- API authentication
- Document signing
- Certificate signatures
- Any system requiring compatibility
❌ Not recommended for:
- Post-quantum security needs
- Systems requiring randomized signatures
- Environments needing signature encryption
Security Notice: ZIP-215 ensures maximum compatibility while maintaining security. All signatures that pass ZIP-215 validation are cryptographically valid. Report issues to security@metamui.id
Last Updated: 2025-07-06
**Version: 3.0.0
License: BSL-1.1
Security Analysis
Threat Model: Ed25519-ZIP215 Threat Model
The comprehensive threat analysis covers:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.