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