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

  1. Security Contract
  2. Attack Resistance Matrix
  3. Secure Usage Examples
  4. Common Mistakes
  5. Performance vs Security
  6. Platform-Specific Notes
  7. Compliance Information

Security Contract

generateKeyPair(): { privateKey: Uint8Array, publicKey: Uint8Array }

Preconditions:

Postconditions:

Side Effects:

sign(privateKey: Uint8Array, message: Uint8Array): Uint8Array

Preconditions:

Postconditions:

Side Effects:

verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean

Preconditions:

Postconditions:

Side Effects:

verifyBatch(entries: Array<{publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array}>): boolean

Preconditions:

Postconditions:

Side Effects:

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

  1. Deterministic Signatures: Same message always produces same signature
  2. No Forward Secrecy: Old signatures remain valid forever
  3. Quantum Vulnerable: ~64-bit security against quantum computers
  4. 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

Python

Rust

Swift

Kotlin/JVM

Compliance Information

Standards Compliance

ZIP-215 Specific Rules

  1. Accept Non-Canonical S: S values ≥ L are accepted
  2. Accept Non-Canonical A: Any 32-byte encoding accepted
  3. Accept Identity Points: Including point at infinity
  4. Consistent Validation: All implementations accept same signatures

Security Properties

Use Cases

Ideal for:

Not recommended for:


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:

For complete security analysis and risk assessment, see the dedicated threat model documentation.