Ed25519 Signature Malleability Remediation
How to fix Ed25519 signature verification to prevent malleability attacks.
Ed25519 Signature Malleability Remediation
Overview
Related Detector: Ed25519 Signature Malleability
Ed25519 signatures are malleable: for each valid signature (R, S), another valid signature (R, L - S) exists for the same message. Programs that assume signature uniqueness for deduplication, single-use authorization, or replay prevention must enforce canonical form (S < L) before verification and storage.
Recommended Fix
Before (Vulnerable)
pub fn claim_airdrop(
accounts: &[AccountInfo],
signature: &[u8; 64],
message: &[u8],
) -> ProgramResult {
// VULNERABLE: accepts non-canonical signatures
if !ed25519_verify(&accounts[0].key.to_bytes(), message, signature) {
return Err(ProgramError::InvalidArgument);
}
// Attacker submits malleable variant to claim again
mark_signature_used(accounts, signature)?;
distribute_tokens(accounts)?;
Ok(())
}
After (Fixed)
pub fn claim_airdrop(
accounts: &[AccountInfo],
signature: &[u8; 64],
message: &[u8],
) -> ProgramResult {
// FIXED: reject non-canonical signatures before verification
if !is_canonical_signature(signature) {
return Err(ProgramError::InvalidArgument);
}
if !ed25519_verify(&accounts[0].key.to_bytes(), message, signature) {
return Err(ProgramError::InvalidArgument);
}
mark_signature_used(accounts, signature)?;
distribute_tokens(accounts)?;
Ok(())
}
/// Check that Ed25519 signature is in canonical form (S < L).
/// The top 3 bits of S (byte 63) must be zero for canonical signatures.
fn is_canonical_signature(signature: &[u8; 64]) -> bool {
signature[63] & 0xE0 == 0
}
Alternative Mitigations
1. Use verify_strict variants
If your cryptography library provides a strict verification function, prefer it over manual canonical checks:
// ed25519-dalek provides verify_strict
use ed25519_dalek::{Verifier, VerifyingKey};
let verifying_key = VerifyingKey::from_bytes(&pubkey_bytes)?;
let sig = ed25519_dalek::Signature::from_bytes(signature);
// verify_strict rejects non-canonical S values
verifying_key.verify_strict(message, &sig)?;
2. Hash-based deduplication instead of signature-based
Instead of storing raw signatures for deduplication, hash the message content:
use solana_program::hash::hash;
// Deduplicate by message content hash, not signature
let msg_hash = hash(message);
if is_message_processed(&msg_hash)? {
return Err(ProgramError::InvalidArgument);
}
mark_message_processed(&msg_hash)?;
This sidesteps malleability entirely because the message is the same regardless of which signature variant was used.
3. Use the Ed25519 precompile with instruction introspection
use solana_program::ed25519_program;
use solana_program::sysvar::instructions;
// The Ed25519 precompile verifies signatures before your program runs
// Use instruction introspection to confirm verification occurred
let ix = instructions::load_instruction_at_checked(0, &accounts[0])?;
if ix.program_id != ed25519_program::ID {
return Err(ProgramError::InvalidArgument);
}
Common Mistakes
Mistake 1: Checking canonical form after storage
// WRONG: signature stored before canonical check
store_signature(signature)?;
if signature[63] & 0xE0 != 0 {
return Err(ProgramError::InvalidArgument);
}
Always validate canonical form before any processing or storage.
Mistake 2: Only checking the R component
// WRONG: R is always canonical; malleability is in S
if signature[31] & 0x80 != 0 {
return Err(ProgramError::InvalidArgument);
}
The malleability is in the S component (bytes 32-63), not the R component (bytes 0-31).
Mistake 3: Comparing signatures by reference instead of content
// WRONG: two different byte arrays can represent the same logical signature
if &stored_sig as *const _ == &new_sig as *const _ { ... }
Compare signature bytes by value after ensuring both are in canonical form.