Skip to main content

citadel_sdk/
sign_in.rs

1//! Managing a post-quantum account's sign-in factors from an authenticated session: list, add a
2//! security key, rename, remove, set the policy, regenerate the recovery codes.
3//!
4//! Every change carries a fresh step-up (the account's factors, proven again, bound to the change)
5//! except in a session signed in with a recovery code, which may only add a key and set the policy.
6//!
7//! Security keys need the server to name its relying party, with
8//! `PqAuthServerSettings::with_security_key_rp_id`. With the `security-key` feature, the client's
9//! keys are served by `SecurityKey::new(prompt).serve()` natively, or `SecurityKey::new().serve()`
10//! in a browser; without it, serve the channel yourself:
11//!
12//! ```no_run
13//! # use citadel_sdk::prelude::*;
14//! # async fn f(conn: CitadelClientServerConnection<StackedRatchet>) -> Result<(), NetworkError> {
15//! let (key, mut touches) = security_key_channel();
16//! // Serve `touches` by running each request's WebAuthn ceremony with the PRF extension, then
17//! // answering.
18//! let step_up = SignInFactors::password("hunter2").with_security_key(key);
19//! let op = SignInManagementOp::AddSecurityKey { label: "YubiKey".into() };
20//! let added = conn.manage_sign_in(op, step_up).await?;
21//! # Ok(()) }
22//! ```
23
24use crate::prelude::*;
25use citadel_io::{error, ErrorCode};
26use citadel_user::auth::pq::client::ClientManagement;
27use citadel_user::auth::pq::messages::{ManagementDone, ManagementMessage, ServerOutcome};
28use futures::StreamExt;
29
30#[async_trait]
31pub trait SignInManagementExt {
32    /// Runs one change. `step_up` holds the factors the account's policy asks for (and, for
33    /// `AddSecurityKey`, the key channel that is asked to create the new key's credential).
34    async fn manage_sign_in(
35        &self,
36        op: SignInManagementOp,
37        step_up: SignInFactors,
38    ) -> Result<SignInManagementOutcome, NetworkError>;
39}
40
41#[async_trait]
42impl<R: Ratchet> SignInManagementExt for CitadelClientServerConnection<R> {
43    async fn manage_sign_in(
44        &self,
45        op: SignInManagementOp,
46        step_up: SignInFactors,
47    ) -> Result<SignInManagementOutcome, NetworkError> {
48        let cid = self.cid;
49        let remote = &self.remote;
50        let username = remote
51            .account_manager()
52            .get_username_by_cid(cid)
53            .await?
54            .ok_or_else(|| error!(ErrorCode::SessionClientNotLoaded))?;
55        let (begin, client) =
56            ClientManagement::begin(&username, op.clone(), step_up.password.as_ref())?;
57        let challenge = match exchange(remote, cid, ManagementMessage::Begin(begin)).await? {
58            ManagementMessage::Challenge(challenge) => challenge,
59            other => return Err(unexpected(other)),
60        };
61        let key = step_up.security_key.as_ref();
62        let ask = |purpose, request| async move {
63            let key = key.ok_or_else(|| {
64                error!(ErrorCode::PqSignInFactorMissing, "a security key channel")
65            })?;
66            key.ask(purpose, request).await
67        };
68        let step_up_key = match ClientManagement::step_up_key_request(&challenge)? {
69            Some(request) => Some(ask(SecurityKeyPurpose::StepUp, request).await?),
70            None => None,
71        };
72        let new_key = match client.new_key_request(&challenge)? {
73            Some(request) => Some(ask(SecurityKeyPurpose::Enrol, request).await?),
74            None => None,
75        };
76        let (commit, committed) = client.commit(cid, &challenge, step_up_key, new_key).await?;
77        let done = match exchange(remote, cid, ManagementMessage::Commit(commit)).await? {
78            ManagementMessage::EnrolChallenge(enrol) => {
79                let proof = committed.enrol_proof(cid, &enrol)?;
80                exchange(remote, cid, ManagementMessage::EnrolProof(proof)).await?
81            }
82            other => other,
83        };
84        let outcome = match done {
85            ManagementMessage::Done(ManagementDone::Done(outcome)) => outcome,
86            other => return Err(unexpected(other)),
87        };
88        Ok(match outcome {
89            ServerOutcome::Credentials {
90                policy,
91                credentials,
92            } => SignInManagementOutcome::Credentials {
93                policy,
94                credentials,
95            },
96            ServerOutcome::Added { id } => SignInManagementOutcome::Added { id },
97            ServerOutcome::Renamed => SignInManagementOutcome::Renamed,
98            ServerOutcome::Removed => SignInManagementOutcome::Removed,
99            ServerOutcome::PolicySet => SignInManagementOutcome::PolicySet,
100            ServerOutcome::RecoveryCodesReplaced => SignInManagementOutcome::RecoveryCodes(
101                committed
102                    .into_recovery_codes()
103                    .iter()
104                    .map(|code| code.display().to_string())
105                    .collect(),
106            ),
107        })
108    }
109}
110
111fn unexpected(message: ManagementMessage) -> NetworkError {
112    match message {
113        ManagementMessage::Done(ManagementDone::Refused(reason)) => {
114            error!(ErrorCode::PqSignInPolicy, reason)
115        }
116        _ => error!(
117            ErrorCode::PqSignInMalformed,
118            "an out-of-order management reply"
119        ),
120    }
121}
122
123/// One request and its reply, over the session's signal channel.
124async fn exchange<R: Ratchet>(
125    remote: &ClientServerRemote<R>,
126    cid: u64,
127    message: ManagementMessage,
128) -> Result<ManagementMessage, NetworkError> {
129    let request = NodeRequest::PeerCommand(PeerCommand {
130        session_cid: cid,
131        command: PeerSignal::SignInManagement {
132            session_cid: cid,
133            message,
134        },
135    });
136    let mut replies = remote.send_callback_subscription(request).await?;
137    while let Some(reply) = replies.next().await {
138        if let NodeResult::PeerEvent(PeerEvent {
139            event: PeerSignal::SignInManagement { message, .. },
140            ..
141        }) = reply.into_result()?
142        {
143            return Ok(message);
144        }
145    }
146    Err(error!(
147        ErrorCode::RemoteKernelStreamDied,
148        "sign-in management"
149    ))
150}