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}