Skip to main content

pray_core/
auth_store_keys.rs

1use super::secrets::generate_auth_challenge;
2use super::support::*;
3use super::RegistryAuthStore;
4use crate::auth::{
5    AuthPasskeyChallengeResponse, AuthPasskeyEnrollmentResponse, AuthPasskeyLoginResponse,
6    AuthSessionKind, AuthSshKeyChallengeResponse, AuthSshKeyEnrollmentResponse,
7    AuthSshKeyLoginResponse,
8};
9use crate::{PrayError, PrayResult};
10use rusqlite::OptionalExtension;
11
12impl RegistryAuthStore {
13    pub fn request_passkey_challenge(
14        &self,
15        credential_id: &str,
16    ) -> PrayResult<AuthPasskeyChallengeResponse> {
17        validate_identifier(credential_id, "credential id")?;
18        let connection = self.connection()?;
19        let email: String = connection.query_row(
20            "SELECT email FROM passkeys WHERE credential_id = ?1",
21            rusqlite::params![credential_id],
22            |row| row.get(0),
23        )?;
24        let challenge = generate_auth_challenge("passkey", credential_id)?;
25        let challenge_id = generate_challenge_id(&email, credential_id, "passkey", &challenge)?;
26        store_challenge(&connection, &challenge_id, &email, &challenge, "passkey")?;
27        Ok(AuthPasskeyChallengeResponse {
28            credential_id: credential_id.to_string(),
29            challenge_id,
30            challenge,
31        })
32    }
33    pub fn respond_passkey_challenge(
34        &self,
35        credential_id: &str,
36        challenge_id: &str,
37        signature: &str,
38    ) -> PrayResult<AuthPasskeyLoginResponse> {
39        validate_identifier(credential_id, "credential id")?;
40        validate_identifier(challenge_id, "challenge id")?;
41        validate_signature(signature)?;
42        let mut connection = self.connection()?;
43        let email: String = connection.query_row(
44            "SELECT email FROM passkeys WHERE credential_id = ?1",
45            rusqlite::params![credential_id],
46            |row| row.get(0),
47        )?;
48        let public_key = load_passkey_public_key(&connection, credential_id)?;
49        consume_challenge(
50            &mut connection,
51            challenge_id,
52            &email,
53            "passkey",
54            |message| verify_signature(&public_key, message.as_bytes(), signature),
55        )?;
56        let session = self.issue_session(&email, AuthSessionKind::Passkey)?;
57        Ok(AuthPasskeyLoginResponse {
58            email,
59            token: session.token,
60        })
61    }
62    pub fn request_ssh_key_challenge(
63        &self,
64        public_key: &str,
65    ) -> PrayResult<AuthSshKeyChallengeResponse> {
66        validate_public_key(public_key)?;
67        let connection = self.connection()?;
68        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
69        let fingerprint = ssh_key_fingerprint(&public_key);
70        let email: String = connection.query_row(
71            "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
72            rusqlite::params![fingerprint],
73            |row| row.get(0),
74        )?;
75        let challenge = generate_auth_challenge("ssh_key", &public_key)?;
76        let challenge_id = generate_challenge_id(&email, &fingerprint, "ssh_key", &challenge)?;
77        store_challenge(&connection, &challenge_id, &email, &challenge, "ssh_key")?;
78        Ok(AuthSshKeyChallengeResponse {
79            fingerprint,
80            challenge_id,
81            challenge,
82        })
83    }
84    pub fn respond_ssh_key_challenge(
85        &self,
86        public_key: &str,
87        challenge_id: &str,
88        signature: &str,
89    ) -> PrayResult<AuthSshKeyLoginResponse> {
90        validate_public_key(public_key)?;
91        validate_identifier(challenge_id, "challenge id")?;
92        validate_signature(signature)?;
93        let mut connection = self.connection()?;
94        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
95        let fingerprint = ssh_key_fingerprint(&public_key);
96        let email: String = connection.query_row(
97            "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
98            rusqlite::params![fingerprint],
99            |row| row.get(0),
100        )?;
101        consume_challenge(
102            &mut connection,
103            challenge_id,
104            &email,
105            "ssh_key",
106            |message| verify_signature(&public_key, message.as_bytes(), signature),
107        )?;
108        let session = self.issue_session(&email, AuthSessionKind::SshKey)?;
109        Ok(AuthSshKeyLoginResponse {
110            email,
111            token: session.token,
112        })
113    }
114    pub fn enroll_passkey(
115        &self,
116        email: &str,
117        credential_id: &str,
118        public_key: &str,
119        label: Option<&str>,
120    ) -> PrayResult<AuthPasskeyEnrollmentResponse> {
121        validate_email(email)?;
122        validate_identifier(credential_id, "credential id")?;
123        validate_public_key(public_key)?;
124        let connection = self.connection()?;
125        ensure_user_can_authenticate(&connection, email)?;
126        reject_credential_reassignment(
127            &connection,
128            "passkeys",
129            "credential_id",
130            credential_id,
131            email,
132        )?;
133        let timestamp = current_unix_timestamp()?;
134        connection.execute(
135        "INSERT INTO passkeys (credential_id, email, public_key, label, created_at, last_used_at)
136         VALUES (?1, ?2, ?3, ?4, ?5, NULL)
137         ON CONFLICT(credential_id) DO UPDATE SET public_key = excluded.public_key, label = excluded.label",
138        rusqlite::params![credential_id, email, public_key, label.unwrap_or(""), timestamp],
139    )?;
140        Ok(AuthPasskeyEnrollmentResponse {
141            email: email.to_string(),
142            credential_id: credential_id.to_string(),
143            enrolled: true,
144        })
145    }
146    pub fn enroll_ssh_key(
147        &self,
148        email: &str,
149        public_key: &str,
150        label: Option<&str>,
151    ) -> PrayResult<AuthSshKeyEnrollmentResponse> {
152        validate_email(email)?;
153        validate_public_key(public_key)?;
154        let connection = self.connection()?;
155        ensure_user_can_authenticate(&connection, email)?;
156        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
157        let fingerprint = ssh_key_fingerprint(&public_key);
158        reject_credential_reassignment(
159            &connection,
160            "ssh_keys",
161            "fingerprint",
162            &fingerprint,
163            email,
164        )?;
165        let timestamp = current_unix_timestamp()?;
166        connection.execute(
167        "INSERT INTO ssh_keys (fingerprint, email, public_key, label, created_at, last_used_at)
168         VALUES (?1, ?2, ?3, ?4, ?5, NULL)
169         ON CONFLICT(fingerprint) DO UPDATE SET public_key = excluded.public_key, label = excluded.label",
170        rusqlite::params![fingerprint, email, public_key, label.unwrap_or(""), timestamp],
171    )?;
172        Ok(AuthSshKeyEnrollmentResponse {
173            email: email.to_string(),
174            fingerprint,
175            enrolled: true,
176        })
177    }
178}
179
180fn consume_challenge(
181    connection: &mut rusqlite::Connection,
182    challenge_id: &str,
183    email: &str,
184    kind: &str,
185    verify: impl FnOnce(&str) -> PrayResult<()>,
186) -> PrayResult<()> {
187    let transaction =
188        connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
189    let challenge = load_challenge(&transaction, challenge_id, email, kind)?;
190    verify(&challenge.challenge)?;
191    mark_challenge_used(&transaction, challenge_id)?;
192    transaction.commit()?;
193    Ok(())
194}
195
196fn reject_credential_reassignment(
197    connection: &rusqlite::Connection,
198    table: &str,
199    column: &str,
200    identity: &str,
201    email: &str,
202) -> PrayResult<()> {
203    let existing: Option<String> = match (table, column) {
204        ("passkeys", "credential_id") => connection
205            .query_row(
206                "SELECT email FROM passkeys WHERE credential_id = ?1",
207                rusqlite::params![identity],
208                |row| row.get(0),
209            )
210            .optional()?,
211        ("ssh_keys", "fingerprint") => connection
212            .query_row(
213                "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
214                rusqlite::params![identity],
215                |row| row.get(0),
216            )
217            .optional()?,
218        _ => {
219            return Err(PrayError::Unsupported(
220                "unknown authenticator table".to_string(),
221            ))
222        }
223    };
224    match existing {
225        Some(owner) if owner != email => Err(PrayError::Resolution(
226            "authenticator already enrolled for another user".to_string(),
227        )),
228        _ => Ok(()),
229    }
230}