From 782e5f4db3f5d0f14760553a15294fabf3b3b554 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Fri, 9 Oct 2026 16:28:52 -0600 Subject: [PATCH] XEdDSA strict-verification Signed-off-by: Dave Grantham --- CHANGELOG.md | 7 +++++ Cargo.toml | 2 +- src/views/xeddsa.rs | 74 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3af78..40b00a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - 2026-10-09 + +### Fixed + +- Switched the XEdDSA `VerifyView` from plain Ed25519 verification to `verify_strict`. The view documentation states strict verification, but the code called plain `verify`. `verify_strict` also rejects a small-order `R` and a small-order verifying key on top of the canonicality checks, so the `xeddsa-msig` verifier no longer accepts malleable or weak-key signature forms. Verification of signatures produced by the `SignView` is unchanged. Added reject tests for non-canonical `R`, non-canonical `S`, and small-order keys. + ## [2.2.0] - 2026-10-08 ### Changed @@ -272,6 +278,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Major dependency updates: ed25519-dalek 3, blsful 4, elliptic-curve 0.14, vsss-rs 6, ssh-key 0.7. - Initial published release on crates.io as `multi-key`. +[2.3.0]: https://github.com/cryptidtech/multi-key/compare/v2.2.0...v2.3.0 [2.2.0]: https://github.com/cryptidtech/multi-key/compare/v2.1.1...v2.2.0 [2.1.1]: https://github.com/cryptidtech/multi-key/compare/v2.1.0...v2.1.1 [2.1.0]: https://github.com/cryptidtech/multi-key/compare/v2.0.0...v2.1.0 diff --git a/Cargo.toml b/Cargo.toml index 3bc4248..2d34e55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-key" -version = "2.2.0" +version = "2.3.0" edition = "2024" rust-version = "1.99" authors = ["Dave Grantham "] diff --git a/src/views/xeddsa.rs b/src/views/xeddsa.rs index 0843495..164d17a 100644 --- a/src/views/xeddsa.rs +++ b/src/views/xeddsa.rs @@ -28,7 +28,7 @@ use crate::{ error::{AttributesError, ConversionsError, SignError, VerifyError}, }; use curve25519_dalek::{edwards::EdwardsPoint, montgomery::MontgomeryPoint, scalar::Scalar}; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use ed25519_dalek::{Signature, VerifyingKey}; use multi_codec::Codec; use multi_hash::{Multihash, mh}; use multi_sig::{Multisig, ViewBuilder, ms}; @@ -391,9 +391,10 @@ impl<'a> VerifyView for View<'a> { ); } - // Ed25519 strict verification: a non-canonical R or S fails. + // Ed25519 strict verification: a non-canonical R or S fails, and a + // small-order R or verifying key fails. verifying_key - .verify(msg, &sig) + .verify_strict(msg, &sig) .map_err(|e| VerifyError::BadSignature(e.to_string()))?; Ok(()) @@ -539,6 +540,73 @@ mod tests { ); } + #[test] + fn test_xeddsa_rejects_non_canonical_r() { + let (sk, pk) = key_pair_mks(); + let msg = message32(); + let sig = ViewBuilder::new(&sk) + .sign() + .build() + .unwrap() + .sign(&msg, false, None) + .unwrap(); + let view = SigViewBuilder::new(&sig).data().build().unwrap(); + let mut bytes = view.sig_bytes().unwrap(); + // Non-canonical R: the y coordinate encodes 2^255 - 1, which is above + // the field modulus p = 2^255 - 19, so the bytes are not a canonical + // compressed Edwards point encoding. + for byte in &mut bytes[..32] { + *byte = 0xff; + } + bytes[31] &= 0x7f; + let tampered = ms::Builder::new(Codec::XeddsaMsig) + .with_signature_bytes(&bytes) + .try_build() + .unwrap(); + assert!( + ViewBuilder::new(&pk) + .verify() + .build() + .unwrap() + .verify(&tampered, Some(&msg)) + .is_err(), + "non-canonical R must not verify" + ); + } + + #[test] + fn test_xeddsa_rejects_non_canonical_s() { + let (sk, pk) = key_pair_mks(); + let msg = message32(); + let sig = ViewBuilder::new(&sk) + .sign() + .build() + .unwrap() + .sign(&msg, false, None) + .unwrap(); + let view = SigViewBuilder::new(&sig).data().build().unwrap(); + let mut bytes = view.sig_bytes().unwrap(); + // Non-canonical S: the scalar encodes 2^255 - 1, which is above the + // group order l, so the S component is not a reduced scalar. + for byte in &mut bytes[32..] { + *byte = 0xff; + } + bytes[63] &= 0x7f; + let tampered = ms::Builder::new(Codec::XeddsaMsig) + .with_signature_bytes(&bytes) + .try_build() + .unwrap(); + assert!( + ViewBuilder::new(&pk) + .verify() + .build() + .unwrap() + .verify(&tampered, Some(&msg)) + .is_err(), + "non-canonical S must not verify" + ); + } + #[test] fn test_xeddsa_kat_rfc7748_seed() { // KAT over the RFC 7748 Alice key pair. The seed is the RFC 7748