Skip to content

ed448-goldilocks: reject non-canonical point encodings in decompress_unchecked - #1974

Open
DefaultMo wants to merge 1 commit into
RustCrypto:masterfrom
DefaultMo:ed448-canonical-decode
Open

DefaultMo wants to merge 1 commit into
RustCrypto:masterfrom
DefaultMo:ed448-canonical-decode

Conversation

@DefaultMo

@DefaultMo DefaultMo commented Oct 23, 2026 •

Copy link
Copy Markdown

Closes #1970.

Why

RFC 8032 section 5.2.3, step 1: "The y-coordinate is recovered simply by clearing this bit. If the resulting value is >= p, decoding fails." CompressedEdwardsY::decompress_unchecked read only bit 455 and reduced y mod p, so an encoding with any of bits 448 to 454 set, and the encoding of y + p, both decoded. Wycheproof ed448 tcId 63, 64 and 65 verified. Conformance, not forgery: only the key holder can produce such a signature, but two verifiers disagree on the same input and one key has two encodings.

What

  • decompress_unchecked requires sign & 0x7f == 0 and takes y from FieldElement::from_repr (the checked decoder already in the crate), both folded into the returned CtOption. Constant time, no early return.
  • decompress, signature R decoding and VerifyingKey::from_bytes go through it, so they inherit the check.
  • Overlap: ed448-goldilocks: don't reduce points during decompression #1462 switches this path to from_repr as well (the y >= p half). This change also covers the unused bits and adds the vectors; whichever lands second rebases, happy to take that side.

Tests

  • tests/wycheproof.rs over the vendored ed448_test.json (C2SP/wycheproof 12fd3aaf, sha256 3b3c7995...289a): every valid case verifies, every invalid case is refused, tcId 63 to 65 present. Fails on master with "tcId 63: invalid case accepted".
  • Unit tests: each of the seven unused bits on the generator's encoding refused by both decoders; y = 1 + p refused; the generator and y = 1 still decode.
  • Measured with the issue's harness, before and after: invalid accepted tcId 63, 64, 65 to none; valid 17 of 17 both ways; decompress(y + p) true to false; VerifyingKey::from_bytes(y + p) Ok to Err.

Gates

cargo test -p ed448-goldilocks --all-features: 74 + 1 + 3 passed. cargo fmt --check clean. cargo clippy --all-features -- -D warnings clean on 1.89 (the workspace pin) and 1.95.0.

Not in this PR

CompressedEdwardsY::IDENTITY encodes y = 0; #1949 fixes that.

…unchecked

RFC 8032 section 5.2.3 says decoding fails when the cleared y is >= p.
decompress_unchecked read only bit 455 and reduced y mod p, so an encoding
with any of bits 448-454 set, and the encoding of y + p, both decoded.
Wycheproof ed448 RustCrypto#63, RustCrypto#64 and RustCrypto#65 verified (3 invalid cases accepted of
70; none after this change; valid 17 of 17 both ways).

The decoder now requires the unused bits to be zero and takes y from
FieldElement::from_repr, folded into the returned CtOption. decompress,
signature R decoding and VerifyingKey::from_bytes inherit the check.

Tests: tests/wycheproof.rs over the vendored ed448_test.json (every valid
case verifies, every invalid case is refused; fails on master with
"tcId 63: invalid case accepted"); unit tests for each unused bit, for
y = 1 + p, and for the generator and y = 1 still decoding.

Closes RustCrypto#1970
@tarcieri

Copy link
Copy Markdown
Member

See #1972

@DefaultMo

Copy link
Copy Markdown
Author

Pointing to #1972 is taken. I posted the survey for question 1 there.

This PR today: decompress_unchecked refuses set bits 448 to 454 and y >= p (RFC 8032 section 5.2.3). decompress, the verifier's R decoding and VerifyingKey::from_bytes all go through it. The Wycheproof vectors are added as a test. It is a conformance fix, not a vulnerability report.

On question 2, the PR is "implement RFC 8032 outright", applied everywhere. It could take these shapes:

  1. As it is now.
  2. Lenient rules kept, with parallel strict entry points.
  3. Held until ed448-goldilocks: point decompression and validation criteria #1972 settles.

Which do you want? The vector test fits all three. On #1462 and #1949, whichever lands second adapts. Happy to take that side.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants