Skip to main content

dryoc/
keypair.rs

1//! # Public/secret keypair tools
2//!
3//! Provides an implementation for handling public/private keypairs based on
4//! libsodium's crypto_box, which uses X25519.
5//!
6//! With the `protected` feature, [`KeyPair`] also provides locked-memory
7//! constructors such as [`KeyPair::generate_locked_keypair`]; the key types
8//! come from [`protected`](crate::protected).
9
10use core::fmt;
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14use zeroize::{Zeroize, ZeroizeOnDrop};
15
16use crate::classic::crypto_box::crypto_box_seed_keypair_inplace;
17use crate::constants::{
18    CRYPTO_BOX_BEFORENMBYTES, CRYPTO_BOX_PUBLICKEYBYTES, CRYPTO_BOX_SECRETKEYBYTES,
19    CRYPTO_BOX_SEEDBYTES,
20};
21use crate::error::Error;
22use crate::precalc::PrecalcSecretKey;
23use crate::types::*;
24use crate::utils::ct_eq_bytes;
25
26/// Stack-allocated public key type alias.
27pub type PublicKey = StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES>;
28/// Stack-allocated secret key type alias.
29pub type SecretKey = StackByteArray<CRYPTO_BOX_SECRETKEYBYTES>;
30/// Stack-allocated key pair type alias.
31pub type StackKeyPair = KeyPair<PublicKey, SecretKey>;
32
33#[derive(Zeroize, ZeroizeOnDrop, Clone)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35/// Public/secret keypair for use with [`crate::dryocbox::DryocBox`] and
36/// libsodium-compatible public-key encryption.
37///
38/// Create keypairs with [`KeyPair::generate`], [`KeyPair::from_seed`], or
39/// [`KeyPair::from_secret_key`]. There is no `new` or [`Default`]
40/// constructor, so an all-zero secret key is never produced implicitly.
41pub struct KeyPair<
42    PublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
43    SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
44> {
45    /// Public key
46    pub public_key: PublicKey,
47    /// Secret key
48    pub secret_key: SecretKey,
49}
50
51impl<
52    PublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
53    SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
54> fmt::Debug for KeyPair<PublicKey, SecretKey>
55{
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.debug_struct("KeyPair")
58            .field("public_key", &"[REDACTED]")
59            .field("secret_key", &"[REDACTED]")
60            .finish()
61    }
62}
63
64impl<
65    PublicKey: NewByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
66    SecretKey: NewByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
67> KeyPair<PublicKey, SecretKey>
68{
69    /// Generates a random keypair.
70    #[must_use]
71    pub fn generate() -> Self {
72        use crate::classic::crypto_box::crypto_box_keypair_inplace;
73
74        let mut public_key = PublicKey::new_byte_array();
75        let mut secret_key = SecretKey::new_byte_array();
76        crypto_box_keypair_inplace(public_key.as_mut_array(), secret_key.as_mut_array());
77
78        Self {
79            public_key,
80            secret_key,
81        }
82    }
83
84    /// Derives the public key for `secret_key` and returns the complete
85    /// keypair, consuming the secret key.
86    #[must_use]
87    pub fn from_secret_key(secret_key: SecretKey) -> Self {
88        use crate::classic::crypto_core::crypto_scalarmult_base;
89
90        let mut public_key = PublicKey::new_byte_array();
91        crypto_scalarmult_base(public_key.as_mut_array(), secret_key.as_array());
92
93        Self {
94            public_key,
95            secret_key,
96        }
97    }
98
99    /// Deterministically derives a keypair from `seed`.
100    #[must_use]
101    pub fn from_seed<Seed: ByteArray<CRYPTO_BOX_SEEDBYTES>>(seed: &Seed) -> Self {
102        let mut public_key = PublicKey::new_byte_array();
103        let mut secret_key = SecretKey::new_byte_array();
104
105        crypto_box_seed_keypair_inplace(
106            public_key.as_mut_array(),
107            secret_key.as_mut_array(),
108            seed.as_array(),
109        );
110
111        Self {
112            public_key,
113            secret_key,
114        }
115    }
116}
117
118impl<
119    'a,
120    PublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + core::convert::TryFrom<&'a [u8]> + Zeroize,
121    SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES> + core::convert::TryFrom<&'a [u8]> + Zeroize,
122> KeyPair<PublicKey, SecretKey>
123{
124    /// Constructs a new keypair from key slices, consuming them. Does not check
125    /// validity or authenticity of keypair.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if either slice does not have the required key length,
130    /// or if the target key type rejects the key bytes.
131    pub fn from_slices(public_key: &'a [u8], secret_key: &'a [u8]) -> Result<Self, Error> {
132        validate_length!(
133            exact CRYPTO_BOX_PUBLICKEYBYTES,
134            public_key.len(),
135            crate::ErrorContext::PublicKey
136        );
137        validate_length!(
138            exact CRYPTO_BOX_SECRETKEYBYTES,
139            secret_key.len(),
140            crate::ErrorContext::SecretKey
141        );
142
143        Ok(Self {
144            public_key: PublicKey::try_from(public_key)
145                .map_err(|_| Error::invalid_key(crate::ErrorContext::PublicKey))?,
146            secret_key: SecretKey::try_from(secret_key)
147                .map_err(|_| Error::invalid_key(crate::ErrorContext::SecretKey))?,
148        })
149    }
150}
151
152/// Checks if the given public key is valid according to X25519 rules.
153///
154/// For X25519 ([`crypto_box`](`crate::classic::crypto_box`),
155/// [`DryocBox`](`crate::dryocbox::DryocBox`)), this performs a trial scalar
156/// multiplication and rejects public keys that produce an all-zero shared
157/// secret, including low-order inputs rejected by libsodium. As required by
158/// RFC 7748, the high bit of the encoded public key is ignored.
159///
160/// Use [`crate::sign::is_valid_public_key`] for Ed25519 signing keys.
161///
162/// ## Validating Protected Keys
163///
164/// You can validate keys stored in protected memory directly, as the
165/// validation functions operate on references.
166///
167/// ```
168/// # #[cfg(all(feature = "protected", any(unix, windows)))]
169/// # {
170/// use dryoc::constants::{CRYPTO_BOX_PUBLICKEYBYTES, CRYPTO_BOX_SECRETKEYBYTES};
171/// use dryoc::keypair::{KeyPair, is_valid_public_key};
172/// use dryoc::protected::{HeapByteArray, LockedRO};
173///
174/// // Generate a keypair stored in locked, read-only memory
175/// let protected_kp: KeyPair<
176///     LockedRO<HeapByteArray<CRYPTO_BOX_PUBLICKEYBYTES>>,
177///     LockedRO<HeapByteArray<CRYPTO_BOX_SECRETKEYBYTES>>,
178/// > = KeyPair::generate_readonly_locked_keypair().expect("Failed to generate locked keypair");
179///
180/// // Validate the X25519 public key.
181/// assert!(
182///     is_valid_public_key(&protected_kp.public_key),
183///     "Protected X25519 key should be valid"
184/// );
185/// # }
186/// ```
187#[must_use]
188pub fn is_valid_public_key<PK: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>>(key: &PK) -> bool {
189    let scalar = [0u8; CRYPTO_BOX_SECRETKEYBYTES];
190    let mut shared_secret = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
191
192    crate::classic::crypto_core::crypto_scalarmult(&mut shared_secret, &scalar, key.as_array())
193        .is_ok()
194}
195
196impl<
197    PublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
198    SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
199> KeyPair<PublicKey, SecretKey>
200{
201    /// Computes a stack-allocated shared secret key using a secret key from
202    /// this keypair and `third_party_public_key`.
203    ///
204    /// Compatible with libsodium's `crypto_box_beforenm`.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if `third_party_public_key` is an unacceptable
209    /// low-order point.
210    #[inline]
211    pub fn precalculate<OtherPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>>(
212        &self,
213        third_party_public_key: &OtherPublicKey,
214    ) -> Result<PrecalcSecretKey<StackByteArray<CRYPTO_BOX_BEFORENMBYTES>>, Error> {
215        PrecalcSecretKey::precalculate(third_party_public_key, &self.secret_key)
216    }
217}
218
219#[cfg(any(
220    all(feature = "protected", any(unix, windows)),
221    all(doc, not(doctest), feature = "std")
222))]
223#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
224mod protected {
225    //! Locked-memory constructors and methods for [`KeyPair`].
226    use super::*;
227    use crate::classic::crypto_box::crypto_box_keypair_inplace;
228    use crate::protected::*;
229
230    impl
231        KeyPair<
232            Locked<HeapByteArray<CRYPTO_BOX_PUBLICKEYBYTES>>,
233            Locked<HeapByteArray<CRYPTO_BOX_SECRETKEYBYTES>>,
234        >
235    {
236        /// Returns a new randomly generated locked keypair.
237        ///
238        /// # Errors
239        ///
240        /// Returns [`Error::Io`] if either allocation cannot be locked,
241        /// commonly because the process has reached its locked-memory limit.
242        ///
243        /// # Panics
244        ///
245        /// Panics if either page-aligned allocation cannot be created, its
246        /// size cannot be represented with guard pages, or the operating
247        /// system's random number generator fails.
248        pub fn generate_locked_keypair() -> Result<Self, Error> {
249            let mut res = Self {
250                public_key: HeapByteArray::<CRYPTO_BOX_PUBLICKEYBYTES>::new_locked()?,
251                secret_key: HeapByteArray::<CRYPTO_BOX_SECRETKEYBYTES>::new_locked()?,
252            };
253
254            crypto_box_keypair_inplace(
255                res.public_key.as_mut_array(),
256                res.secret_key.as_mut_array(),
257            );
258
259            Ok(res)
260        }
261
262        /// Computes a heap-allocated, page-aligned, locked shared secret key
263        /// using a secret key from this keypair and
264        /// `third_party_public_key`.
265        ///
266        /// Compatible with libsodium's `crypto_box_beforenm`.
267        ///
268        /// # Errors
269        ///
270        /// Returns an error if `third_party_public_key` is an unacceptable
271        /// low-order point or the shared-key allocation cannot be locked.
272        ///
273        /// # Panics
274        ///
275        /// Panics if the page-aligned shared-key allocation cannot be created
276        /// or its size cannot be represented with guard pages.
277        #[inline]
278        pub fn precalculate_locked<OtherPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>>(
279            &self,
280            third_party_public_key: &OtherPublicKey,
281        ) -> Result<PrecalcSecretKey<Locked<HeapByteArray<CRYPTO_BOX_BEFORENMBYTES>>>, Error>
282        {
283            PrecalcSecretKey::precalculate_locked(third_party_public_key, &self.secret_key)
284        }
285    }
286
287    impl
288        KeyPair<
289            LockedRO<HeapByteArray<CRYPTO_BOX_PUBLICKEYBYTES>>,
290            LockedRO<HeapByteArray<CRYPTO_BOX_SECRETKEYBYTES>>,
291        >
292    {
293        /// Returns a new randomly generated locked, read-only keypair.
294        ///
295        /// # Errors
296        ///
297        /// Returns [`Error::Io`] if either allocation cannot be locked or its
298        /// page permissions cannot be changed to read-only.
299        ///
300        /// # Panics
301        ///
302        /// Panics if either page-aligned allocation cannot be created, its
303        /// size cannot be represented with guard pages, or the operating
304        /// system's random number generator fails.
305        pub fn generate_readonly_locked_keypair() -> Result<Self, Error> {
306            let mut public_key = HeapByteArray::<CRYPTO_BOX_PUBLICKEYBYTES>::new_locked()?;
307            let mut secret_key = HeapByteArray::<CRYPTO_BOX_SECRETKEYBYTES>::new_locked()?;
308
309            crypto_box_keypair_inplace(public_key.as_mut_array(), secret_key.as_mut_array());
310
311            let public_key = public_key.mprotect_readonly()?;
312            let secret_key = secret_key.mprotect_readonly()?;
313
314            Ok(Self {
315                public_key,
316                secret_key,
317            })
318        }
319
320        /// Computes a heap-allocated, page-aligned, locked, read-only shared
321        /// secret key using a secret key from this keypair and
322        /// `third_party_public_key`.
323        ///
324        /// Compatible with libsodium's `crypto_box_beforenm`.
325        ///
326        /// # Errors
327        ///
328        /// Returns an error if `third_party_public_key` is an unacceptable
329        /// low-order point, the shared-key allocation cannot be locked, or its
330        /// page permissions cannot be changed to read-only.
331        ///
332        /// # Panics
333        ///
334        /// Panics if the page-aligned shared-key allocation cannot be created
335        /// or its size cannot be represented with guard pages.
336        #[inline]
337        pub fn precalculate_readonly_locked<
338            OtherPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
339        >(
340            &self,
341            third_party_public_key: &OtherPublicKey,
342        ) -> Result<PrecalcSecretKey<LockedRO<HeapByteArray<CRYPTO_BOX_BEFORENMBYTES>>>, Error>
343        {
344            PrecalcSecretKey::precalculate_readonly_locked(third_party_public_key, &self.secret_key)
345        }
346    }
347}
348
349impl<
350    PublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
351    SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
352> PartialEq<KeyPair<PublicKey, SecretKey>> for KeyPair<PublicKey, SecretKey>
353{
354    fn eq(&self, other: &Self) -> bool {
355        ct_eq_bytes(self.public_key.as_slice(), other.public_key.as_slice())
356            && ct_eq_bytes(self.secret_key.as_slice(), other.secret_key.as_slice())
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn keypair_debug_redacts_keys() {
366        let keypair = StackKeyPair::generate();
367        let debug = format!("{keypair:?}");
368
369        assert_eq!(
370            debug,
371            "KeyPair { public_key: \"[REDACTED]\", secret_key: \"[REDACTED]\" }"
372        );
373    }
374
375    #[test]
376    fn test_from_secret_key() {
377        let keypair_1 = KeyPair::<
378            StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
379            StackByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
380        >::generate();
381        let keypair_2 = KeyPair::from_secret_key(keypair_1.secret_key.clone());
382
383        assert_eq!(keypair_1.public_key, keypair_2.public_key);
384    }
385
386    /// `crypto_box_seed_keypair` outputs from libsodium for the all-`0x01` and
387    /// all-`0x02` seeds.
388    const SEED_KEYPAIRS: [([u8; CRYPTO_BOX_SEEDBYTES], &str, &str); 2] = [
389        (
390            [1u8; CRYPTO_BOX_SEEDBYTES],
391            "1b1b58dd50ea14b60da17b790cd02754d970c9bab864ebb3c0f3016fe51d3f57",
392            "5ce86efb75fa4e2c410f46e16de9f6acae1a1703528651b69bc176c088bef3ee",
393        ),
394        (
395            [2u8; CRYPTO_BOX_SEEDBYTES],
396            "60346e7c911a5f6ba154129174cafe75b294ac3bbd5549632f48cec6266f8410",
397            "aa3c626bc9c38c8c201878ebb1d5b0b50ac40e8986c78793db1d4ef369fca1ce",
398        ),
399    ];
400
401    /// RFC 7748 section 6.1 / NaCl `tests/box.c` keys and their
402    /// `crypto_box_beforenm` shared key.
403    const ALICE_SK: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
404    const ALICE_PK: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
405    const BOB_SK: &str = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
406    const BOB_PK: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
407    const SHARED_KEY: &str = "1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389";
408
409    fn keypair_from_hex(public_key: &str, secret_key: &str) -> StackKeyPair {
410        KeyPair::from_slices(
411            &hex::decode(public_key).expect("hex"),
412            &hex::decode(secret_key).expect("hex"),
413        )
414        .expect("keypair")
415    }
416
417    #[test]
418    fn from_seed_matches_libsodium_and_classic_seed_keypair() {
419        for (seed, public_key, secret_key) in SEED_KEYPAIRS {
420            let keypair: StackKeyPair = KeyPair::from_seed(&seed);
421            assert_eq!(keypair, keypair_from_hex(public_key, secret_key));
422
423            let (classic_pk, classic_sk) =
424                crate::classic::crypto_box::crypto_box_seed_keypair(&seed);
425            assert_eq!(keypair.public_key.as_array(), &classic_pk);
426            assert_eq!(keypair.secret_key.as_array(), &classic_sk);
427
428            assert_eq!(
429                KeyPair::from_secret_key(keypair.secret_key.clone()),
430                keypair
431            );
432            assert!(is_valid_public_key(&keypair.public_key));
433        }
434        assert_ne!(
435            StackKeyPair::from_seed(&SEED_KEYPAIRS[0].0),
436            StackKeyPair::from_seed(&SEED_KEYPAIRS[1].0)
437        );
438    }
439
440    #[test]
441    fn generated_public_key_is_the_base_point_multiple_of_the_secret_key() {
442        use crate::classic::crypto_core::crypto_scalarmult_base;
443
444        let keypair = StackKeyPair::generate();
445        let mut public_key = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
446        crypto_scalarmult_base(&mut public_key, keypair.secret_key.as_array());
447        assert_eq!(keypair.public_key.as_array(), &public_key);
448        assert_eq!(
449            KeyPair::from_secret_key(keypair.secret_key.clone()),
450            keypair
451        );
452    }
453
454    #[test]
455    fn from_slices_accepts_exact_lengths_and_reports_the_short_side() {
456        let alice = keypair_from_hex(ALICE_PK, ALICE_SK);
457        assert_eq!(
458            alice.public_key.as_slice(),
459            hex::decode(ALICE_PK).expect("hex")
460        );
461        assert_eq!(
462            alice.secret_key.as_slice(),
463            hex::decode(ALICE_SK).expect("hex")
464        );
465        assert_eq!(
466            StackKeyPair::from_secret_key(alice.secret_key.clone()).public_key,
467            alice.public_key
468        );
469
470        for len in [
471            0,
472            CRYPTO_BOX_PUBLICKEYBYTES - 1,
473            CRYPTO_BOX_PUBLICKEYBYTES + 1,
474        ] {
475            assert!(matches!(
476                StackKeyPair::from_slices(&vec![0u8; len], alice.secret_key.as_slice()),
477                Err(Error::InvalidLength {
478                    context: crate::ErrorContext::PublicKey,
479                    actual,
480                    ..
481                }) if actual == len
482            ));
483        }
484        for len in [
485            0,
486            CRYPTO_BOX_SECRETKEYBYTES - 1,
487            CRYPTO_BOX_SECRETKEYBYTES + 1,
488        ] {
489            assert!(matches!(
490                StackKeyPair::from_slices(alice.public_key.as_slice(), &vec![0u8; len]),
491                Err(Error::InvalidLength {
492                    context: crate::ErrorContext::SecretKey,
493                    actual,
494                    ..
495                }) if actual == len
496            ));
497        }
498    }
499
500    #[test]
501    fn precalculate_matches_nacl_shared_key() {
502        let alice = keypair_from_hex(ALICE_PK, ALICE_SK);
503        let bob = keypair_from_hex(BOB_PK, BOB_SK);
504        let expected = hex::decode(SHARED_KEY).expect("hex");
505
506        let from_alice = alice.precalculate(&bob.public_key).expect("precalc");
507        let from_bob = bob.precalculate(&alice.public_key).expect("precalc");
508        assert_eq!(from_alice.as_slice(), expected.as_slice());
509        assert_eq!(from_alice, from_bob);
510        assert!(alice.precalculate(&PublicKey::default()).is_err());
511    }
512
513    #[cfg(all(feature = "protected", any(unix, windows)))]
514    #[test]
515    fn locked_precalculate_matches_nacl_shared_key() {
516        use crate::protected::*;
517
518        let alice = keypair_from_hex(ALICE_PK, ALICE_SK);
519        let bob = keypair_from_hex(BOB_PK, BOB_SK);
520        let expected = hex::decode(SHARED_KEY).expect("hex");
521        let locked_alice = KeyPair {
522            public_key: HeapByteArray::<CRYPTO_BOX_PUBLICKEYBYTES>::from_slice_into_locked(
523                alice.public_key.as_slice(),
524            )
525            .expect("lock pk"),
526            secret_key: HeapByteArray::<CRYPTO_BOX_SECRETKEYBYTES>::from_slice_into_locked(
527                alice.secret_key.as_slice(),
528            )
529            .expect("lock sk"),
530        };
531
532        let locked = locked_alice
533            .precalculate_locked(&bob.public_key)
534            .expect("precalc locked");
535        assert_eq!(locked.as_slice(), expected.as_slice());
536        assert!(
537            locked_alice
538                .precalculate_locked(&PublicKey::default())
539                .is_err()
540        );
541    }
542
543    #[cfg(all(feature = "serde", feature = "alloc"))]
544    #[test]
545    fn serde_round_trip_keeps_the_keypair_usable_for_boxes() {
546        use crate::dryocbox::{DryocBox, Nonce, VecBox};
547
548        let alice = keypair_from_hex(ALICE_PK, ALICE_SK);
549        let bob = keypair_from_hex(BOB_PK, BOB_SK);
550
551        let json = serde_json::to_string(&bob).expect("serialize");
552        let decoded: StackKeyPair = serde_json::from_str(&json).expect("deserialize");
553        assert_eq!(decoded, bob);
554
555        let nonce = Nonce::from([3u8; crate::constants::CRYPTO_BOX_NONCEBYTES]);
556        let dryocbox =
557            DryocBox::encrypt_to_vecbox(b"for bob", &nonce, &decoded.public_key, &alice.secret_key)
558                .expect("encrypt");
559        assert_eq!(
560            VecBox::from_bytes(&dryocbox.to_vec())
561                .expect("parse")
562                .decrypt_to_vec(&nonce, &alice.public_key, &decoded.secret_key)
563                .expect("decrypt"),
564            b"for bob"
565        );
566
567        // Swapping the encoded fields yields a keypair that cannot open the
568        // box.
569        let swapped = json
570            .replacen("public_key", "tmp", 1)
571            .replacen("secret_key", "public_key", 1)
572            .replacen("tmp", "secret_key", 1);
573        let swapped: StackKeyPair = serde_json::from_str(&swapped).expect("deserialize");
574        assert_ne!(swapped, bob);
575        assert!(
576            dryocbox
577                .decrypt_to_vec(&nonce, &alice.public_key, &swapped.secret_key)
578                .is_err()
579        );
580    }
581
582    #[test]
583    fn test_is_valid_public_key() {
584        // Known valid key (assuming it meets X25519 criteria)
585        // This specific key is also a valid Ed25519 key.
586        let valid_pk_bytes = [
587            215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114,
588            243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26,
589        ];
590        let valid_pk = PublicKey::from(valid_pk_bytes);
591        assert!(
592            is_valid_public_key(&valid_pk),
593            "Known valid key failed validation"
594        );
595
596        // RFC 7748 requires the high bit to be ignored when decoding X25519
597        // public keys.
598        let mut high_bit_bytes = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
599        high_bit_bytes[0] = 9;
600        high_bit_bytes[31] = 0x80;
601        let high_bit = PublicKey::from(high_bit_bytes);
602        assert!(
603            is_valid_public_key(&high_bit),
604            "RFC 7748 high-bit encoding should be accepted"
605        );
606
607        // Invalid: Zero point
608        let zero_bytes = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
609        let zero_pk = PublicKey::from(zero_bytes);
610        assert!(!is_valid_public_key(&zero_pk), "Zero key should be invalid");
611
612        let mut identity_bytes = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
613        identity_bytes[0] = 1;
614        let identity = PublicKey::from(identity_bytes);
615        assert!(
616            !is_valid_public_key(&identity),
617            "Low-order key should be invalid"
618        );
619
620        // Generated key should be valid
621        let kp = StackKeyPair::generate();
622        assert!(
623            is_valid_public_key(&kp.public_key),
624            "Generated key failed validation"
625        );
626    }
627
628    #[cfg(dryoc_native_tests)]
629    mod native_tests {
630        use super::*;
631
632        #[test]
633        fn test_gen_keypair() {
634            use crate::classic::crypto_core::crypto_scalarmult_base;
635            use crate::native_test_util::scalarmult_curve25519_base;
636
637            let keypair = KeyPair::<
638                StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
639                StackByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
640            >::generate();
641
642            let mut public_key = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
643            crypto_scalarmult_base(&mut public_key, keypair.secret_key.as_array());
644
645            assert_eq!(keypair.public_key.as_array(), &public_key);
646
647            let ge = scalarmult_curve25519_base(&keypair.secret_key);
648
649            assert_eq!(ge, public_key);
650        }
651    }
652}