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.
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
225#[inline]
226fn salsa20_rotl32(x: u32, y: u32, rot: u32) -> u32 {
227    x.wrapping_add(y).rotate_left(rot)
228}
229
230/// Implements the HSalsa20 function.
231///
232/// Compatible with libsodium's `crypto_core_hsalsa20`.
233pub fn crypto_core_hsalsa20(
234    output: &mut HSalsa20Output,
235    input: &HSalsa20Input,
236    key: &HSalsa20Key,
237    constants: Option<(u32, u32, u32, u32)>,
238) {
239    let (mut x0, mut x5, mut x10, mut x15) =
240        constants.unwrap_or((0x61707865, 0x3320646e, 0x79622d32, 0x6b206574));
241    let (
242        mut x1,
243        mut x2,
244        mut x3,
245        mut x4,
246        mut x11,
247        mut x12,
248        mut x13,
249        mut x14,
250        mut x6,
251        mut x7,
252        mut x8,
253        mut x9,
254    ) = (
255        load_u32_le(&key[0..4]),
256        load_u32_le(&key[4..8]),
257        load_u32_le(&key[8..12]),
258        load_u32_le(&key[12..16]),
259        load_u32_le(&key[16..20]),
260        load_u32_le(&key[20..24]),
261        load_u32_le(&key[24..28]),
262        load_u32_le(&key[28..32]),
263        load_u32_le(&input[0..4]),
264        load_u32_le(&input[4..8]),
265        load_u32_le(&input[8..12]),
266        load_u32_le(&input[12..16]),
267    );
268
269    for _ in (0..20).step_by(2) {
270        x4 ^= salsa20_rotl32(x0, x12, 7);
271        x8 ^= salsa20_rotl32(x4, x0, 9);
272        x12 ^= salsa20_rotl32(x8, x4, 13);
273        x0 ^= salsa20_rotl32(x12, x8, 18);
274        x9 ^= salsa20_rotl32(x5, x1, 7);
275        x13 ^= salsa20_rotl32(x9, x5, 9);
276        x1 ^= salsa20_rotl32(x13, x9, 13);
277        x5 ^= salsa20_rotl32(x1, x13, 18);
278        x14 ^= salsa20_rotl32(x10, x6, 7);
279        x2 ^= salsa20_rotl32(x14, x10, 9);
280        x6 ^= salsa20_rotl32(x2, x14, 13);
281        x10 ^= salsa20_rotl32(x6, x2, 18);
282        x3 ^= salsa20_rotl32(x15, x11, 7);
283        x7 ^= salsa20_rotl32(x3, x15, 9);
284        x11 ^= salsa20_rotl32(x7, x3, 13);
285        x15 ^= salsa20_rotl32(x11, x7, 18);
286        x1 ^= salsa20_rotl32(x0, x3, 7);
287        x2 ^= salsa20_rotl32(x1, x0, 9);
288        x3 ^= salsa20_rotl32(x2, x1, 13);
289        x0 ^= salsa20_rotl32(x3, x2, 18);
290        x6 ^= salsa20_rotl32(x5, x4, 7);
291        x7 ^= salsa20_rotl32(x6, x5, 9);
292        x4 ^= salsa20_rotl32(x7, x6, 13);
293        x5 ^= salsa20_rotl32(x4, x7, 18);
294        x11 ^= salsa20_rotl32(x10, x9, 7);
295        x8 ^= salsa20_rotl32(x11, x10, 9);
296        x9 ^= salsa20_rotl32(x8, x11, 13);
297        x10 ^= salsa20_rotl32(x9, x8, 18);
298        x12 ^= salsa20_rotl32(x15, x14, 7);
299        x13 ^= salsa20_rotl32(x12, x15, 9);
300        x14 ^= salsa20_rotl32(x13, x12, 13);
301        x15 ^= salsa20_rotl32(x14, x13, 18);
302    }
303
304    output[0..4].copy_from_slice(&x0.to_le_bytes());
305    output[4..8].copy_from_slice(&x5.to_le_bytes());
306    output[8..12].copy_from_slice(&x10.to_le_bytes());
307    output[12..16].copy_from_slice(&x15.to_le_bytes());
308    output[16..20].copy_from_slice(&x6.to_le_bytes());
309    output[20..24].copy_from_slice(&x7.to_le_bytes());
310    output[24..28].copy_from_slice(&x8.to_le_bytes());
311    output[28..32].copy_from_slice(&x9.to_le_bytes());
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::classic::crypto_box::*;
318    use crate::classic::crypto_sign::crypto_sign_keypair;
319    use crate::keypair::{KeyPair, PublicKey, SecretKey};
320
321    #[test]
322    fn test_crypto_scalarmult_base() {
323        use base64::Engine as _;
324        use base64::engine::general_purpose;
325        for _ in 0..20 {
326            use sodiumoxide::crypto::scalarmult::curve25519::{Scalar, scalarmult_base};
327
328            let (pk, sk) = crypto_box_keypair();
329
330            let mut public_key = [0u8; CRYPTO_SCALARMULT_BYTES];
331            crypto_scalarmult_base(&mut public_key, &sk);
332
333            assert_eq!(&pk, &public_key);
334
335            let ge = scalarmult_base(&Scalar::from_slice(&sk).unwrap());
336
337            assert_eq!(
338                general_purpose::STANDARD.encode(ge.as_ref()),
339                general_purpose::STANDARD.encode(public_key)
340            );
341        }
342    }
343
344    #[test]
345    fn test_crypto_scalarmult() {
346        use base64::Engine as _;
347        use base64::engine::general_purpose;
348        for _ in 0..20 {
349            use sodiumoxide::crypto::scalarmult::curve25519::{GroupElement, Scalar, scalarmult};
350
351            let (_our_pk, our_sk) = crypto_box_keypair();
352            let (their_pk, _their_sk) = crypto_box_keypair();
353
354            let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
355            crypto_scalarmult(&mut shared_secret, &our_sk, &their_pk);
356
357            let ge = scalarmult(
358                &Scalar::from_slice(&our_sk).unwrap(),
359                &GroupElement::from_slice(&their_pk).unwrap(),
360            )
361            .expect("scalarmult failed");
362
363            assert_eq!(
364                general_purpose::STANDARD.encode(ge.as_ref()),
365                general_purpose::STANDARD.encode(shared_secret)
366            );
367        }
368    }
369
370    #[test]
371    fn test_crypto_core_hchacha20() {
372        use base64::Engine as _;
373        use base64::engine::general_purpose;
374        use libsodium_sys::crypto_core_hchacha20 as so_crypto_core_hchacha20;
375
376        use crate::rng::copy_randombytes;
377
378        for _ in 0..10 {
379            let mut key = [0u8; 32];
380            let mut data = [0u8; 16];
381            copy_randombytes(&mut key);
382            copy_randombytes(&mut data);
383
384            let mut out = [0u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
385            crypto_core_hchacha20(&mut out, &data, &key, None);
386
387            let mut so_out = [0u8; 32];
388            unsafe {
389                let ret = so_crypto_core_hchacha20(
390                    so_out.as_mut_ptr(),
391                    data.as_ptr(),
392                    key.as_ptr(),
393                    std::ptr::null(),
394                );
395                assert_eq!(ret, 0);
396            }
397            assert_eq!(
398                general_purpose::STANDARD.encode(out),
399                general_purpose::STANDARD.encode(so_out)
400            );
401        }
402    }
403
404    #[test]
405    fn test_crypto_core_hsalsa20() {
406        use base64::Engine as _;
407        use base64::engine::general_purpose;
408        use libsodium_sys::crypto_core_hsalsa20 as so_crypto_core_hsalsa20;
409
410        use crate::rng::copy_randombytes;
411
412        for _ in 0..10 {
413            let mut key = [0u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
414            let mut data = [0u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
415            copy_randombytes(&mut key);
416            copy_randombytes(&mut data);
417
418            let mut out = [0u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
419            crypto_core_hsalsa20(&mut out, &data, &key, None);
420
421            let mut so_out = [0u8; 32];
422            unsafe {
423                let ret = so_crypto_core_hsalsa20(
424                    so_out.as_mut_ptr(),
425                    data.as_ptr(),
426                    key.as_ptr(),
427                    std::ptr::null(),
428                );
429                assert_eq!(ret, 0);
430            }
431            assert_eq!(
432                general_purpose::STANDARD.encode(out),
433                general_purpose::STANDARD.encode(so_out)
434            );
435        }
436    }
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            "\nSummary: {} 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    }
582}