Ed25519 Signature Malleability
Overview
The Ed25519 signature malleability detector identifies Solana programs that verify Ed25519 signatures without enforcing canonical form. Non-canonical signatures allow attackers to produce alternative valid signatures for the same message, bypassing deduplication or single-use signature assumptions. For remediation steps, see the Ed25519 Signature Malleability Remediation.
Why This Is an Issue
Ed25519 signatures have a malleability property: for any valid signature (R, S), the pair (R, L - S) where L is the curve order is also valid for the same message. This means each message can have at least two valid signatures. Programs that rely on signature uniqueness – such as tracking which signatures have been used, deduplicating transactions, or enforcing one-time authorization – can be bypassed.
In practice, an attacker can:
- Replay transactions by submitting the malleable variant of an already-used signature.
- Bypass deduplication in airdrop, claim, or voting systems that track used signatures.
- Forge alternative proofs that pass verification but differ from the original.
The canonical form requires S < L (where L is the Ed25519 curve order). Without this check, both S and L - S are accepted as valid.
How to Resolve
// Before: Vulnerable -- non-strict verification
let result = ed25519_verify(&pubkey, &message, &signature);
if !result {
return Err(ProgramError::InvalidArgument);
}
// Signature passes but may be non-canonical
// After: Fixed -- use strict verification
let result = ed25519_verify_strict(&pubkey, &message, &signature);
if !result {
return Err(ProgramError::InvalidArgument);
}
// Only canonical signatures accepted
Examples
Vulnerable Code
pub fn verify_and_store(
accounts: &[AccountInfo],
signature: &[u8; 64],
message: &[u8],
) -> ProgramResult {
let pubkey = accounts[0].key.to_bytes();
// Non-strict verification accepts both canonical and non-canonical
if !ed25519_verify(&pubkey, message, signature) {
return Err(ProgramError::InvalidArgument);
}
// Store signature as "used" -- attacker can bypass with malleable variant
let used_sigs = &mut accounts[1];
store_used_signature(used_sigs, signature)?;
Ok(())
}
Fixed Code
pub fn verify_and_store(
accounts: &[AccountInfo],
signature: &[u8; 64],
message: &[u8],
) -> ProgramResult {
let pubkey = accounts[0].key.to_bytes();
// Enforce canonical signature form: S < L
if signature[63] & 0xE0 != 0 {
return Err(ProgramError::InvalidArgument);
}
if !ed25519_verify(&pubkey, message, signature) {
return Err(ProgramError::InvalidArgument);
}
let used_sigs = &mut accounts[1];
store_used_signature(used_sigs, signature)?;
Ok(())
}
Example JSON Finding
{
"detector": "ed25519-signature-malleability",
"severity": "high",
"confidence": 0.80,
"title": "Ed25519 Signature Verification Without Canonical Check",
"description": "Ed25519 signature verification is performed without checking for canonical signature form.",
"cwe_ids": [347]
}
Detection Methodology
- Syscall identification: Identifies Ed25519 verification syscalls by name pattern matching (e.g.,
ed25519_verify,sol_ed25519_check). - Strict variant detection: Checks whether the program uses
verify_strictorcanonicalverification variants. - Canonical check search: Scans for manual canonical validation patterns such as bit mask checks (
signature[63] & 0xE0 == 0) or comparisons against the curve order. - Signature storage tracking: Detects when signatures are stored to account data without normalization, indicating deduplication vulnerability.
Limitations
False positives: Programs that implement canonical checks in external library calls or precompiled instructions may be flagged. False negatives: Custom signature verification that does not use recognized syscall names will not be detected. Programs using the Ed25519 precompile instruction via instruction introspection follow a different pattern that may not be fully covered.
Related Detectors
- Signature Replay – detects missing replay protection for signatures
- Weak Randomness – detects insecure cryptographic patterns