dryoc/classic/
crypto_core.rs

1use curve25519_dalek::edwards::CompressedEdwardsY;
2
3use crate::constants::{
4    CRYPTO_CORE_ED25519_BYTES, CRYPTO_CORE_HCHACHA20_INPUTBYTES, CRYPTO_CORE_HCHACHA20_KEYBYTES,
5    CRYPTO_CORE_HCHACHA20_OUTPUTBYTES, CRYPTO_CORE_HSALSA20_INPUTBYTES,
6    CRYPTO_CORE_HSALSA20_KEYBYTES, CRYPTO_CORE_HSALSA20_OUTPUTBYTES, CRYPTO_SCALARMULT_BYTES,
7    CRYPTO_SCALARMULT_SCALARBYTES,
8};
9use crate::scalarmult_curve25519::{
10    crypto_scalarmult_curve25519, crypto_scalarmult_curve25519_base,
11};
12use crate::types::*;
13use crate::utils::load_u32_le;
14
15/// Stack-allocated HChaCha20 input.
16pub type HChaCha20Input = [u8; CRYPTO_CORE_HCHACHA20_INPUTBYTES];
17/// Stack-allocated HChaCha20 key.
18pub type HChaCha20Key = [u8; CRYPTO_CORE_HCHACHA20_KEYBYTES];
19/// Stack-allocated HChaCha20 output.
20pub type HChaCha20Output = [u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
21/// Stack-allocated HSalsa20 input.
22pub type HSalsa20Input = [u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
23/// Stack-allocated HSalsa20 key.
24pub type HSalsa20Key = [u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
25/// Stack-allocated HSalsa20 output.
26pub type HSalsa20Output = [u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
27/// Stack-allocated Ed25519 point.
28pub type Ed25519Point = [u8; CRYPTO_CORE_ED25519_BYTES];
29
30/// Computes the public key for a previously generated secret key.
31///
32/// Compatible with libsodium's `crypto_scalarmult_base`.
33pub fn crypto_scalarmult_base(
34    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
35    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
36) {
37    crypto_scalarmult_curve25519_base(q, n)
38}
39
40/// Computes a shared secret `q`, given `n`, our secret key, and `p`, their
41/// public key, using a Diffie-Hellman key exchange.
42///
43/// Compatible with libsodium's `crypto_scalarmult`.
44pub fn crypto_scalarmult(
45    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
46    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
47    p: &[u8; CRYPTO_SCALARMULT_BYTES],
48) {
49    crypto_scalarmult_curve25519(q, n, p)
50}
51
52#[inline]
53fn chacha20_round(x: &mut u32, y: &u32, z: &mut u32, rot: u32) {
54    *x = x.wrapping_add(*y);
55    *z = (*z ^ *x).rotate_left(rot);
56}
57
58#[inline]
59fn chacha20_quarterround(a: &mut u32, b: &mut u32, c: &mut u32, d: &mut u32) {
60    chacha20_round(a, b, d, 16);
61    chacha20_round(c, d, b, 12);
62    chacha20_round(a, b, d, 8);
63    chacha20_round(c, d, b, 7);
64}
65
66/// Implements the HChaCha20 function.
67///
68/// Compatible with libsodium's `crypto_core_hchacha20`.
69pub fn crypto_core_hchacha20(
70    output: &mut HChaCha20Output,
71    input: &HChaCha20Input,
72    key: &HChaCha20Key,
73    constants: Option<(u32, u32, u32, u32)>,
74) {
75    let input = input.as_array();
76    let key = key.as_array();
77    assert_eq!(input.len(), 16);
78    assert_eq!(key.len(), 32);
79    let (mut x0, mut x1, mut x2, mut x3) =
80        constants.unwrap_or((0x61707865, 0x3320646e, 0x79622d32, 0x6b206574));
81    let (
82        mut x4,
83        mut x5,
84        mut x6,
85        mut x7,
86        mut x8,
87        mut x9,
88        mut x10,
89        mut x11,
90        mut x12,
91        mut x13,
92        mut x14,
93        mut x15,
94    ) = (
95        load_u32_le(&key[0..4]),
96        load_u32_le(&key[4..8]),
97        load_u32_le(&key[8..12]),
98        load_u32_le(&key[12..16]),
99        load_u32_le(&key[16..20]),
100        load_u32_le(&key[20..24]),
101        load_u32_le(&key[24..28]),
102        load_u32_le(&key[28..32]),
103        load_u32_le(&input[0..4]),
104        load_u32_le(&input[4..8]),
105        load_u32_le(&input[8..12]),
106        load_u32_le(&input[12..16]),
107    );
108
109    for _ in 0..10 {
110        chacha20_quarterround(&mut x0, &mut x4, &mut x8, &mut x12);
111        chacha20_quarterround(&mut x1, &mut x5, &mut x9, &mut x13);
112        chacha20_quarterround(&mut x2, &mut x6, &mut x10, &mut x14);
113        chacha20_quarterround(&mut x3, &mut x7, &mut x11, &mut x15);
114        chacha20_quarterround(&mut x0, &mut x5, &mut x10, &mut x15);
115        chacha20_quarterround(&mut x1, &mut x6, &mut x11, &mut x12);
116        chacha20_quarterround(&mut x2, &mut x7, &mut x8, &mut x13);
117        chacha20_quarterround(&mut x3, &mut x4, &mut x9, &mut x14);
118    }
119
120    output[0..4].copy_from_slice(&x0.to_le_bytes());
121    output[4..8].copy_from_slice(&x1.to_le_bytes());
122    output[8..12].copy_from_slice(&x2.to_le_bytes());
123    output[12..16].copy_from_slice(&x3.to_le_bytes());
124    output[16..20].copy_from_slice(&x12.to_le_bytes());
125    output[20..24].copy_from_slice(&x13.to_le_bytes());
126    output[24..28].copy_from_slice(&x14.to_le_bytes());
127    output[28..32].copy_from_slice(&x15.to_le_bytes());
128}
129
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.
175pub 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.
185pub 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.
191fn 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 and
216    // reject points with a torsion component (not in the main subgroup).
217    match CompressedEdwardsY::from_slice(p) {
218        Ok(compressed) => match compressed.decompress() {
219            Some(point) => point.is_torsion_free(),
220            None => false,
221        },
222        Err(_) => false, // Should not happen if length is correct, but handle defensively.
223    }
224}
225
226#[inline]
227fn salsa20_rotl32(x: u32, y: u32, rot: u32) -> u32 {
228    x.wrapping_add(y).rotate_left(rot)
229}
230
231/// Implements the HSalsa20 function.
232///
233/// Compatible with libsodium's `crypto_core_hsalsa20`.
234pub fn crypto_core_hsalsa20(
235    output: &mut HSalsa20Output,
236    input: &HSalsa20Input,
237    key: &HSalsa20Key,
238    constants: Option<(u32, u32, u32, u32)>,
239) {
240    let (mut x0, mut x5, mut x10, mut x15) =
241        constants.unwrap_or((0x61707865, 0x3320646e, 0x79622d32, 0x6b206574));
242    let (
243        mut x1,
244        mut x2,
245        mut x3,
246        mut x4,
247        mut x11,
248        mut x12,
249        mut x13,
250        mut x14,
251        mut x6,
252        mut x7,
253        mut x8,
254        mut x9,
255    ) = (
256        load_u32_le(&key[0..4]),
257        load_u32_le(&key[4..8]),
258        load_u32_le(&key[8..12]),
259        load_u32_le(&key[12..16]),
260        load_u32_le(&key[16..20]),
261        load_u32_le(&key[20..24]),
262        load_u32_le(&key[24..28]),
263        load_u32_le(&key[28..32]),
264        load_u32_le(&input[0..4]),
265        load_u32_le(&input[4..8]),
266        load_u32_le(&input[8..12]),
267        load_u32_le(&input[12..16]),
268    );
269
270    for _ in (0..20).step_by(2) {
271        x4 ^= salsa20_rotl32(x0, x12, 7);
272        x8 ^= salsa20_rotl32(x4, x0, 9);
273        x12 ^= salsa20_rotl32(x8, x4, 13);
274        x0 ^= salsa20_rotl32(x12, x8, 18);
275        x9 ^= salsa20_rotl32(x5, x1, 7);
276        x13 ^= salsa20_rotl32(x9, x5, 9);
277        x1 ^= salsa20_rotl32(x13, x9, 13);
278        x5 ^= salsa20_rotl32(x1, x13, 18);
279        x14 ^= salsa20_rotl32(x10, x6, 7);
280        x2 ^= salsa20_rotl32(x14, x10, 9);
281        x6 ^= salsa20_rotl32(x2, x14, 13);
282        x10 ^= salsa20_rotl32(x6, x2, 18);
283        x3 ^= salsa20_rotl32(x15, x11, 7);
284        x7 ^= salsa20_rotl32(x3, x15, 9);
285        x11 ^= salsa20_rotl32(x7, x3, 13);
286        x15 ^= salsa20_rotl32(x11, x7, 18);
287        x1 ^= salsa20_rotl32(x0, x3, 7);
288        x2 ^= salsa20_rotl32(x1, x0, 9);
289        x3 ^= salsa20_rotl32(x2, x1, 13);
290        x0 ^= salsa20_rotl32(x3, x2, 18);
291        x6 ^= salsa20_rotl32(x5, x4, 7);
292        x7 ^= salsa20_rotl32(x6, x5, 9);
293        x4 ^= salsa20_rotl32(x7, x6, 13);
294        x5 ^= salsa20_rotl32(x4, x7, 18);
295        x11 ^= salsa20_rotl32(x10, x9, 7);
296        x8 ^= salsa20_rotl32(x11, x10, 9);
297        x9 ^= salsa20_rotl32(x8, x11, 13);
298        x10 ^= salsa20_rotl32(x9, x8, 18);
299        x12 ^= salsa20_rotl32(x15, x14, 7);
300        x13 ^= salsa20_rotl32(x12, x15, 9);
301        x14 ^= salsa20_rotl32(x13, x12, 13);
302        x15 ^= salsa20_rotl32(x14, x13, 18);
303    }
304
305    output[0..4].copy_from_slice(&x0.to_le_bytes());
306    output[4..8].copy_from_slice(&x5.to_le_bytes());
307    output[8..12].copy_from_slice(&x10.to_le_bytes());
308    output[12..16].copy_from_slice(&x15.to_le_bytes());
309    output[16..20].copy_from_slice(&x6.to_le_bytes());
310    output[20..24].copy_from_slice(&x7.to_le_bytes());
311    output[24..28].copy_from_slice(&x8.to_le_bytes());
312    output[28..32].copy_from_slice(&x9.to_le_bytes());
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::classic::crypto_box::*;
319    use crate::classic::crypto_sign::crypto_sign_keypair;
320    use crate::keypair::{KeyPair, PublicKey, SecretKey};
321
322    #[test]
323    fn test_crypto_scalarmult_base() {
324        use base64::Engine as _;
325        use base64::engine::general_purpose;
326        for _ in 0..20 {
327            use sodiumoxide::crypto::scalarmult::curve25519::{Scalar, scalarmult_base};
328
329            let (pk, sk) = crypto_box_keypair();
330
331            let mut public_key = [0u8; CRYPTO_SCALARMULT_BYTES];
332            crypto_scalarmult_base(&mut public_key, &sk);
333
334            assert_eq!(&pk, &public_key);
335
336            let ge = scalarmult_base(&Scalar::from_slice(&sk).unwrap());
337
338            assert_eq!(
339                general_purpose::STANDARD.encode(ge.as_ref()),
340                general_purpose::STANDARD.encode(public_key)
341            );
342        }
343    }
344
345    #[test]
346    fn test_crypto_scalarmult() {
347        use base64::Engine as _;
348        use base64::engine::general_purpose;
349        for _ in 0..20 {
350            use sodiumoxide::crypto::scalarmult::curve25519::{GroupElement, Scalar, scalarmult};
351
352            let (_our_pk, our_sk) = crypto_box_keypair();
353            let (their_pk, _their_sk) = crypto_box_keypair();
354
355            let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
356            crypto_scalarmult(&mut shared_secret, &our_sk, &their_pk);
357
358            let ge = scalarmult(
359                &Scalar::from_slice(&our_sk).unwrap(),
360                &GroupElement::from_slice(&their_pk).unwrap(),
361            )
362            .expect("scalarmult failed");
363
364            assert_eq!(
365                general_purpose::STANDARD.encode(ge.as_ref()),
366                general_purpose::STANDARD.encode(shared_secret)
367            );
368        }
369    }
370
371    #[test]
372    fn test_crypto_core_hchacha20() {
373        use base64::Engine as _;
374        use base64::engine::general_purpose;
375        use libsodium_sys::crypto_core_hchacha20 as so_crypto_core_hchacha20;
376
377        use crate::rng::copy_randombytes;
378
379        for _ in 0..10 {
380            let mut key = [0u8; 32];
381            let mut data = [0u8; 16];
382            copy_randombytes(&mut key);
383            copy_randombytes(&mut data);
384
385            let mut out = [0u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
386            crypto_core_hchacha20(&mut out, &data, &key, None);
387
388            let mut so_out = [0u8; 32];
389            unsafe {
390                let ret = so_crypto_core_hchacha20(
391                    so_out.as_mut_ptr(),
392                    data.as_ptr(),
393                    key.as_ptr(),
394                    std::ptr::null(),
395                );
396                assert_eq!(ret, 0);
397            }
398            assert_eq!(
399                general_purpose::STANDARD.encode(out),
400                general_purpose::STANDARD.encode(so_out)
401            );
402        }
403    }
404
405    #[test]
406    fn test_crypto_core_hsalsa20() {
407        use base64::Engine as _;
408        use base64::engine::general_purpose;
409        use libsodium_sys::crypto_core_hsalsa20 as so_crypto_core_hsalsa20;
410
411        use crate::rng::copy_randombytes;
412
413        for _ in 0..10 {
414            let mut key = [0u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
415            let mut data = [0u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
416            copy_randombytes(&mut key);
417            copy_randombytes(&mut data);
418
419            let mut out = [0u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
420            crypto_core_hsalsa20(&mut out, &data, &key, None);
421
422            let mut so_out = [0u8; 32];
423            unsafe {
424                let ret = so_crypto_core_hsalsa20(
425                    so_out.as_mut_ptr(),
426                    data.as_ptr(),
427                    key.as_ptr(),
428                    std::ptr::null(),
429                );
430                assert_eq!(ret, 0);
431            }
432            assert_eq!(
433                general_purpose::STANDARD.encode(out),
434                general_purpose::STANDARD.encode(so_out)
435            );
436        }
437    }
438
439    #[test]
440    fn test_crypto_core_ed25519_is_valid_point() {
441        // Test with a known valid public key (from one of the crypto_sign test vectors)
442        // This point is on the curve and correctly encoded.
443        let valid_pk = [
444            215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114,
445            243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26,
446        ];
447        assert!(
448            crypto_core_ed25519_is_valid_point(&valid_pk),
449            "Known valid Ed25519 public key should be considered valid"
450        );
451
452        // Test a point with the high bit set (invalid compressed format)
453        // Standard Ed25519 compression requires the high bit of the last byte to be 0.
454        let mut invalid_point_high_bit = [0u8; CRYPTO_CORE_ED25519_BYTES];
455        invalid_point_high_bit[31] = 0x80; // Set high bit, making it invalid
456        assert!(
457            !crypto_core_ed25519_is_valid_point(&invalid_point_high_bit),
458            "Point with high bit set in last byte should be invalid"
459        );
460
461        // Test the identity element (0, 1), which is a valid point.
462        // Its compressed form is [1, 0, ..., 0].
463        // While mathematically valid, this is a small-order point that can cause
464        // security issues in certain cryptographic protocols, such as enabling
465        // invalid curve attacks. Stricter implementations (like curve25519-dalek)
466        // reject small-order points for this reason, whereas Libsodium accepts them.
467        let small_order_point_identity = [
468            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,
469            0, 0, 0,
470        ];
471        assert!(
472            !crypto_core_ed25519_is_valid_point(&small_order_point_identity),
473            "Small-order point (identity element) should be rejected by stricter validation"
474        );
475
476        // Test torsion points and mixed-order points to ensure we reject anything
477        // outside the main subgroup.
478        use curve25519_dalek::traits::IsIdentity;
479
480        let torsion_point = curve25519_dalek::constants::EIGHT_TORSION
481            .iter()
482            .find(|point| {
483                let torsion_bytes = point.compress().to_bytes();
484                let mixed_point = curve25519_dalek::constants::ED25519_BASEPOINT_POINT + *point;
485                let mixed_bytes = mixed_point.compress().to_bytes();
486                torsion_bytes[31] & 0x80 == 0
487                    && mixed_bytes[31] & 0x80 == 0
488                    && !point.is_identity()
489                    && !mixed_point.is_torsion_free()
490            })
491            .copied()
492            .expect("expected a non-identity torsion point with canonical encoding");
493
494        let torsion_bytes = torsion_point.compress().to_bytes();
495        assert!(
496            !crypto_core_ed25519_is_valid_point(&torsion_bytes),
497            "Torsion point should be rejected"
498        );
499        assert!(
500            !crypto_core_ed25519_is_valid_point_relaxed(&torsion_bytes),
501            "Torsion point should be rejected even with relaxed validation"
502        );
503
504        let mixed_bytes = (curve25519_dalek::constants::ED25519_BASEPOINT_POINT + torsion_point)
505            .compress()
506            .to_bytes();
507        assert!(
508            !crypto_core_ed25519_is_valid_point(&mixed_bytes),
509            "Mixed-order point should be rejected"
510        );
511        assert!(
512            !crypto_core_ed25519_is_valid_point_relaxed(&mixed_bytes),
513            "Mixed-order point should be rejected even with relaxed validation"
514        );
515
516        // Test a point that is not on the curve (but is canonically encoded)
517        // Example: A point generated randomly is unlikely to be on the curve.
518        // We expect this to be rejected by the decompression check.
519        let mut point_not_on_curve = [0u8; CRYPTO_CORE_ED25519_BYTES];
520        // Fill with some non-zero value that's unlikely to form a valid point
521        // but is canonically encoded (last byte < 128)
522        point_not_on_curve[0] = 2; // Example modification
523        assert!(
524            !crypto_core_ed25519_is_valid_point(&point_not_on_curve),
525            "Point not on the curve should be invalid"
526        );
527
528        // Test the zero point [0, ..., 0], which is invalid encoding.
529        let zero_point = [0u8; CRYPTO_CORE_ED25519_BYTES];
530        assert!(
531            !crypto_core_ed25519_is_valid_point(&zero_point),
532            "Zero point represents invalid encoding"
533        );
534    }
535
536    #[test]
537    fn test_keypair_on_curve() {
538        // Run multiple attempts to ensure we catch any potential issues
539        let iterations = 25;
540        let mut strict_failures = 0;
541        let mut relaxed_failures = 0;
542
543        println!(
544            "\n=== Testing Ed25519 key validation across {} iterations ===",
545            iterations
546        );
547
548        for i in 0..iterations {
549            // Generate an Ed25519 keypair
550            let (ed25519_pk, _) = crypto_sign_keypair();
551
552            // Check with strict validation (may fail due to high bit)
553            let strict_valid = crypto_core_ed25519_is_valid_point(&ed25519_pk);
554
555            // Check with relaxed validation (should always pass for generated keys)
556            let relaxed_valid = crypto_core_ed25519_is_valid_point_relaxed(&ed25519_pk);
557
558            if !strict_valid {
559                strict_failures += 1;
560                // Only check the reason when strict validation fails
561                let high_bit_set = ed25519_pk[CRYPTO_CORE_ED25519_BYTES - 1] & 0x80 != 0;
562                println!("Iteration {}: Ed25519 key strict validation failed:", i);
563                println!("  High bit set: {}", high_bit_set);
564            }
565
566            if !relaxed_valid {
567                relaxed_failures += 1;
568                // This shouldn't happen for properly generated keys
569                println!(
570                    "ERROR: Iteration {}: Ed25519 key failed relaxed validation",
571                    i
572                );
573
574                // Check all conditions to see why it failed
575                const ZERO_POINT: Ed25519Point = [0u8; CRYPTO_CORE_ED25519_BYTES];
576                let is_zero = ed25519_pk == ZERO_POINT;
577
578                const SMALL_ORDER_POINT_IDENTITY: Ed25519Point = [
579                    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,
580                    0, 0, 0, 0, 0, 0,
581                ];
582                let is_identity = ed25519_pk == SMALL_ORDER_POINT_IDENTITY;
583
584                let on_curve = match CompressedEdwardsY::from_slice(&ed25519_pk) {
585                    Ok(compressed) => compressed.decompress().is_some(),
586                    Err(_) => false,
587                };
588
589                println!("  Zero point: {}", is_zero);
590                println!("  Identity element: {}", is_identity);
591                println!("  On curve: {}", on_curve);
592                println!("  Key: {:?}", ed25519_pk);
593            }
594
595            // We should always be able to verify keys with relaxed validation
596            assert!(
597                relaxed_valid,
598                "Generated Ed25519 key failed relaxed validation"
599            );
600        }
601
602        println!(
603            "\nSummary: {} of {} Ed25519 keys failed strict validation",
604            strict_failures, iterations
605        );
606        println!(
607            "Summary: {} of {} Ed25519 keys failed relaxed validation",
608            relaxed_failures, iterations
609        );
610
611        // X25519 keys should be valid with standard validation (they're generated
612        // clamped)
613        println!("\n=== Testing X25519 key validation ===");
614        let (x25519_pk, _) = crypto_box_keypair();
615
616        assert!(
617            KeyPair::<PublicKey, SecretKey>::is_valid_public_key(&x25519_pk),
618            "X25519 public key should be valid according to X25519 rules"
619        );
620
621        println!("X25519 key validation: Success");
622    }
623}