Skip to content

[cryptography] reject ed25519 signatures with non-canonical S at decode time#3532

Open
0xAysh wants to merge 3 commits intocommonwarexyz:mainfrom
0xAysh:fix/signature-validity-consistency
Open

[cryptography] reject ed25519 signatures with non-canonical S at decode time#3532
0xAysh wants to merge 3 commits intocommonwarexyz:mainfrom
0xAysh:fix/signature-validity-consistency

Conversation

@0xAysh
Copy link
Copy Markdown

@0xAysh 0xAysh commented Apr 3, 2026

Problem

ed25519::Signature::read_cfg accepted any 64-byte sequence without validation. A signature with S >= l (the ed25519 group order) would decode successfully but fail verification later. This is inconsistent with the rest of the codebase:

  • ed25519::PublicKey::read_cfg calls VerificationKey::try_from — validates the compressed Edwards point at decode time
  • secp256r1::Signature::read_cfg calls p256::ecdsa::Signature::from_slice — validates R and S at decode time
  • ed25519::Signature::read_cfg — previously did nothing beyond confirming 64 bytes were present

Non-canonical S values introduce signature malleability: two different byte sequences (S and S mod l) decode to logically equivalent signatures since verification reduces S mod l. In adversarial environments this matters — a valid signature can be transformed into a "different" one that also passes verification.

Fix

Add a canonicality check for S (bytes 32..63 of the signature) in read_cfg. S must be in [0, l-1] where:

l = 2^252 + 27742317777372353535851937790883648493

The check compares S against l byte-by-byte from the most significant byte down (required because S is stored little-endian — index 0 is the LSB, so Rust's default lexicographic array comparison would be incorrect here).

The ed25519_consensus library does not expose standalone signature validation without a message, so the R component (bytes 0..31) cannot be validated at decode time without adding a dependency on curve25519-dalek. Only S canonicality is checkable, which is the primary malleability concern.

Tests

  • test_high_s_rejected_at_decode: S with high bit set (S >= 2^255 >> l)
  • test_s_equal_to_l_rejected_at_decode: S == l exactly (boundary — valid range is [0, l-1])
  • test_s_above_l_rejected_at_decode: S == l+1

Closes #1070

@cronokirby cronokirby added the breaking-format This PR modifies codec and/or storage formats. label Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-format This PR modifies codec and/or storage formats.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cryptography] Signature Scheme Validity Consistency

2 participants