Skip to main content
Sigvex

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.

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.

References