1+ use curve25519_dalek:: edwards:: CompressedEdwardsY ;
2+
13use crate :: constants:: {
2- CRYPTO_CORE_HCHACHA20_INPUTBYTES , CRYPTO_CORE_HCHACHA20_KEYBYTES ,
4+ CRYPTO_CORE_ED25519_BYTES , CRYPTO_CORE_HCHACHA20_INPUTBYTES , CRYPTO_CORE_HCHACHA20_KEYBYTES ,
35 CRYPTO_CORE_HCHACHA20_OUTPUTBYTES , CRYPTO_CORE_HSALSA20_INPUTBYTES ,
46 CRYPTO_CORE_HSALSA20_KEYBYTES , CRYPTO_CORE_HSALSA20_OUTPUTBYTES , CRYPTO_SCALARMULT_BYTES ,
57 CRYPTO_SCALARMULT_SCALARBYTES ,
@@ -22,6 +24,8 @@ pub type HSalsa20Input = [u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
2224pub type HSalsa20Key = [ u8 ; CRYPTO_CORE_HSALSA20_KEYBYTES ] ;
2325/// Stack-allocated HSalsa20 output.
2426pub type HSalsa20Output = [ u8 ; CRYPTO_CORE_HSALSA20_OUTPUTBYTES ] ;
27+ /// Stack-allocated Ed25519 point.
28+ pub type Ed25519Point = [ u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
2529
2630/// Computes the public key for a previously generated secret key.
2731///
@@ -123,6 +127,101 @@ pub fn crypto_core_hchacha20(
123127 output[ 28 ..32 ] . copy_from_slice ( & x15. to_le_bytes ( ) ) ;
124128}
125129
130+ /// Checks if a given point is on the Ed25519 curve.
131+ ///
132+ /// This function determines if a given point is a valid point on the Ed25519
133+ /// curve that can be safely used for cryptographic operations.
134+ ///
135+ /// # Security Note
136+ ///
137+ /// This implementation uses `curve25519-dalek` for validation and is stricter
138+ /// than libsodium's `crypto_core_ed25519_is_valid_point`. Specifically, it may
139+ /// reject certain points, such as small-order points (e.g., the point
140+ /// represented by `[1, 0, ..., 0]`), which libsodium might accept. While
141+ /// libsodium's behavior provides compatibility, using points rejected by this
142+ /// function can lead to security vulnerabilities in certain protocols. Relying
143+ /// on this stricter check is generally recommended for new applications.
144+ ///
145+ /// By default, this function enforces canonical encoding by requiring the high
146+ /// bit of the last byte to be 0. If you're working with Ed25519 keys generated
147+ /// by [`crypto_sign_keypair`](`crate::classic::crypto_sign::crypto_sign_keypair`)
148+ /// that might have the high bit set, you should use
149+ /// [`crypto_core_ed25519_is_valid_point_relaxed`] instead.
150+ ///
151+ /// # Example
152+ ///
153+ /// ```
154+ /// use dryoc::classic::crypto_core::{
155+ /// Ed25519Point, crypto_core_ed25519_is_valid_point,
156+ /// crypto_core_ed25519_is_valid_point_relaxed,
157+ /// };
158+ /// use dryoc::classic::crypto_sign::crypto_sign_keypair;
159+ ///
160+ /// // Get a valid Ed25519 public key (valid point)
161+ /// let (pk, _) = crypto_sign_keypair();
162+ ///
163+ /// // For keys from crypto_sign_keypair(), use the relaxed validation
164+ /// // as they may have the high bit set
165+ /// assert!(crypto_core_ed25519_is_valid_point_relaxed(&pk));
166+ ///
167+ /// // Strict validation for a manually constructed point
168+ /// let mut invalid_point = [0u8; 32];
169+ /// invalid_point[31] = 0x80; // Set high bit, making it invalid
170+ /// assert!(!crypto_core_ed25519_is_valid_point(&invalid_point));
171+ /// ```
172+ ///
173+ /// Not fully compatible with libsodium's `crypto_core_ed25519_is_valid_point`
174+ /// due to stricter checks.
175+ pub fn crypto_core_ed25519_is_valid_point ( p : & Ed25519Point ) -> bool {
176+ crypto_core_ed25519_is_valid_point_internal ( p, false )
177+ }
178+
179+ /// Version of [`crypto_core_ed25519_is_valid_point`] that optionally ignores
180+ /// the high bit check.
181+ ///
182+ /// This is particularly useful when validating Ed25519 public keys generated by
183+ /// [`crypto_sign_keypair`](`crate::classic::crypto_sign::crypto_sign_keypair`),
184+ /// which may have the high bit set.
185+ pub fn crypto_core_ed25519_is_valid_point_relaxed ( p : & Ed25519Point ) -> bool {
186+ crypto_core_ed25519_is_valid_point_internal ( p, true )
187+ }
188+
189+ /// Internal implementation for point validation that can optionally ignore the
190+ /// high bit check.
191+ fn crypto_core_ed25519_is_valid_point_internal ( p : & Ed25519Point , ignore_high_bit : bool ) -> bool {
192+ // Check 1: Canonical encoding. The high bit of the last byte must be 0, unless
193+ // ignore_high_bit is true.
194+ let last_byte = p[ CRYPTO_CORE_ED25519_BYTES - 1 ] ;
195+ if !ignore_high_bit && last_byte & 0x80 != 0 {
196+ return false ;
197+ }
198+
199+ // Check 2: Reject the all-zero point, which is invalid.
200+ const ZERO_POINT : Ed25519Point = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
201+ if p == & ZERO_POINT {
202+ return false ;
203+ }
204+
205+ // Check 3: Reject the identity element ([1, 0, ..., 0]) which is a small-order
206+ // point.
207+ const SMALL_ORDER_POINT_IDENTITY : Ed25519Point = [
208+ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
209+ 0 , 0 ,
210+ ] ;
211+ if p == & SMALL_ORDER_POINT_IDENTITY {
212+ return false ;
213+ }
214+
215+ // Check 4: Use curve25519-dalek decompression for point-on-curve check.
216+ // This will also reject points not in the prime-order subgroup if the feature
217+ // `serde` is not enabled for curve25519-dalek, but we explicitly checked
218+ // identity.
219+ match CompressedEdwardsY :: from_slice ( p) {
220+ Ok ( compressed) => compressed. decompress ( ) . is_some ( ) ,
221+ Err ( _) => false , // Should not happen if length is correct, but handle defensively.
222+ }
223+ }
224+
126225#[ inline]
127226fn salsa20_rotl32 ( x : u32 , y : u32 , rot : u32 ) -> u32 {
128227 x. wrapping_add ( y) . rotate_left ( rot)
@@ -216,6 +315,8 @@ pub fn crypto_core_hsalsa20(
216315mod tests {
217316 use super :: * ;
218317 use crate :: classic:: crypto_box:: * ;
318+ use crate :: classic:: crypto_sign:: crypto_sign_keypair;
319+ use crate :: keypair:: { KeyPair , PublicKey , SecretKey } ;
219320
220321 #[ test]
221322 fn test_crypto_scalarmult_base ( ) {
@@ -333,4 +434,149 @@ mod tests {
333434 ) ;
334435 }
335436 }
437+
438+ #[ test]
439+ fn test_crypto_core_ed25519_is_valid_point ( ) {
440+ // Test with a known valid public key (from one of the crypto_sign test vectors)
441+ // This point is on the curve and correctly encoded.
442+ let valid_pk = [
443+ 215 , 90 , 152 , 1 , 130 , 177 , 10 , 183 , 213 , 75 , 254 , 211 , 201 , 100 , 7 , 58 , 14 , 225 , 114 ,
444+ 243 , 218 , 166 , 35 , 37 , 175 , 2 , 26 , 104 , 247 , 7 , 81 , 26 ,
445+ ] ;
446+ assert ! (
447+ crypto_core_ed25519_is_valid_point( & valid_pk) ,
448+ "Known valid Ed25519 public key should be considered valid"
449+ ) ;
450+
451+ // Test a point with the high bit set (invalid compressed format)
452+ // Standard Ed25519 compression requires the high bit of the last byte to be 0.
453+ let mut invalid_point_high_bit = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
454+ invalid_point_high_bit[ 31 ] = 0x80 ; // Set high bit, making it invalid
455+ assert ! (
456+ !crypto_core_ed25519_is_valid_point( & invalid_point_high_bit) ,
457+ "Point with high bit set in last byte should be invalid"
458+ ) ;
459+
460+ // Test the identity element (0, 1), which is a valid point.
461+ // Its compressed form is [1, 0, ..., 0].
462+ // While mathematically valid, this is a small-order point that can cause
463+ // security issues in certain cryptographic protocols, such as enabling
464+ // invalid curve attacks. Stricter implementations (like curve25519-dalek)
465+ // reject small-order points for this reason, whereas Libsodium accepts them.
466+ let small_order_point_identity = [
467+ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
468+ 0 , 0 , 0 ,
469+ ] ;
470+ assert ! (
471+ !crypto_core_ed25519_is_valid_point( & small_order_point_identity) ,
472+ "Small-order point (identity element) should be rejected by stricter validation"
473+ ) ;
474+
475+ // Test a point that is not on the curve (but is canonically encoded)
476+ // Example: A point generated randomly is unlikely to be on the curve.
477+ // We expect this to be rejected by the decompression check.
478+ let mut point_not_on_curve = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
479+ // Fill with some non-zero value that's unlikely to form a valid point
480+ // but is canonically encoded (last byte < 128)
481+ point_not_on_curve[ 0 ] = 2 ; // Example modification
482+ assert ! (
483+ !crypto_core_ed25519_is_valid_point( & point_not_on_curve) ,
484+ "Point not on the curve should be invalid"
485+ ) ;
486+
487+ // Test the zero point [0, ..., 0], which is invalid encoding.
488+ let zero_point = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
489+ assert ! (
490+ !crypto_core_ed25519_is_valid_point( & zero_point) ,
491+ "Zero point represents invalid encoding"
492+ ) ;
493+ }
494+
495+ #[ test]
496+ fn test_keypair_on_curve ( ) {
497+ // Run multiple attempts to ensure we catch any potential issues
498+ let iterations = 25 ;
499+ let mut strict_failures = 0 ;
500+ let mut relaxed_failures = 0 ;
501+
502+ println ! (
503+ "\n === Testing Ed25519 key validation across {} iterations ===" ,
504+ iterations
505+ ) ;
506+
507+ for i in 0 ..iterations {
508+ // Generate an Ed25519 keypair
509+ let ( ed25519_pk, _) = crypto_sign_keypair ( ) ;
510+
511+ // Check with strict validation (may fail due to high bit)
512+ let strict_valid = crypto_core_ed25519_is_valid_point ( & ed25519_pk) ;
513+
514+ // Check with relaxed validation (should always pass for generated keys)
515+ let relaxed_valid = crypto_core_ed25519_is_valid_point_relaxed ( & ed25519_pk) ;
516+
517+ if !strict_valid {
518+ strict_failures += 1 ;
519+ // Only check the reason when strict validation fails
520+ let high_bit_set = ed25519_pk[ CRYPTO_CORE_ED25519_BYTES - 1 ] & 0x80 != 0 ;
521+ println ! ( "Iteration {}: Ed25519 key strict validation failed:" , i) ;
522+ println ! ( " High bit set: {}" , high_bit_set) ;
523+ }
524+
525+ if !relaxed_valid {
526+ relaxed_failures += 1 ;
527+ // This shouldn't happen for properly generated keys
528+ println ! (
529+ "ERROR: Iteration {}: Ed25519 key failed relaxed validation" ,
530+ i
531+ ) ;
532+
533+ // Check all conditions to see why it failed
534+ const ZERO_POINT : Ed25519Point = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
535+ let is_zero = ed25519_pk == ZERO_POINT ;
536+
537+ const SMALL_ORDER_POINT_IDENTITY : Ed25519Point = [
538+ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
539+ 0 , 0 , 0 , 0 , 0 , 0 ,
540+ ] ;
541+ let is_identity = ed25519_pk == SMALL_ORDER_POINT_IDENTITY ;
542+
543+ let on_curve = match CompressedEdwardsY :: from_slice ( & ed25519_pk) {
544+ Ok ( compressed) => compressed. decompress ( ) . is_some ( ) ,
545+ Err ( _) => false ,
546+ } ;
547+
548+ println ! ( " Zero point: {}" , is_zero) ;
549+ println ! ( " Identity element: {}" , is_identity) ;
550+ println ! ( " On curve: {}" , on_curve) ;
551+ println ! ( " Key: {:?}" , ed25519_pk) ;
552+ }
553+
554+ // We should always be able to verify keys with relaxed validation
555+ assert ! (
556+ relaxed_valid,
557+ "Generated Ed25519 key failed relaxed validation"
558+ ) ;
559+ }
560+
561+ println ! (
562+ "\n Summary: {} of {} Ed25519 keys failed strict validation" ,
563+ strict_failures, iterations
564+ ) ;
565+ println ! (
566+ "Summary: {} of {} Ed25519 keys failed relaxed validation" ,
567+ relaxed_failures, iterations
568+ ) ;
569+
570+ // X25519 keys should be valid with standard validation (they're generated
571+ // clamped)
572+ println ! ( "\n === Testing X25519 key validation ===" ) ;
573+ let ( x25519_pk, _) = crypto_box_keypair ( ) ;
574+
575+ assert ! (
576+ KeyPair :: <PublicKey , SecretKey >:: is_valid_public_key( & x25519_pk) ,
577+ "X25519 public key should be valid according to X25519 rules"
578+ ) ;
579+
580+ println ! ( "X25519 key validation: Success" ) ;
581+ }
336582}
0 commit comments