aucpace_conflux/
server.rs

1use crate::Database;
2use crate::constants::MIN_SSID_LEN;
3use crate::utils::{
4    H0, compute_authenticator_messages, compute_first_session_key, compute_session_key,
5    compute_ssid, generate_keypair, generate_nonce, generate_server_keypair,
6};
7use crate::{Error, Result};
8use core::marker::PhantomData;
9use curve25519_dalek::traits::IsIdentity;
10use curve25519_dalek::{
11    digest::consts::U64,
12    digest::{Digest, Output},
13    ristretto::RistrettoPoint,
14    scalar::Scalar,
15};
16use password_hash::{ParamsString, SaltString};
17use rand_core::{TryCryptoRng, TryRngCore};
18use subtle::ConstantTimeEq;
19
20#[cfg(feature = "partial_augmentation")]
21use crate::database::PartialAugDatabase;
22
23#[cfg(feature = "strong_aucpace")]
24use crate::database::StrongDatabase;
25
26#[cfg(feature = "serde")]
27use crate::utils::{serde_paramsstring, serde_saltstring};
28
29#[cfg(feature = "serde")]
30use serde::{Deserialize, Serialize};
31
32/// A non-copy wrapper around u64
33#[derive(Clone)]
34struct ServerSecret(u64);
35
36impl ServerSecret {
37    fn new<CSPRNG: TryRngCore + TryCryptoRng>(rng: &mut CSPRNG) -> Result<Self> {
38        Ok(Self(rng.try_next_u64().map_err(|_| Error::Rng)?))
39    }
40}
41
42/// Implementation of the server side of the `AuCPace` protocol
43pub struct AuCPaceServer<D, CSPRNG, const K1: usize>
44where
45    D: Digest + Default,
46    CSPRNG: TryRngCore + TryCryptoRng,
47{
48    /// The CSPRNG used to generate random values where needed
49    rng: CSPRNG,
50
51    /// the secret used to obscure when a password lookup failed
52    secret: ServerSecret,
53
54    d: PhantomData<D>,
55}
56
57impl<D, CSPRNG, const K1: usize> AuCPaceServer<D, CSPRNG, K1>
58where
59    D: Digest<OutputSize = U64> + Default,
60    CSPRNG: TryRngCore + TryCryptoRng,
61{
62    /// Create a new server
63    pub fn new(mut rng: CSPRNG) -> Result<Self> {
64        let secret = ServerSecret::new(&mut rng)?;
65        Ok(Self {
66            rng,
67            secret,
68            d: PhantomData,
69        })
70    }
71
72    /// Create a new server in the SSID agreement phase
73    ///
74    /// # Return:
75    /// ([`next_step`](AuCPaceServerSsidEstablish), [`message`](ServerMessage::Nonce))
76    /// - [`next_step`](AuCPaceServerSsidEstablish): the server in the SSID establishment stage
77    /// - [`message`](ServerMessage::Nonce): the message to send to the server
78    ///
79    pub fn begin(
80        &mut self,
81    ) -> Result<(
82        AuCPaceServerSsidEstablish<D, K1>,
83        ServerMessage<'static, K1>,
84    )> {
85        let next_step = AuCPaceServerSsidEstablish::new(self.secret.clone(), &mut self.rng)?;
86        let message = ServerMessage::Nonce(next_step.nonce);
87        Ok((next_step, message))
88    }
89
90    /// Create a new server in the Augmentation layer phase, provided an SSID
91    ///
92    /// # Argument:
93    /// `ssid`: Some data to be hashed and act as the sub-session ID
94    ///
95    /// # Return:
96    /// - Ok([`next_step`](AuCPaceServerAugLayer)): the server in the SSID establishment stage
97    /// - Err([`Error::InsecureSsid`](Error::InsecureSsid)): the SSID provided was not long enough to be secure
98    ///
99    pub fn begin_prestablished_ssid<S>(&mut self, ssid: S) -> Result<AuCPaceServerAugLayer<D, K1>>
100    where
101        S: AsRef<[u8]>,
102    {
103        // if the SSID isn't long enough return an error
104        if ssid.as_ref().len() < MIN_SSID_LEN {
105            return Err(Error::InsecureSsid);
106        }
107
108        // hash the SSID and begin the next step
109        let mut hasher: D = H0();
110        hasher.update(ssid);
111        let ssid_hash = hasher.finalize();
112        let next_step = AuCPaceServerAugLayer::new(self.secret.clone(), ssid_hash);
113        Ok(next_step)
114    }
115
116    /// Generate a new long-term keypair
117    ///
118    /// This is inteded to be used when registering a user when using partial augmentation.
119    /// As well as on all password changes.
120    ///
121    /// # Return:
122    /// (`private_key`, `public_key`):
123    /// - `private_key`: the private key
124    /// - `public_key`: the public key
125    ///
126    #[cfg(feature = "partial_augmentation")]
127    pub fn generate_long_term_keypair(&mut self) -> Result<(Scalar, RistrettoPoint)> {
128        generate_server_keypair::<D, _>(&mut self.rng)
129    }
130}
131
132/// Server in the SSID agreement phase
133#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
134pub struct AuCPaceServerSsidEstablish<D, const K1: usize>
135where
136    D: Digest<OutputSize = U64> + Default,
137{
138    #[zeroize(skip)]
139    secret: ServerSecret,
140    nonce: [u8; K1],
141    _d: PhantomData<D>,
142}
143
144impl<D, const K1: usize> AuCPaceServerSsidEstablish<D, K1>
145where
146    D: Digest<OutputSize = U64> + Default,
147{
148    fn new<CSPRNG>(secret: ServerSecret, rng: &mut CSPRNG) -> Result<Self>
149    where
150        CSPRNG: TryRngCore + TryCryptoRng,
151    {
152        Ok(Self {
153            secret,
154            nonce: generate_nonce(rng)?,
155            _d: PhantomData,
156        })
157    }
158
159    /// Consume the client's nonce - `t` and progress to the augmentation layer
160    ///
161    /// # arguments:
162    /// - `client_nonce` - the nonce received from the server
163    ///
164    /// # return:
165    /// [`next_step`](AuCPaceServerAugLayer): the server in the augmentation layer
166    ///
167    #[must_use]
168    pub fn agree_ssid(self, client_nonce: [u8; K1]) -> AuCPaceServerAugLayer<D, K1> {
169        let ssid = compute_ssid::<D, K1>(self.nonce, client_nonce);
170        AuCPaceServerAugLayer::new(self.secret.clone(), ssid)
171    }
172}
173
174/// Server in the Augmentation layer phase
175#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
176pub struct AuCPaceServerAugLayer<D, const K1: usize>
177where
178    D: Digest<OutputSize = U64> + Default,
179{
180    #[zeroize(skip)]
181    secret: ServerSecret,
182    #[zeroize(skip)]
183    ssid: Output<D>,
184}
185
186impl<D, const K1: usize> AuCPaceServerAugLayer<D, K1>
187where
188    D: Digest<OutputSize = U64> + Default,
189{
190    const fn new(secret: ServerSecret, ssid: Output<D>) -> Self {
191        Self { secret, ssid }
192    }
193
194    /// Accept the user's username and generate the `ClientInfo` for the response.
195    /// Moves the protocol into the `CPace` substep phase
196    ///
197    /// # Arguments:
198    /// - `username`: the client's username
199    /// - `database`: the password verifier database to retrieve the client's information from
200    ///
201    /// # Return:
202    /// ([`next_step`](AuCPaceServerCPaceSubstep), [`message`](ServerMessage::AugmentationInfo))
203    /// - [`next_step`](AuCPaceServerCPaceSubstep): the server in the `CPace` substep stage
204    /// - [`message`](ServerMessage::AugmentationInfo): the message to send to the client
205    ///
206    pub fn generate_client_info<U, DB, CSPRNG>(
207        self,
208        username: U,
209        database: &DB,
210        mut rng: CSPRNG,
211    ) -> Result<(
212        AuCPaceServerCPaceSubstep<D, CSPRNG, K1>,
213        ServerMessage<'static, K1>,
214    )>
215    where
216        U: AsRef<[u8]>,
217        DB: Database<PasswordVerifier = RistrettoPoint>,
218        CSPRNG: TryRngCore + TryCryptoRng,
219    {
220        let (x, x_pub) = generate_server_keypair::<D, _>(&mut rng)?;
221
222        // generate the prs and client message
223        let (prs, message) = self.generate_prs(username.as_ref(), database, &mut rng, x, x_pub)?;
224        let next_step = AuCPaceServerCPaceSubstep::new(self.ssid, prs, rng);
225
226        Ok((next_step, message))
227    }
228
229    /// Accept the user's username and generate the `ClientInfo` for the response.
230    /// Moves the protocol into the `CPace` substep phase
231    ///
232    /// This method performs the "partial augmentation" variant of the protocol.
233    /// This means that instead of generating `x` and `x_pub` as ephemeral keys, a long term keypair is
234    /// retrieved from the database instead. This comes with decreased security in the case of
235    /// server compromise but significantly decreases the amount of computation the server has to
236    /// do. The reference paper goes into more detail on the tradeoffs and why you might choose to
237    /// use this method.
238    ///
239    /// # Arguments:
240    /// - `username`: the client's username
241    /// - `database`: the password verifier database to retrieve the client's information from
242    ///    This is a `PartialAugDatabase` so we can lookup the server's long term keypair.
243    ///
244    /// # Return:
245    /// ([`next_step`](AuCPaceServerCPaceSubstep), [`message`](ServerMessage::AugmentationInfo))
246    /// - [`next_step`](AuCPaceServerCPaceSubstep): the server in the `CPace` substep stage
247    /// - [`message`](ServerMessage::AugmentationInfo): the message to send to the client
248    ///
249    #[cfg(feature = "partial_augmentation")]
250    pub fn generate_client_info_partial_aug<U, DB, CSPRNG>(
251        self,
252        username: U,
253        database: &DB,
254        mut rng: CSPRNG,
255    ) -> Result<(
256        AuCPaceServerCPaceSubstep<D, CSPRNG, K1>,
257        ServerMessage<'static, K1>,
258    )>
259    where
260        U: AsRef<[u8]>,
261        DB: Database<PasswordVerifier = RistrettoPoint>
262            + PartialAugDatabase<PrivateKey = Scalar, PublicKey = RistrettoPoint>,
263        CSPRNG: TryRngCore + TryCryptoRng,
264    {
265        let user = username.as_ref();
266        let (prs, message) = if let Some((x, x_pub)) = database.lookup_long_term_keypair(user) {
267            // generate the prs and client message
268            self.generate_prs(user, database, &mut rng, x, x_pub)?
269        } else {
270            // if the user does not have a keypair stored then we generate a random point on the
271            // curve to be the public key, and handle the failed lookup as normal
272            let x_pub = {
273                let mut seed = [0u8; 32];
274                rng.try_fill_bytes(&mut seed).map_err(|_| Error::Rng)?;
275                let mut hasher: D = crate::utils::H1();
276                hasher.update(&seed);
277                RistrettoPoint::from_hash(hasher)
278            };
279            self.lookup_failed(user, x_pub, &mut rng)?
280        };
281        let next_step = AuCPaceServerCPaceSubstep::new(self.ssid, prs, rng);
282
283        Ok((next_step, message))
284    }
285
286    /// Accept the user's username, and blinded point U and generate the `ClientInfo` for the response.
287    /// Moves the protocol into the `CPace` substep phase
288    ///
289    /// This method performs the Strong variant of the protocol.
290    /// This means that the information is blinded in transit so that it is impossible to do any
291    /// precomputation to attack the user's password before the actual verifier database is compromised.
292    ///
293    /// # Arguments:
294    /// - `username`: the client's username
295    /// - `blinded`: the client's blinded point `U`
296    /// - `database`: the password verifier database to retrieve the client's information from
297    ///    This is a `PartialAugDatabase` so we can lookup the server's long term keypair.
298    ///
299    /// # Return:
300    /// ([`next_step`](AuCPaceServerCPaceSubstep), [`message`](ServerMessage::AugmentationInfo))
301    /// - [`next_step`](AuCPaceServerCPaceSubstep): the server in the `CPace` substep stage
302    /// - [`message`](ServerMessage::AugmentationInfo): the message to send to the client
303    ///
304    #[cfg(feature = "strong_aucpace")]
305    pub fn generate_client_info_strong<U, DB, CSPRNG>(
306        self,
307        username: U,
308        blinded: RistrettoPoint,
309        database: &DB,
310        mut rng: CSPRNG,
311    ) -> Result<(
312        AuCPaceServerCPaceSubstep<D, CSPRNG, K1>,
313        ServerMessage<'static, K1>,
314    )>
315    where
316        U: AsRef<[u8]>,
317        DB: StrongDatabase<PasswordVerifier = RistrettoPoint, Exponent = Scalar>,
318        CSPRNG: TryRngCore + TryCryptoRng,
319    {
320        let (x, x_pub) = generate_server_keypair::<D, _>(&mut rng)?;
321
322        // generate the prs and client message
323        let (prs, message) =
324            self.generate_prs_strong(username.as_ref(), blinded, database, &mut rng, x, x_pub)?;
325        let next_step = AuCPaceServerCPaceSubstep::new(self.ssid, prs, rng);
326
327        Ok((next_step, message))
328    }
329
330    /// Accept the user's username, and blinded point U and generate the `ClientInfo` for the response.
331    /// Moves the protocol into the `CPace` substep phase
332    ///
333    /// This method performs the Strong + Partially augmented variant of the protocol.
334    /// This means that the information is blinded in transit so that it is impossible to do any
335    /// precomputation to attack the user's password before the actual verifier database is compromised.
336    /// And that the server looks up the user's long term keypair in the database instead of generating it.
337    ///
338    /// # Arguments:
339    /// - `username`: the client's username
340    /// - `blinded`: the client's blinded point `U`
341    /// - `database`: the password verifier database to retrieve the client's information from
342    ///    This is a `PartialAugDatabase` so we can lookup the server's long term keypair.
343    ///
344    /// # Return:
345    /// ([`next_step`](AuCPaceServerCPaceSubstep), [`message`](ServerMessage::AugmentationInfo))
346    /// - [`next_step`](AuCPaceServerCPaceSubstep): the server in the `CPace` substep stage
347    /// - [`message`](ServerMessage::AugmentationInfo): the message to send to the client
348    ///
349    #[cfg(all(feature = "strong_aucpace", feature = "partial_augmentation"))]
350    pub fn generate_client_info_partial_strong<U, DB, CSPRNG>(
351        self,
352        username: U,
353        blinded: RistrettoPoint,
354        database: &DB,
355        mut rng: CSPRNG,
356    ) -> Result<(
357        AuCPaceServerCPaceSubstep<D, CSPRNG, K1>,
358        ServerMessage<'static, K1>,
359    )>
360    where
361        U: AsRef<[u8]>,
362        DB: StrongDatabase<PasswordVerifier = RistrettoPoint, Exponent = Scalar>
363            + PartialAugDatabase<PrivateKey = Scalar, PublicKey = RistrettoPoint>,
364        CSPRNG: TryRngCore + TryCryptoRng,
365    {
366        let user = username.as_ref();
367        let (prs, message) = if let Some((x, x_pub)) = database.lookup_long_term_keypair(user) {
368            // generate the prs and client message
369            self.generate_prs_strong(user, blinded, database, &mut rng, x, x_pub)?
370        } else {
371            // if the user does not have a keypair stored then we generate a random point on the
372            // curve to be the public key, and handle the failed lookup as normal
373            let x_pub = {
374                let mut seed = [0u8; 32];
375                rng.try_fill_bytes(&mut seed).map_err(|_| Error::Rng)?;
376                let mut hasher: D = crate::utils::H1();
377                hasher.update(&seed);
378                RistrettoPoint::from_hash(hasher)
379            };
380            self.lookup_failed_strong(user, blinded, x_pub, &mut rng)?
381        };
382        let next_step = AuCPaceServerCPaceSubstep::new(self.ssid, prs, rng);
383
384        Ok((next_step, message))
385    }
386
387    /// Generate the Password Related String (PRS) and the message to be sent to the user.
388    fn generate_prs<DB, CSPRNG>(
389        &self,
390        username: &[u8],
391        database: &DB,
392        rng: &mut CSPRNG,
393        x: Scalar,
394        x_pub: RistrettoPoint,
395    ) -> Result<([u8; 32], ServerMessage<'static, K1>)>
396    where
397        DB: Database<PasswordVerifier = RistrettoPoint>,
398        CSPRNG: TryRngCore + TryCryptoRng,
399    {
400        if let Some((w, salt, sigma)) = database.lookup_verifier(username.as_ref()) {
401            let cofactor = Scalar::ONE;
402            let prs = (w * x * cofactor).compress().to_bytes();
403            let message = ServerMessage::AugmentationInfo {
404                // this will have to be provided by the trait in future
405                group: "ristretto255",
406                x_pub,
407                salt,
408                pbkdf_params: sigma,
409            };
410            Ok((prs, message))
411        } else {
412            // handle the failure case
413            self.lookup_failed(username, x_pub, rng)
414        }
415    }
416
417    /// Generate the Password Related String (PRS) and the message to be sent to the user.
418    /// This variant uses a strong database
419    #[cfg(feature = "strong_aucpace")]
420    fn generate_prs_strong<DB, CSPRNG>(
421        &self,
422        username: &[u8],
423        blinded: RistrettoPoint,
424        database: &DB,
425        rng: &mut CSPRNG,
426        x: Scalar,
427        x_pub: RistrettoPoint,
428    ) -> Result<([u8; 32], ServerMessage<'static, K1>)>
429    where
430        DB: StrongDatabase<PasswordVerifier = RistrettoPoint, Exponent = Scalar>,
431        CSPRNG: TryRngCore + TryCryptoRng,
432    {
433        if let Some((w, q, sigma)) = database.lookup_verifier_strong(username.as_ref()) {
434            let cofactor = Scalar::ONE;
435            let prs = (w * (x * cofactor)).compress().to_bytes();
436            let uq = blinded * (q * cofactor);
437            if uq.is_identity() {
438                return Err(Error::IllegalPointError);
439            }
440            let message = ServerMessage::StrongAugmentationInfo {
441                // this will have to be provided by the trait in future
442                group: "ristretto255",
443                x_pub,
444                blinded_salt: uq,
445                pbkdf_params: sigma,
446            };
447            Ok((prs, message))
448        } else {
449            // handle the failure case
450            self.lookup_failed_strong(username, blinded, x_pub, rng)
451        }
452    }
453
454    /// Generate the message for if the lookup failed
455    fn lookup_failed<CSPRNG>(
456        &self,
457        username: &[u8],
458        x_pub: RistrettoPoint,
459        rng: &mut CSPRNG,
460    ) -> Result<([u8; 32], ServerMessage<'static, K1>)>
461    where
462        CSPRNG: TryRngCore + TryCryptoRng,
463    {
464        let prs = {
465            let mut tmp = [0u8; 32];
466            rng.try_fill_bytes(&mut tmp).map_err(|_| Error::Rng)?;
467            tmp
468        };
469
470        // generate the salt from the hash of the server secret and the user's name
471        let mut hasher: D = Default::default();
472        hasher.update(self.secret.0.to_le_bytes());
473        hasher.update(username);
474        let hash = hasher.finalize();
475        let hash_bytes: &[u8] = hash.as_ref();
476
477        // It is okay to expect here because SaltString has a buffer of 64 bytes by requirement
478        // from the PHC spec. 48 bytes of data when encoded as base64 transform to 64 bytes.
479        // This gives us the most entropy possible from the hash in the SaltString.
480        let salt = SaltString::encode_b64(&hash_bytes[..48]).map_err(Error::PasswordHashing)?;
481
482        let message = ServerMessage::AugmentationInfo {
483            group: "ristretto255",
484            x_pub,
485            salt,
486            pbkdf_params: ParamsString::default(),
487        };
488
489        Ok((prs, message))
490    }
491
492    /// Generate the message for if the lookup failed
493    #[cfg(feature = "strong_aucpace")]
494    fn lookup_failed_strong<CSPRNG>(
495        &self,
496        username: &[u8],
497        blinded: RistrettoPoint,
498        x_pub: RistrettoPoint,
499        rng: &mut CSPRNG,
500    ) -> Result<([u8; 32], ServerMessage<'static, K1>)>
501    where
502        CSPRNG: TryRngCore + TryCryptoRng,
503    {
504        let prs = {
505            let mut tmp = [0u8; 32];
506            rng.try_fill_bytes(&mut tmp).map_err(|_| Error::Rng)?;
507            tmp
508        };
509
510        // generate q from the hash of the username and the server secret
511        let mut hasher: D = Default::default();
512        hasher.update(self.secret.0.to_le_bytes());
513        hasher.update(username);
514        let cofactor = Scalar::ONE;
515        let q = Scalar::from_hash(hasher);
516        let fake_blinded_salt = blinded * (q * cofactor);
517
518        // check uq isn't the neutral element
519        if fake_blinded_salt.is_identity() {
520            return Err(Error::IllegalPointError);
521        }
522
523        let message = ServerMessage::StrongAugmentationInfo {
524            group: "ristretto255",
525            x_pub,
526            blinded_salt: fake_blinded_salt,
527            pbkdf_params: ParamsString::default(),
528        };
529
530        Ok((prs, message))
531    }
532}
533
534/// Server in the `CPace` substep phase
535#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
536pub struct AuCPaceServerCPaceSubstep<D, CSPRNG, const K1: usize>
537where
538    D: Digest<OutputSize = U64> + Default,
539    CSPRNG: TryRngCore + TryCryptoRng,
540{
541    #[zeroize(skip)]
542    ssid: Output<D>,
543    prs: [u8; 32],
544    #[zeroize(skip)]
545    rng: CSPRNG,
546}
547
548impl<D, CSPRNG, const K1: usize> AuCPaceServerCPaceSubstep<D, CSPRNG, K1>
549where
550    D: Digest<OutputSize = U64> + Default,
551    CSPRNG: TryRngCore + TryCryptoRng,
552{
553    const fn new(ssid: Output<D>, prs: [u8; 32], rng: CSPRNG) -> Self {
554        Self { ssid, prs, rng }
555    }
556
557    /// Generate a public key
558    /// moving the protocol onto the second half of the `CPace` substep - Receive Server Pubkey
559    ///
560    /// # Arguments:
561    /// - `channel_identifier` - `CI` from the protocol definition, in the context of TCP/IP this
562    ///     is usually some combination of the server and client's IP address and TCP port numbers.
563    ///     It's purpose is to prevent relay attacks.
564    /// - `rng` - the CSPRNG used when generating the public/private keypair
565    ///
566    /// # Return:
567    /// ([`next_step`](AuCPaceServerRecvClientKey), [`message`](ServerMessage::PublicKey))
568    /// - [`next_step`](AuCPaceServerRecvClientKey): the server waiting for the client's public key
569    /// - [`message`](ServerMessage::PublicKey): the message to send to the client
570    ///
571    pub fn generate_public_key<CI: AsRef<[u8]>>(
572        mut self,
573        channel_identifier: CI,
574    ) -> Result<(
575        AuCPaceServerRecvClientKey<D, K1>,
576        ServerMessage<'static, K1>,
577    )> {
578        let (priv_key, pub_key) = generate_keypair::<D, CSPRNG, CI>(
579            &mut self.rng,
580            self.ssid,
581            self.prs,
582            channel_identifier,
583        )?;
584
585        let next_step = AuCPaceServerRecvClientKey::new(self.ssid, priv_key);
586        let message = ServerMessage::PublicKey(pub_key);
587
588        Ok((next_step, message))
589    }
590}
591
592/// Server in the `CPace` substep phase
593#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
594pub struct AuCPaceServerRecvClientKey<D, const K1: usize>
595where
596    D: Digest<OutputSize = U64> + Default,
597{
598    #[zeroize(skip)]
599    ssid: Output<D>,
600    priv_key: Scalar,
601}
602
603impl<D, const K1: usize> AuCPaceServerRecvClientKey<D, K1>
604where
605    D: Digest<OutputSize = U64> + Default,
606{
607    const fn new(ssid: Output<D>, priv_key: Scalar) -> Self {
608        Self { ssid, priv_key }
609    }
610
611    /// Receive the client's public key
612    /// This completes the `CPace` substep and moves the client on to explicit mutual authentication.
613    ///
614    /// # Arguments:
615    /// - `client_pubkey` - the client's public key
616    ///
617    /// # Return:
618    /// [`next_step`](AuCPaceServerExpMutAuth): the server in the Explicit Mutual Authentication phase
619    ///
620    pub fn receive_client_pubkey(
621        self,
622        client_pubkey: RistrettoPoint,
623    ) -> Result<AuCPaceServerExpMutAuth<D, K1>> {
624        // check for the neutral point
625        if client_pubkey.is_identity() {
626            return Err(Error::IllegalPointError);
627        }
628
629        let sk1 = compute_first_session_key::<D>(self.ssid, self.priv_key, client_pubkey);
630        Ok(AuCPaceServerExpMutAuth::new(self.ssid, sk1))
631    }
632
633    /// Allow exiting the protocol early in the case of implicit authentication
634    /// Note: this should only be used in special circumstances and the
635    ///       explicit mutual authentication stage should be used in all other cases
636    ///
637    /// # Arguments:
638    /// - `client_pubkey` - the client's public key
639    ///
640    /// # Return:
641    /// `sk`: the session key reached by the `AuCPace` protocol
642    ///
643    pub fn implicit_auth(
644        self,
645        client_pubkey: RistrettoPoint,
646    ) -> Result<secret_utils::wrappers::SecretKey> {
647        // check for the neutral point
648        if client_pubkey.is_identity() {
649            return Err(Error::IllegalPointError);
650        }
651
652        let sk1 = compute_first_session_key::<D>(self.ssid, self.priv_key, client_pubkey);
653        Ok(secret_utils::wrappers::SecretKey::from(
654            compute_session_key::<D>(self.ssid, sk1).as_slice().to_vec(),
655        ))
656    }
657}
658
659/// Server in the Explicity Mutual Authenticaton phase
660#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
661pub struct AuCPaceServerExpMutAuth<D, const K1: usize>
662where
663    D: Digest<OutputSize = U64> + Default,
664{
665    #[zeroize(skip)]
666    ssid: Output<D>,
667    #[zeroize(skip)]
668    sk1: Output<D>,
669}
670
671impl<D, const K1: usize> AuCPaceServerExpMutAuth<D, K1>
672where
673    D: Digest<OutputSize = U64> + Default,
674{
675    const fn new(ssid: Output<D>, sk1: Output<D>) -> Self {
676        Self { ssid, sk1 }
677    }
678
679    /// Receive the server's authenticator.
680    /// This completes the protocol and returns the derived key.
681    ///
682    /// # Arguments:
683    /// - `server_authenticator` - the server's authenticator
684    ///
685    /// # Return:
686    /// either:
687    /// - Ok((`sk`, `message`)):
688    ///     - `sk` - the session key reached by the `AuCPace` protocol
689    ///     - [`message`](ServerMessage::Authenticator) - the message to send to the client
690    /// - Err([`Error::MutualAuthFail`](Error::MutualAuthFail)): an error if the authenticator we computed doesn't match
691    ///     the client's authenticator, compared in constant time.
692    ///
693    pub fn receive_client_authenticator(
694        self,
695        client_authenticator: [u8; 64],
696    ) -> Result<(
697        secret_utils::wrappers::SecretKey,
698        ServerMessage<'static, K1>,
699    )> {
700        let (ta, tb) = compute_authenticator_messages::<D>(self.ssid, self.sk1);
701        if tb.ct_eq(&client_authenticator).into() {
702            let sk = compute_session_key::<D>(self.ssid, self.sk1);
703            let ta_arr = ta
704                .as_slice()
705                .try_into()
706                .map_err(|_| Error::HashSizeInvalid)?;
707            let message = ServerMessage::Authenticator(ta_arr);
708            Ok((
709                secret_utils::wrappers::SecretKey::from(sk.as_slice().to_vec()),
710                message,
711            ))
712        } else {
713            Err(Error::MutualAuthFail)
714        }
715    }
716}
717
718/// An enum representing the different messages the server can send to the client
719#[derive(Debug)]
720#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
721pub enum ServerMessage<'a, const K1: usize> {
722    /// SSID establishment message - the server's nonce: `s`
723    Nonce(#[cfg_attr(feature = "serde", serde(with = "serde_byte_array"))] [u8; K1]),
724
725    /// Information required for the AuCPace Augmentation layer sub-step
726    AugmentationInfo {
727        /// J from the protocol definition
728        group: &'a str,
729
730        /// X from the protocol definition
731        x_pub: RistrettoPoint,
732
733        /// the salt used with the PBKDF
734        #[cfg_attr(feature = "serde", serde(with = "serde_saltstring"))]
735        salt: SaltString,
736
737        /// the parameters for the PBKDF used - sigma from the protocol definition
738        #[cfg_attr(feature = "serde", serde(with = "serde_paramsstring"))]
739        pbkdf_params: ParamsString,
740    },
741
742    /// Information required for the AuCPace Augmentation layer sub-step
743    #[cfg(feature = "strong_aucpace")]
744    StrongAugmentationInfo {
745        /// J from the protocol definition
746        group: &'a str,
747
748        /// X from the protocol definition
749        x_pub: RistrettoPoint,
750
751        /// the blinded salt used with the PBKDF
752        blinded_salt: RistrettoPoint,
753
754        /// the parameters for the PBKDF used - sigma from the protocol definition
755        #[cfg_attr(feature = "serde", serde(with = "serde_paramsstring"))]
756        pbkdf_params: ParamsString,
757    },
758
759    /// `CPace` substep message - the server's public key: `Ya`
760    PublicKey(RistrettoPoint),
761
762    /// Explicit Mutual Authentication - the server's authenticator: `Ta`
763    Authenticator(#[cfg_attr(feature = "serde", serde(with = "serde_byte_array"))] [u8; 64]),
764}
765
766#[cfg(test)]
767mod tests {
768    #[allow(unused)]
769    use super::*;
770    #[allow(unused)]
771    use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
772
773    #[test]
774    #[cfg(all(feature = "sha2", feature = "getrandom"))]
775    fn test_server_doesnt_accept_insecure_ssid() {
776        use crate::Server;
777        use rand::rngs::OsRng;
778        let mut server = Server::new(OsRng).expect("failed to initialize server RNG");
779        let res = server.begin_prestablished_ssid("bad ssid");
780        assert!(matches!(res, Err(Error::InsecureSsid)));
781    }
782
783    #[test]
784    #[cfg(feature = "sha2")]
785    fn test_server_doesnt_accept_invalid_pubkey() {
786        use crate::utils::H0;
787        use curve25519_dalek::traits::Identity;
788        let ssid = H0::<sha2::Sha512>().finalize();
789        let aug_server: AuCPaceServerRecvClientKey<sha2::Sha512, 16> =
790            AuCPaceServerRecvClientKey::new(ssid, Scalar::from(420u32));
791        let res = aug_server.receive_client_pubkey(RistrettoPoint::identity());
792
793        if let Err(e) = res {
794            assert_eq!(e, Error::IllegalPointError);
795        } else {
796            panic!("Client accepted illegal point.");
797        }
798    }
799
800    #[test]
801    #[cfg(feature = "sha2")]
802    fn test_server_doesnt_accept_invalid_pubkey_implicit_auth() {
803        use crate::utils::H0;
804        use curve25519_dalek::traits::Identity;
805        let ssid = H0::<sha2::Sha512>().finalize();
806        let aug_server: AuCPaceServerRecvClientKey<sha2::Sha512, 16> =
807            AuCPaceServerRecvClientKey::new(ssid, Scalar::from(420u32));
808        let res = aug_server.implicit_auth(RistrettoPoint::identity());
809
810        if let Err(e) = res {
811            assert_eq!(e, Error::IllegalPointError);
812        } else {
813            panic!("Client accepted illegal point.");
814        }
815    }
816
817    #[cfg(all(feature = "sha2", feature = "strong_aucpace"))]
818    struct FakeDatabase();
819
820    #[cfg(all(feature = "sha2", feature = "strong_aucpace"))]
821    impl StrongDatabase for FakeDatabase {
822        type PasswordVerifier = RistrettoPoint;
823        type Exponent = Scalar;
824
825        fn lookup_verifier_strong(
826            &self,
827            _username: &[u8],
828        ) -> Option<(Self::PasswordVerifier, Self::Exponent, ParamsString)> {
829            Some((
830                RISTRETTO_BASEPOINT_POINT,
831                Scalar::ZERO,
832                ParamsString::default(),
833            ))
834        }
835
836        fn store_verifier_strong(
837            &mut self,
838            _username: &[u8],
839            _uad: Option<&[u8]>,
840            _verifier: Self::PasswordVerifier,
841            _secret_exponent: Self::Exponent,
842            _params: ParamsString,
843        ) {
844            unimplemented!()
845        }
846    }
847
848    #[cfg(all(feature = "sha2", feature = "strong_aucpace"))]
849    impl PartialAugDatabase for FakeDatabase {
850        type PrivateKey = Scalar;
851        type PublicKey = RistrettoPoint;
852
853        fn lookup_long_term_keypair(
854            &self,
855            _username: &[u8],
856        ) -> Option<(Self::PrivateKey, Self::PublicKey)> {
857            Some((Scalar::ZERO, RISTRETTO_BASEPOINT_POINT))
858        }
859
860        fn store_long_term_keypair(
861            &mut self,
862            _username: &[u8],
863            _priv_key: Self::PrivateKey,
864            _pub_key: Self::PublicKey,
865        ) -> Result<()> {
866            unimplemented!()
867        }
868    }
869
870    #[test]
871    #[cfg(all(feature = "sha2", feature = "getrandom", feature = "strong_aucpace"))]
872    fn test_server_doesnt_accept_invalid_uq() {
873        use crate::utils::H0;
874        use curve25519_dalek::traits::Identity;
875        use rand::rngs::OsRng;
876
877        let ssid = H0::<sha2::Sha512>().finalize();
878        let aug_server: AuCPaceServerAugLayer<sha2::Sha512, 16> =
879            AuCPaceServerAugLayer::new(ServerSecret(25519), ssid);
880        let res = aug_server.generate_client_info_strong(
881            b"bobbyyyy",
882            RistrettoPoint::identity(),
883            &FakeDatabase(),
884            OsRng,
885        );
886
887        if let Err(e) = res {
888            assert_eq!(e, Error::IllegalPointError);
889        } else {
890            panic!("Client accepted illegal point.");
891        }
892    }
893
894    #[test]
895    #[cfg(all(feature = "sha2", feature = "getrandom", feature = "strong_aucpace"))]
896    fn test_server_doesnt_accept_invalid_uq_partial() {
897        use crate::utils::H0;
898        use curve25519_dalek::traits::Identity;
899        use rand::rngs::OsRng;
900
901        let ssid = H0::<sha2::Sha512>().finalize();
902        let aug_server: AuCPaceServerAugLayer<sha2::Sha512, 16> =
903            AuCPaceServerAugLayer::new(ServerSecret(25519), ssid);
904        let res = aug_server.generate_client_info_partial_strong(
905            b"bobbyyyy",
906            RistrettoPoint::identity(),
907            &FakeDatabase(),
908            OsRng,
909        );
910
911        if let Err(e) = res {
912            assert_eq!(e, Error::IllegalPointError);
913        } else {
914            panic!("Client accepted illegal point.");
915        }
916    }
917
918    #[test]
919    #[cfg(all(feature = "sha2", feature = "getrandom"))]
920    fn test_server_lookup_failed_returns_ok() {
921        use crate::utils::H0;
922        use rand::rngs::OsRng;
923
924        // Fake DB that always returns None for lookup_verifier to force lookup_failed path
925        struct NoneDb;
926        impl Database for NoneDb {
927            type PasswordVerifier = RistrettoPoint;
928
929            fn lookup_verifier(
930                &self,
931                _username: &[u8],
932            ) -> Option<(Self::PasswordVerifier, SaltString, ParamsString)> {
933                None
934            }
935
936            fn store_verifier(
937                &mut self,
938                _username: &[u8],
939                _salt: SaltString,
940                _uad: Option<&[u8]>,
941                _verifier: Self::PasswordVerifier,
942                _params: ParamsString,
943            ) {
944                unimplemented!()
945            }
946        }
947
948        let ssid = H0::<sha2::Sha512>().finalize();
949        let aug_server: AuCPaceServerAugLayer<sha2::Sha512, 16> =
950            AuCPaceServerAugLayer::new(ServerSecret(25519), ssid);
951
952        // This should take the lookup_failed path and not panic; it should return Ok
953        let res = aug_server.generate_client_info(b"missing-user", &NoneDb, OsRng);
954
955        assert!(res.is_ok());
956        if let Ok((_next_step, ServerMessage::AugmentationInfo { .. })) = res {
957            // ok
958        } else {
959            panic!("Expected AugmentationInfo on lookup_failed path");
960        }
961    }
962}