Skip to main content

dryoc/classic/
crypto_core.rs

1use subtle::ConstantTimeEq;
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::edwards25519::Point;
10use crate::error::Error;
11use crate::scalarmult_curve25519::{
12    crypto_scalarmult_curve25519, crypto_scalarmult_curve25519_base,
13};
14use crate::types::*;
15use crate::utils::{SIGMA, load_u32_le};
16
17/// Stack-allocated HChaCha20 input.
18pub type HChaCha20Input = [u8; CRYPTO_CORE_HCHACHA20_INPUTBYTES];
19/// Stack-allocated HChaCha20 key.
20pub type HChaCha20Key = [u8; CRYPTO_CORE_HCHACHA20_KEYBYTES];
21/// Stack-allocated HChaCha20 output.
22pub type HChaCha20Output = [u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
23/// Stack-allocated HSalsa20 input.
24pub type HSalsa20Input = [u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
25/// Stack-allocated HSalsa20 key.
26pub type HSalsa20Key = [u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
27/// Stack-allocated HSalsa20 output.
28pub type HSalsa20Output = [u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
29/// Stack-allocated Ed25519 point.
30pub type Ed25519Point = [u8; CRYPTO_CORE_ED25519_BYTES];
31
32/// Computes the public key for a previously generated secret key.
33///
34/// Compatible with libsodium's `crypto_scalarmult_base`.
35pub fn crypto_scalarmult_base(
36    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
37    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
38) {
39    crypto_scalarmult_curve25519_base(q, n)
40}
41
42/// Computes a shared secret `q`, given `n`, our secret key, and `p`, their
43/// public key, using a Diffie-Hellman key exchange.
44///
45/// Compatible with libsodium's `crypto_scalarmult`.
46///
47/// # Errors
48///
49/// Returns an error if `p` is an unacceptable low-order public key that
50/// produces an all-zero shared secret.
51pub fn crypto_scalarmult(
52    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
53    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
54    p: &[u8; CRYPTO_SCALARMULT_BYTES],
55) -> Result<(), Error> {
56    crypto_scalarmult_curve25519(q, n, p);
57
58    if q.ct_eq(&[0u8; CRYPTO_SCALARMULT_BYTES]).into() {
59        Err(Error::invalid_key(crate::ErrorContext::Curve25519PublicKey))
60    } else {
61        Ok(())
62    }
63}
64
65/// [`crypto_scalarmult`] of `p` and [`crypto_scalarmult_base`] with the same
66/// secret `n`, sharing one field inversion (X-Wing needs both). `public` is
67/// only meaningful when this returns `Ok`.
68///
69/// # Errors
70///
71/// As [`crypto_scalarmult`].
72pub(crate) fn crypto_scalarmult_and_base(
73    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
74    public: &mut [u8; CRYPTO_SCALARMULT_BYTES],
75    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
76    p: &[u8; CRYPTO_SCALARMULT_BYTES],
77) -> Result<(), Error> {
78    crate::scalarmult_curve25519::crypto_scalarmult_curve25519_and_base(q, public, n, p);
79
80    if q.ct_eq(&[0u8; CRYPTO_SCALARMULT_BYTES]).into() {
81        Err(Error::invalid_key(crate::ErrorContext::Curve25519PublicKey))
82    } else {
83        Ok(())
84    }
85}
86
87/// Implements the HChaCha20 function.
88///
89/// Compatible with libsodium's `crypto_core_hchacha20`.
90pub fn crypto_core_hchacha20(
91    output: &mut HChaCha20Output,
92    input: &HChaCha20Input,
93    key: &HChaCha20Key,
94    constants: Option<(u32, u32, u32, u32)>,
95) {
96    let input = input.as_array();
97    let key = key.as_array();
98    let (c0, c1, c2, c3) = constants.unwrap_or((SIGMA[0], SIGMA[1], SIGMA[2], SIGMA[3]));
99    // Loads and stores spelled out: at opt-level `z` and `s` the `zip`
100    // length helpers are out of line and got iterators over `x`, which kept
101    // the key-loaded state in stack memory nothing wipes.
102    let (k, _) = key.as_chunks::<4>();
103    let (n, _) = input.as_chunks::<4>();
104    let mut x = [c0, c1, c2, c3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
105    x[4] = u32::from_le_bytes(k[0]);
106    x[5] = u32::from_le_bytes(k[1]);
107    x[6] = u32::from_le_bytes(k[2]);
108    x[7] = u32::from_le_bytes(k[3]);
109    x[8] = u32::from_le_bytes(k[4]);
110    x[9] = u32::from_le_bytes(k[5]);
111    x[10] = u32::from_le_bytes(k[6]);
112    x[11] = u32::from_le_bytes(k[7]);
113    x[12] = u32::from_le_bytes(n[0]);
114    x[13] = u32::from_le_bytes(n[1]);
115    x[14] = u32::from_le_bytes(n[2]);
116    x[15] = u32::from_le_bytes(n[3]);
117
118    crate::chacha20::rounds(&mut x);
119
120    // Words 0..4 and 12..16 of the permuted state.
121    let (out, _) = output.as_chunks_mut::<4>();
122    out[0] = x[0].to_le_bytes();
123    out[1] = x[1].to_le_bytes();
124    out[2] = x[2].to_le_bytes();
125    out[3] = x[3].to_le_bytes();
126    out[4] = x[12].to_le_bytes();
127    out[5] = x[13].to_le_bytes();
128    out[6] = x[14].to_le_bytes();
129    out[7] = x[15].to_le_bytes();
130}
131
132/// Checks whether `p` is a valid prime-order Ed25519 point.
133///
134/// This validates the canonical compressed encoding, rejects points that are
135/// not on the curve or have small order, and requires membership in the main
136/// subgroup. The high bit is the sign of the x-coordinate and may legitimately
137/// be set.
138///
139/// # Example
140///
141/// ```
142/// use dryoc::classic::crypto_core::crypto_core_ed25519_is_valid_point;
143/// use dryoc::classic::crypto_sign::crypto_sign_keypair;
144///
145/// let (pk, _) = crypto_sign_keypair();
146/// assert!(crypto_core_ed25519_is_valid_point(&pk));
147/// ```
148///
149/// # Compatibility
150///
151/// This matches `crypto_core_ed25519_is_valid_point` in libsodium 1.0.21 and
152/// later. Libsodium versions through 1.0.20 incorrectly accepted some
153/// mixed-order points; this function rejects them.
154#[must_use]
155pub fn crypto_core_ed25519_is_valid_point(p: &Ed25519Point) -> bool {
156    decompress_prime_order_ed25519_point(p).is_some()
157}
158
159/// Decompresses `p` only if it is a canonical encoding of a point in the
160/// prime-order subgroup: not small order and torsion free.
161pub(crate) fn decompress_prime_order_ed25519_point(p: &Ed25519Point) -> Option<Point> {
162    decompress_canonical_ed25519_point(p)
163        .filter(|point| !point.is_small_order() && ed25519_is_torsion_free(point))
164}
165
166/// Whether `point` lies in the prime-order subgroup, i.e. `[L]P` is the
167/// identity for the basepoint order `L`.
168///
169/// The multiplication runs in variable time with respect to the scalar: `L`
170/// is a public constant and the points checked here are public keys and
171/// encodings, so nothing secret is involved, and the NAF form of `L` needs a
172/// third fewer point additions than a constant-time fixed-window
173/// multiplication.
174pub(crate) fn ed25519_is_torsion_free(point: &Point) -> bool {
175    point.is_torsion_free_vartime()
176}
177
178/// Decompresses an Ed25519 point only if its encoding is canonical.
179///
180/// Decompression reduces the encoded y-coordinate modulo the field prime, and
181/// any sign bit decodes when `x == 0`. Checking the encoding first rejects
182/// alternate encodings of the same point without recompressing the result,
183/// which would cost a field inversion.
184pub(crate) fn decompress_canonical_ed25519_point(p: &Ed25519Point) -> Option<Point> {
185    if !is_canonical_ed25519_encoding(p) {
186        return None;
187    }
188    Point::decompress(p)
189}
190
191/// Whether `p` is the unique encoding of the point it decodes to (if any):
192/// its y-coordinate is below the field prime `2^255 - 19`, and its sign bit
193/// is clear when `y` is `1` or `-1`, the only y-coordinates with `x == 0`.
194///
195/// This is the byte-level equivalent of decompressing and recompressing the
196/// point, which is how a non-canonical encoding would otherwise be detected.
197fn is_canonical_ed25519_encoding(p: &Ed25519Point) -> bool {
198    let sign = p[31] >> 7;
199    let y_top = p[31] & 0x7f;
200    let y_middle_all_ones = p[1..31].iter().all(|&b| b == 0xff);
201    let y_middle_all_zero = p[1..31].iter().all(|&b| b == 0);
202
203    // y >= p: bits 8..255 all set (p = 2^255 - 19 has them set) and the low
204    // byte at least 0xed.
205    let y_at_least_p = y_top == 0x7f && y_middle_all_ones && p[0] >= 0xed;
206    let y_is_one = y_top == 0 && y_middle_all_zero && p[0] == 1;
207    let y_is_minus_one = y_top == 0x7f && y_middle_all_ones && p[0] == 0xec;
208
209    !y_at_least_p && !(sign == 1 && (y_is_one || y_is_minus_one))
210}
211
212#[inline]
213fn salsa20_rotl32(x: u32, y: u32, rot: u32) -> u32 {
214    x.wrapping_add(y).rotate_left(rot)
215}
216
217/// Implements the HSalsa20 function.
218///
219/// Compatible with libsodium's `crypto_core_hsalsa20`.
220pub fn crypto_core_hsalsa20(
221    output: &mut HSalsa20Output,
222    input: &HSalsa20Input,
223    key: &HSalsa20Key,
224    constants: Option<(u32, u32, u32, u32)>,
225) {
226    let (mut x0, mut x5, mut x10, mut x15) =
227        constants.unwrap_or((SIGMA[0], SIGMA[1], SIGMA[2], SIGMA[3]));
228    let (
229        mut x1,
230        mut x2,
231        mut x3,
232        mut x4,
233        mut x11,
234        mut x12,
235        mut x13,
236        mut x14,
237        mut x6,
238        mut x7,
239        mut x8,
240        mut x9,
241    ) = (
242        load_u32_le(&key[0..4]),
243        load_u32_le(&key[4..8]),
244        load_u32_le(&key[8..12]),
245        load_u32_le(&key[12..16]),
246        load_u32_le(&key[16..20]),
247        load_u32_le(&key[20..24]),
248        load_u32_le(&key[24..28]),
249        load_u32_le(&key[28..32]),
250        load_u32_le(&input[0..4]),
251        load_u32_le(&input[4..8]),
252        load_u32_le(&input[8..12]),
253        load_u32_le(&input[12..16]),
254    );
255
256    for _ in (0..20).step_by(2) {
257        x4 ^= salsa20_rotl32(x0, x12, 7);
258        x8 ^= salsa20_rotl32(x4, x0, 9);
259        x12 ^= salsa20_rotl32(x8, x4, 13);
260        x0 ^= salsa20_rotl32(x12, x8, 18);
261        x9 ^= salsa20_rotl32(x5, x1, 7);
262        x13 ^= salsa20_rotl32(x9, x5, 9);
263        x1 ^= salsa20_rotl32(x13, x9, 13);
264        x5 ^= salsa20_rotl32(x1, x13, 18);
265        x14 ^= salsa20_rotl32(x10, x6, 7);
266        x2 ^= salsa20_rotl32(x14, x10, 9);
267        x6 ^= salsa20_rotl32(x2, x14, 13);
268        x10 ^= salsa20_rotl32(x6, x2, 18);
269        x3 ^= salsa20_rotl32(x15, x11, 7);
270        x7 ^= salsa20_rotl32(x3, x15, 9);
271        x11 ^= salsa20_rotl32(x7, x3, 13);
272        x15 ^= salsa20_rotl32(x11, x7, 18);
273        x1 ^= salsa20_rotl32(x0, x3, 7);
274        x2 ^= salsa20_rotl32(x1, x0, 9);
275        x3 ^= salsa20_rotl32(x2, x1, 13);
276        x0 ^= salsa20_rotl32(x3, x2, 18);
277        x6 ^= salsa20_rotl32(x5, x4, 7);
278        x7 ^= salsa20_rotl32(x6, x5, 9);
279        x4 ^= salsa20_rotl32(x7, x6, 13);
280        x5 ^= salsa20_rotl32(x4, x7, 18);
281        x11 ^= salsa20_rotl32(x10, x9, 7);
282        x8 ^= salsa20_rotl32(x11, x10, 9);
283        x9 ^= salsa20_rotl32(x8, x11, 13);
284        x10 ^= salsa20_rotl32(x9, x8, 18);
285        x12 ^= salsa20_rotl32(x15, x14, 7);
286        x13 ^= salsa20_rotl32(x12, x15, 9);
287        x14 ^= salsa20_rotl32(x13, x12, 13);
288        x15 ^= salsa20_rotl32(x14, x13, 18);
289    }
290
291    // Array chunk stores: at opt-level `z` and `s` `copy_from_slice` is out
292    // of line and took each subkey word through a stack temporary.
293    let (out, _) = output.as_chunks_mut::<4>();
294    out[0] = x0.to_le_bytes();
295    out[1] = x5.to_le_bytes();
296    out[2] = x10.to_le_bytes();
297    out[3] = x15.to_le_bytes();
298    out[4] = x6.to_le_bytes();
299    out[5] = x7.to_le_bytes();
300    out[6] = x8.to_le_bytes();
301    out[7] = x9.to_le_bytes();
302}
303
304#[cfg(test)]
305mod tests {
306    use curve25519_dalek::edwards::CompressedEdwardsY;
307
308    use super::*;
309    use crate::classic::crypto_sign::crypto_sign_keypair;
310    use crate::edwards25519::test_vectors::{
311        IDENTITY, NONCANONICAL_IDENTITY, mixed_order_point, torsion_point,
312    };
313    use crate::scalarmult_curve25519::test_vectors::low_order_u_encodings;
314    use crate::test_prelude::*;
315
316    #[test]
317    fn test_crypto_core_ed25519_is_valid_point() {
318        let basepoint = curve25519_dalek::constants::ED25519_BASEPOINT_COMPRESSED.to_bytes();
319        assert!(crypto_core_ed25519_is_valid_point(&basepoint));
320
321        let mut negative_basepoint = basepoint;
322        negative_basepoint[31] |= 0x80;
323        assert!(
324            crypto_core_ed25519_is_valid_point(&negative_basepoint),
325            "the high bit is a valid x-coordinate sign bit"
326        );
327
328        assert!(!crypto_core_ed25519_is_valid_point(&IDENTITY));
329
330        assert!(
331            decompress_canonical_ed25519_point(&NONCANONICAL_IDENTITY).is_none(),
332            "p + 1 must not be accepted as an alternate encoding of the identity"
333        );
334        assert!(!crypto_core_ed25519_is_valid_point(&NONCANONICAL_IDENTITY));
335
336        let torsion = torsion_point();
337        assert!(torsion.is_small_order());
338        assert!(!crypto_core_ed25519_is_valid_point(
339            &torsion.compress().to_bytes()
340        ));
341
342        let mixed_order = mixed_order_point();
343        assert!(!mixed_order.is_small_order());
344        assert!(!mixed_order.is_torsion_free());
345        assert!(!crypto_core_ed25519_is_valid_point(
346            &mixed_order.compress().to_bytes()
347        ));
348
349        let mut point_not_on_curve = [0u8; CRYPTO_CORE_ED25519_BYTES];
350        point_not_on_curve[0] = 2;
351        assert!(!crypto_core_ed25519_is_valid_point(&point_not_on_curve));
352        assert!(!crypto_core_ed25519_is_valid_point(
353            &[0u8; CRYPTO_CORE_ED25519_BYTES]
354        ));
355    }
356
357    #[test]
358    fn test_generated_ed25519_keys_are_valid_points() {
359        for _ in 0..25 {
360            let (ed25519_pk, _) = crypto_sign_keypair();
361            assert!(crypto_core_ed25519_is_valid_point(&ed25519_pk));
362        }
363    }
364
365    /// The variable-time subgroup check must agree with dalek's constant-time
366    /// one on prime-order, small-order and mixed-order points.
367    #[test]
368    fn test_torsion_check_matches_dalek() {
369        let mut points = vec![curve25519_dalek::constants::ED25519_BASEPOINT_POINT];
370        // Every torsion class is retained; fewer generated prime-order
371        // points keep the interpreted cross-product bounded.
372        for _ in 0..if cfg!(miri) { 2 } else { 32 } {
373            let (pk, _) = crypto_sign_keypair();
374            points.push(CompressedEdwardsY(pk).decompress().unwrap());
375        }
376        let prime_order = points.clone();
377        for torsion in curve25519_dalek::constants::EIGHT_TORSION {
378            points.push(torsion);
379            for point in &prime_order {
380                points.push(point + torsion);
381            }
382        }
383        for point in points {
384            let ours = Point::decompress(&point.compress().to_bytes()).unwrap();
385            assert_eq!(ed25519_is_torsion_free(&ours), point.is_torsion_free());
386        }
387    }
388
389    /// The byte-level canonical check must accept exactly the encodings that
390    /// survive a decompress/recompress round trip.
391    #[test]
392    fn test_canonical_encoding_check_matches_recompression() {
393        let round_trip = |p: &Ed25519Point| {
394            let compressed = CompressedEdwardsY(*p);
395            compressed
396                .decompress()
397                .is_some_and(|point| point.compress() == compressed)
398        };
399        let mut cases = Vec::new();
400        // Every y in p - 2 ..= 2^255 - 1 (canonical, then the 19 non-canonical
401        // encodings of 0..18), and small y, each with both sign bits.
402        for low in 0xebu8..=0xff {
403            let mut point = [0xff; CRYPTO_CORE_ED25519_BYTES];
404            point[0] = low;
405            point[31] = 0x7f;
406            cases.push(point);
407        }
408        for low in 0u8..=20 {
409            let mut point = [0; CRYPTO_CORE_ED25519_BYTES];
410            point[0] = low;
411            cases.push(point);
412        }
413        cases.push(curve25519_dalek::constants::ED25519_BASEPOINT_COMPRESSED.to_bytes());
414        for torsion in curve25519_dalek::constants::EIGHT_TORSION {
415            cases.push(torsion.compress().to_bytes());
416        }
417        for _ in 0..if cfg!(miri) { 4 } else { 64 } {
418            cases.push(crypto_sign_keypair().0);
419            let mut random = [0u8; CRYPTO_CORE_ED25519_BYTES];
420            crate::rng::copy_randombytes(&mut random);
421            cases.push(random);
422        }
423        for mut point in cases {
424            for sign in [0u8, 0x80] {
425                point[31] = (point[31] & 0x7f) | sign;
426                assert_eq!(
427                    decompress_canonical_ed25519_point(&point).is_some(),
428                    round_trip(&point),
429                    "{point:02x?}"
430                );
431            }
432        }
433    }
434
435    /// Mixed-order points that libsodium through 1.0.20 accepted: `y = 9`,
436    /// and the regression vector added when libsodium fixed its main
437    /// subgroup check (a prime-order point plus order-two torsion).
438    fn legacy_libsodium_mixed_order_points() -> [[u8; CRYPTO_CORE_ED25519_BYTES]; 2] {
439        let mut y_is_nine = [0u8; CRYPTO_CORE_ED25519_BYTES];
440        y_is_nine[0] = 9;
441        let mut order_two_coset = [0x99; CRYPTO_CORE_ED25519_BYTES];
442        order_two_coset[0] = 0x95;
443        [y_is_nine, order_two_coset]
444    }
445
446    #[test]
447    fn test_crypto_core_ed25519_rejects_legacy_libsodium_mixed_order_points() {
448        for point in legacy_libsodium_mixed_order_points() {
449            let decoded = decompress_canonical_ed25519_point(&point)
450                .expect("regression vector must be a canonical curve point");
451            assert!(!decoded.is_small_order());
452            assert!(!ed25519_is_torsion_free(&decoded));
453            assert!(!crypto_core_ed25519_is_valid_point(&point));
454        }
455    }
456
457    /// Every low-order `u` (libsodium's blacklist, with and without bit 255)
458    /// is rejected, and the all-zero shared secret it produces is what the
459    /// caller's buffer holds afterwards.
460    #[test]
461    fn test_crypto_scalarmult_rejects_low_order_points() {
462        let scalar = [0x42; CRYPTO_SCALARMULT_SCALARBYTES];
463
464        for public_key in low_order_u_encodings() {
465            let mut shared_secret = [0xa5; CRYPTO_SCALARMULT_BYTES];
466            assert!(
467                matches!(
468                    crypto_scalarmult(&mut shared_secret, &scalar, &public_key),
469                    Err(Error::InvalidKey {
470                        context: crate::ErrorContext::Curve25519PublicKey,
471                    })
472                ),
473                "{public_key:02x?}"
474            );
475            assert_eq!(shared_secret, [0u8; CRYPTO_SCALARMULT_BYTES]);
476        }
477    }
478
479    #[test]
480    fn test_crypto_scalarmult_ignores_public_key_high_bit() {
481        let scalar = [0x42; CRYPTO_SCALARMULT_SCALARBYTES];
482        let mut canonical = [0u8; CRYPTO_SCALARMULT_BYTES];
483        canonical[0] = 9;
484        let mut high_bit_set = canonical;
485        high_bit_set[CRYPTO_SCALARMULT_BYTES - 1] = 0x80;
486        let mut canonical_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
487        let mut high_bit_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
488
489        crypto_scalarmult(&mut canonical_secret, &scalar, &canonical).unwrap();
490        crypto_scalarmult(&mut high_bit_secret, &scalar, &high_bit_set).unwrap();
491
492        assert_eq!(canonical_secret, high_bit_secret);
493    }
494
495    /// Non-default constants that differ in every word, so a constant in the
496    /// wrong position or order changes the output.
497    const CUSTOM_CONSTANTS: Constants = (0x0123_4567, 0x89ab_cdef, 0xfedc_ba98, 0x7654_3210);
498
499    /// The default constants passed explicitly.
500    const SIGMA_CONSTANTS: Constants = (SIGMA[0], SIGMA[1], SIGMA[2], SIGMA[3]);
501
502    /// The `constants` argument of the HChaCha20 and HSalsa20 functions.
503    type Constants = (u32, u32, u32, u32);
504
505    /// HChaCha20 test vector of draft-irtf-cfrg-xchacha-03 section 2.2.1 with
506    /// the default constants, implied and explicit, and the same key and
507    /// input with [`CUSTOM_CONSTANTS`] (expected output from libsodium
508    /// 1.0.22's `crypto_core_hchacha20`).
509    #[test]
510    fn test_crypto_core_hchacha20_known_answers() {
511        let key: HChaCha20Key = core::array::from_fn(|i| i as u8);
512        let input: HChaCha20Input = hex::decode("000000090000004a0000000031415927")
513            .unwrap()
514            .try_into()
515            .unwrap();
516        let default = "82413b4227b27bfed30e42508a877d73a0f9e4d58a74a853c12ec41326d3ecdc";
517        for (constants, expected) in [
518            (None, default),
519            (Some(SIGMA_CONSTANTS), default),
520            (
521                Some(CUSTOM_CONSTANTS),
522                "e887f97849587bad0f41aa2b5596fe9967e2785acc6ecc13c7c4e4a02016bd76",
523            ),
524        ] {
525            let mut output = HChaCha20Output::default();
526            crypto_core_hchacha20(&mut output, &input, &key, constants);
527            assert_eq!(hex::encode(output), expected, "{constants:x?}");
528        }
529    }
530
531    /// HSalsa20 test vectors of NaCl's `tests/core1.c` and `tests/core2.c`
532    /// (the XSalsa20 subkeys of the crypto_box example) with the default
533    /// constants, implied and explicit, and the `core1` key and input with
534    /// [`CUSTOM_CONSTANTS`] (expected output from libsodium 1.0.22's
535    /// `crypto_core_hsalsa20`).
536    #[test]
537    fn test_crypto_core_hsalsa20_known_answers() {
538        let shared = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
539        let firstkey = "1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389";
540        let nonce_prefix = "69696ee955b62b73cd62bda875fc73d6";
541        let zero = "00000000000000000000000000000000";
542        for (key, input, constants, expected) in [
543            (shared, zero, None, firstkey),
544            (shared, zero, Some(SIGMA_CONSTANTS), firstkey),
545            (
546                firstkey,
547                nonce_prefix,
548                None,
549                "dc908dda0b9344a953629b733820778880f3ceb421bb61b91cbd4c3e66256ce4",
550            ),
551            (
552                shared,
553                zero,
554                Some(CUSTOM_CONSTANTS),
555                "7ee2bcfe4eec7e4e59e64a4e7b0a97a001f2ad95c10247115f0d261afa7c092c",
556            ),
557        ] {
558            let key: HSalsa20Key = hex::decode(key).unwrap().try_into().unwrap();
559            let input: HSalsa20Input = hex::decode(input).unwrap().try_into().unwrap();
560            let mut output = HSalsa20Output::default();
561            crypto_core_hsalsa20(&mut output, &input, &key, constants);
562            assert_eq!(hex::encode(output), expected, "{constants:x?}");
563        }
564    }
565
566    #[cfg(dryoc_native_tests)]
567    mod native_tests {
568        use super::*;
569        use crate::classic::crypto_box::*;
570        use crate::scalarmult_curve25519::test_vectors::field_prime_plus;
571
572        #[test]
573        fn test_crypto_core_ed25519_is_valid_point_matches_libsodium() {
574            use libsodium_sys::crypto_core_ed25519_is_valid_point as sodium_is_valid_point;
575
576            crate::native_test_util::init();
577
578            let basepoint = curve25519_dalek::constants::ED25519_BASEPOINT_COMPRESSED.to_bytes();
579            let mut negative_basepoint = basepoint;
580            negative_basepoint[31] |= 0x80;
581            let torsion = torsion_point();
582            let mixed_order = mixed_order_point().compress().to_bytes();
583
584            for point in [
585                basepoint,
586                negative_basepoint,
587                IDENTITY,
588                NONCANONICAL_IDENTITY,
589                torsion.compress().to_bytes(),
590                mixed_order,
591                [0u8; CRYPTO_CORE_ED25519_BYTES],
592            ]
593            .into_iter()
594            .chain(legacy_libsodium_mixed_order_points())
595            {
596                let dryoc_result = crypto_core_ed25519_is_valid_point(&point);
597                let sodium_result = unsafe { sodium_is_valid_point(point.as_ptr()) } == 1;
598                assert_eq!(dryoc_result, sodium_result, "point: {point:02x?}");
599            }
600
601            for _ in 0..20 {
602                let (public_key, _) = crypto_sign_keypair();
603                let sodium_result = unsafe { sodium_is_valid_point(public_key.as_ptr()) } == 1;
604                assert!(sodium_result);
605                assert_eq!(
606                    crypto_core_ed25519_is_valid_point(&public_key),
607                    sodium_result
608                );
609            }
610        }
611
612        #[test]
613        fn test_crypto_scalarmult_base() {
614            use base64::Engine as _;
615            use base64::engine::general_purpose;
616            for _ in 0..20 {
617                use crate::native_test_util::scalarmult_curve25519_base;
618
619                let (pk, sk) = crypto_box_keypair();
620
621                let mut public_key = [0u8; CRYPTO_SCALARMULT_BYTES];
622                crypto_scalarmult_base(&mut public_key, &sk);
623
624                assert_eq!(&pk, &public_key);
625
626                let ge = scalarmult_curve25519_base(&sk);
627
628                assert_eq!(
629                    general_purpose::STANDARD.encode(ge),
630                    general_purpose::STANDARD.encode(public_key)
631                );
632            }
633        }
634
635        #[test]
636        fn test_crypto_scalarmult() {
637            use base64::Engine as _;
638            use base64::engine::general_purpose;
639            for _ in 0..20 {
640                use crate::native_test_util::scalarmult_curve25519;
641
642                let (_our_pk, our_sk) = crypto_box_keypair();
643                let (their_pk, _their_sk) = crypto_box_keypair();
644
645                let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
646                crypto_scalarmult(&mut shared_secret, &our_sk, &their_pk)
647                    .expect("scalarmult failed");
648
649                let ge = scalarmult_curve25519(&our_sk, &their_pk).expect("scalarmult failed");
650
651                assert_eq!(
652                    general_purpose::STANDARD.encode(ge),
653                    general_purpose::STANDARD.encode(shared_secret)
654                );
655            }
656        }
657
658        /// libsodium refuses the same fourteen low-order encodings (its
659        /// `has_small_order` blacklist compares with bit 255 masked), and
660        /// both sides reject them for every scalar tried.
661        #[test]
662        fn test_crypto_scalarmult_low_order_compatibility() {
663            use libsodium_sys::crypto_scalarmult as sodium_scalarmult;
664
665            crate::native_test_util::init();
666
667            let mut rng = crate::utils::test_util::XorShift64::new(0x3c6e_f372_fe94_f82b);
668            let scalars = [[0u8; 32], [0xff; 32], [0x42; 32], rng.next_bytes32()];
669
670            for public_key in low_order_u_encodings() {
671                for scalar in scalars {
672                    let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
673                    assert!(
674                        crypto_scalarmult(&mut shared_secret, &scalar, &public_key).is_err(),
675                        "{public_key:02x?}"
676                    );
677                    let mut sodium_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
678                    let sodium_result = unsafe {
679                        sodium_scalarmult(
680                            sodium_secret.as_mut_ptr(),
681                            scalar.as_ptr(),
682                            public_key.as_ptr(),
683                        )
684                    };
685                    assert_eq!(sodium_result, -1, "{public_key:02x?}");
686                }
687            }
688        }
689
690        /// Non-canonical `u` encodings (`p + j`, with and without bit 255)
691        /// are accepted and give libsodium's output, which is the output for
692        /// the reduced `j`.
693        #[test]
694        fn test_crypto_scalarmult_noncanonical_compatibility() {
695            use libsodium_sys::crypto_scalarmult as sodium_scalarmult;
696
697            crate::native_test_util::init();
698
699            let mut rng = crate::utils::test_util::XorShift64::new(0xa54f_f53a_5f1d_36f1);
700            // 0 and 1 are low order (tested above); 2..=18 reach 2^255 - 1.
701            for j in 2..=18u8 {
702                let scalar = rng.next_bytes32();
703                let mut canonical = [0u8; CRYPTO_SCALARMULT_BYTES];
704                canonical[0] = j;
705                let mut expected = [0u8; CRYPTO_SCALARMULT_BYTES];
706                crypto_scalarmult(&mut expected, &scalar, &canonical).unwrap();
707
708                let unreduced = field_prime_plus(j as i8);
709                let mut unreduced_high = unreduced;
710                unreduced_high[31] |= 0x80;
711                for public_key in [unreduced, unreduced_high] {
712                    let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
713                    crypto_scalarmult(&mut shared_secret, &scalar, &public_key).unwrap();
714                    let mut sodium_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
715                    let sodium_result = unsafe {
716                        sodium_scalarmult(
717                            sodium_secret.as_mut_ptr(),
718                            scalar.as_ptr(),
719                            public_key.as_ptr(),
720                        )
721                    };
722                    assert_eq!(sodium_result, 0, "j {j}");
723                    assert_eq!(shared_secret, sodium_secret, "j {j}");
724                    assert_eq!(shared_secret, expected, "j {j}");
725                }
726            }
727        }
728
729        /// Constants for the libsodium comparisons: the defaults implied
730        /// (`None`, a null `c`), the defaults passed explicitly, and random
731        /// ones, each with the 16 little-endian bytes libsodium's `c` takes.
732        fn constant_cases() -> [(Option<Constants>, Option<[u8; 16]>); 3] {
733            fn le_bytes((c0, c1, c2, c3): Constants) -> [u8; 16] {
734                let mut bytes = [0u8; 16];
735                for (chunk, word) in bytes
736                    .as_chunks_mut::<4>()
737                    .0
738                    .iter_mut()
739                    .zip([c0, c1, c2, c3])
740                {
741                    *chunk = word.to_le_bytes();
742                }
743                bytes
744            }
745
746            let mut random = [0u8; 16];
747            crate::rng::copy_randombytes(&mut random);
748            let word = |i: usize| load_u32_le(&random[4 * i..4 * i + 4]);
749            let random = (word(0), word(1), word(2), word(3));
750            let sigma = (SIGMA[0], SIGMA[1], SIGMA[2], SIGMA[3]);
751            [
752                (None, None),
753                (Some(sigma), Some(le_bytes(sigma))),
754                (Some(random), Some(le_bytes(random))),
755            ]
756        }
757
758        #[test]
759        fn test_crypto_core_hchacha20() {
760            use libsodium_sys::crypto_core_hchacha20 as so_crypto_core_hchacha20;
761
762            use crate::rng::copy_randombytes;
763
764            crate::native_test_util::init();
765
766            for _ in 0..10 {
767                let mut key = [0u8; CRYPTO_CORE_HCHACHA20_KEYBYTES];
768                let mut data = [0u8; CRYPTO_CORE_HCHACHA20_INPUTBYTES];
769                copy_randombytes(&mut key);
770                copy_randombytes(&mut data);
771
772                for (constants, c) in constant_cases() {
773                    let mut out = [0u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
774                    crypto_core_hchacha20(&mut out, &data, &key, constants);
775
776                    let mut so_out = [0u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
777                    let ret = unsafe {
778                        so_crypto_core_hchacha20(
779                            so_out.as_mut_ptr(),
780                            data.as_ptr(),
781                            key.as_ptr(),
782                            c.as_ref().map_or(core::ptr::null(), |c| c.as_ptr()),
783                        )
784                    };
785                    assert_eq!(ret, 0);
786                    assert_eq!(out, so_out, "{constants:x?}");
787                }
788            }
789        }
790
791        #[test]
792        fn test_crypto_core_hsalsa20() {
793            use libsodium_sys::crypto_core_hsalsa20 as so_crypto_core_hsalsa20;
794
795            use crate::rng::copy_randombytes;
796
797            crate::native_test_util::init();
798
799            for _ in 0..10 {
800                let mut key = [0u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
801                let mut data = [0u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
802                copy_randombytes(&mut key);
803                copy_randombytes(&mut data);
804
805                for (constants, c) in constant_cases() {
806                    let mut out = [0u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
807                    crypto_core_hsalsa20(&mut out, &data, &key, constants);
808
809                    let mut so_out = [0u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
810                    let ret = unsafe {
811                        so_crypto_core_hsalsa20(
812                            so_out.as_mut_ptr(),
813                            data.as_ptr(),
814                            key.as_ptr(),
815                            c.as_ref().map_or(core::ptr::null(), |c| c.as_ptr()),
816                        )
817                    };
818                    assert_eq!(ret, 0);
819                    assert_eq!(out, so_out, "{constants:x?}");
820                }
821            }
822        }
823    }
824}