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,100 @@ 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+ if !ignore_high_bit && p[ CRYPTO_CORE_ED25519_BYTES - 1 ] & 0x80 != 0 {
195+ return false ;
196+ }
197+
198+ // Check 2: Reject the all-zero point, which is invalid.
199+ const ZERO_POINT : Ed25519Point = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
200+ if p == & ZERO_POINT {
201+ return false ;
202+ }
203+
204+ // Check 3: Reject the identity element ([1, 0, ..., 0]) which is a small-order
205+ // point.
206+ const SMALL_ORDER_POINT_IDENTITY : Ed25519Point = [
207+ 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 ,
208+ 0 , 0 ,
209+ ] ;
210+ if p == & SMALL_ORDER_POINT_IDENTITY {
211+ return false ;
212+ }
213+
214+ // Check 4: Use curve25519-dalek decompression for point-on-curve check.
215+ // This will also reject points not in the prime-order subgroup if the feature
216+ // `serde` is not enabled for curve25519-dalek, but we explicitly checked
217+ // identity.
218+ match CompressedEdwardsY :: from_slice ( p) {
219+ Ok ( compressed) => compressed. decompress ( ) . is_some ( ) ,
220+ Err ( _) => false , // Should not happen if length is correct, but handle defensively.
221+ }
222+ }
223+
126224#[ inline]
127225fn salsa20_rotl32 ( x : u32 , y : u32 , rot : u32 ) -> u32 {
128226 x. wrapping_add ( y) . rotate_left ( rot)
@@ -216,6 +314,8 @@ pub fn crypto_core_hsalsa20(
216314mod tests {
217315 use super :: * ;
218316 use crate :: classic:: crypto_box:: * ;
317+ use crate :: classic:: crypto_sign:: crypto_sign_keypair;
318+ use crate :: keypair:: { KeyPair , PublicKey , SecretKey } ;
219319
220320 #[ test]
221321 fn test_crypto_scalarmult_base ( ) {
@@ -333,4 +433,149 @@ mod tests {
333433 ) ;
334434 }
335435 }
436+
437+ #[ test]
438+ fn test_crypto_core_ed25519_is_valid_point ( ) {
439+ // Test with a known valid public key (from one of the crypto_sign test vectors)
440+ // This point is on the curve and correctly encoded.
441+ let valid_pk = [
442+ 215 , 90 , 152 , 1 , 130 , 177 , 10 , 183 , 213 , 75 , 254 , 211 , 201 , 100 , 7 , 58 , 14 , 225 , 114 ,
443+ 243 , 218 , 166 , 35 , 37 , 175 , 2 , 26 , 104 , 247 , 7 , 81 , 26 ,
444+ ] ;
445+ assert ! (
446+ crypto_core_ed25519_is_valid_point( & valid_pk) ,
447+ "Known valid Ed25519 public key should be considered valid"
448+ ) ;
449+
450+ // Test a point with the high bit set (invalid compressed format)
451+ // Standard Ed25519 compression requires the high bit of the last byte to be 0.
452+ let mut invalid_point_high_bit = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
453+ invalid_point_high_bit[ 31 ] = 0x80 ; // Set high bit, making it invalid
454+ assert ! (
455+ !crypto_core_ed25519_is_valid_point( & invalid_point_high_bit) ,
456+ "Point with high bit set in last byte should be invalid"
457+ ) ;
458+
459+ // Test the identity element (0, 1), which is a valid point.
460+ // Its compressed form is [1, 0, ..., 0].
461+ // While mathematically valid, this is a small-order point that can cause
462+ // security issues in certain cryptographic protocols, such as enabling
463+ // invalid curve attacks. Stricter implementations (like curve25519-dalek)
464+ // reject small-order points for this reason, whereas Libsodium accepts them.
465+ let small_order_point_identity = [
466+ 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 ,
467+ 0 , 0 , 0 ,
468+ ] ;
469+ assert ! (
470+ !crypto_core_ed25519_is_valid_point( & small_order_point_identity) ,
471+ "Small-order point (identity element) should be rejected by stricter validation"
472+ ) ;
473+
474+ // Test a point that is not on the curve (but is canonically encoded)
475+ // Example: A point generated randomly is unlikely to be on the curve.
476+ // We expect this to be rejected by the decompression check.
477+ let mut point_not_on_curve = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
478+ // Fill with some non-zero value that's unlikely to form a valid point
479+ // but is canonically encoded (last byte < 128)
480+ point_not_on_curve[ 0 ] = 2 ; // Example modification
481+ assert ! (
482+ !crypto_core_ed25519_is_valid_point( & point_not_on_curve) ,
483+ "Point not on the curve should be invalid"
484+ ) ;
485+
486+ // Test the zero point [0, ..., 0], which is invalid encoding.
487+ let zero_point = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
488+ assert ! (
489+ !crypto_core_ed25519_is_valid_point( & zero_point) ,
490+ "Zero point represents invalid encoding"
491+ ) ;
492+ }
493+
494+ #[ test]
495+ fn test_keypair_on_curve ( ) {
496+ // Run multiple attempts to ensure we catch any potential issues
497+ let iterations = 25 ;
498+ let mut strict_failures = 0 ;
499+ let mut relaxed_failures = 0 ;
500+
501+ println ! (
502+ "\n === Testing Ed25519 key validation across {} iterations ===" ,
503+ iterations
504+ ) ;
505+
506+ for i in 0 ..iterations {
507+ // Generate an Ed25519 keypair
508+ let ( ed25519_pk, _) = crypto_sign_keypair ( ) ;
509+
510+ // Check with strict validation (may fail due to high bit)
511+ let strict_valid = crypto_core_ed25519_is_valid_point ( & ed25519_pk) ;
512+
513+ // Check with relaxed validation (should always pass for generated keys)
514+ let relaxed_valid = crypto_core_ed25519_is_valid_point_relaxed ( & ed25519_pk) ;
515+
516+ if !strict_valid {
517+ strict_failures += 1 ;
518+ // Only check the reason when strict validation fails
519+ let high_bit_set = ed25519_pk[ CRYPTO_CORE_ED25519_BYTES - 1 ] & 0x80 != 0 ;
520+ println ! ( "Iteration {}: Ed25519 key strict validation failed:" , i) ;
521+ println ! ( " High bit set: {}" , high_bit_set) ;
522+ }
523+
524+ if !relaxed_valid {
525+ relaxed_failures += 1 ;
526+ // This shouldn't happen for properly generated keys
527+ println ! (
528+ "ERROR: Iteration {}: Ed25519 key failed relaxed validation" ,
529+ i
530+ ) ;
531+
532+ // Check all conditions to see why it failed
533+ const ZERO_POINT : Ed25519Point = [ 0u8 ; CRYPTO_CORE_ED25519_BYTES ] ;
534+ let is_zero = ed25519_pk == ZERO_POINT ;
535+
536+ const SMALL_ORDER_POINT_IDENTITY : Ed25519Point = [
537+ 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 ,
538+ 0 , 0 , 0 , 0 , 0 , 0 ,
539+ ] ;
540+ let is_identity = ed25519_pk == SMALL_ORDER_POINT_IDENTITY ;
541+
542+ let on_curve = match CompressedEdwardsY :: from_slice ( & ed25519_pk) {
543+ Ok ( compressed) => compressed. decompress ( ) . is_some ( ) ,
544+ Err ( _) => false ,
545+ } ;
546+
547+ println ! ( " Zero point: {}" , is_zero) ;
548+ println ! ( " Identity element: {}" , is_identity) ;
549+ println ! ( " On curve: {}" , on_curve) ;
550+ println ! ( " Key: {:?}" , ed25519_pk) ;
551+ }
552+
553+ // We should always be able to verify keys with relaxed validation
554+ assert ! (
555+ relaxed_valid,
556+ "Generated Ed25519 key failed relaxed validation"
557+ ) ;
558+ }
559+
560+ println ! (
561+ "\n Summary: {} of {} Ed25519 keys failed strict validation" ,
562+ strict_failures, iterations
563+ ) ;
564+ println ! (
565+ "Summary: {} of {} Ed25519 keys failed relaxed validation" ,
566+ relaxed_failures, iterations
567+ ) ;
568+
569+ // X25519 keys should be valid with standard validation (they're generated
570+ // clamped)
571+ println ! ( "\n === Testing X25519 key validation ===" ) ;
572+ let ( x25519_pk, _) = crypto_box_keypair ( ) ;
573+
574+ assert ! (
575+ KeyPair :: <PublicKey , SecretKey >:: is_valid_public_key( & x25519_pk) ,
576+ "X25519 public key should be valid according to X25519 rules"
577+ ) ;
578+
579+ println ! ( "X25519 key validation: Success" ) ;
580+ }
336581}
0 commit comments