···11+CREATE TABLE IF NOT EXISTS reserved_signing_keys (
22+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
33+ did TEXT,
44+ public_key_did_key TEXT NOT NULL,
55+ private_key_bytes BYTEA NOT NULL,
66+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77+ expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
88+ used_at TIMESTAMPTZ
99+);
1010+1111+CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_did ON reserved_signing_keys(did) WHERE did IS NOT NULL;
1212+CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_expires ON reserved_signing_keys(expires_at) WHERE used_at IS NULL;
+68-6
src/api/identity/account.rs
···1717use tracing::{error, info, warn};
18181919#[derive(Deserialize)]
2020+#[serde(rename_all = "camelCase")]
2021pub struct CreateAccountInput {
2122 pub handle: String,
2223 pub email: String,
2324 pub password: String,
2424- #[serde(rename = "inviteCode")]
2525 pub invite_code: Option<String>,
2626 pub did: Option<String>,
2727+ pub signing_key: Option<String>,
2728}
28292930#[derive(Serialize)]
···185186 }
186187 };
187188188188- let secret_key = SecretKey::random(&mut OsRng);
189189- let secret_key_bytes = secret_key.to_bytes();
189189+ let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
190190+ if let Some(signing_key_did) = &input.signing_key {
191191+ let reserved = sqlx::query!(
192192+ r#"
193193+ SELECT id, private_key_bytes
194194+ FROM reserved_signing_keys
195195+ WHERE public_key_did_key = $1
196196+ AND used_at IS NULL
197197+ AND expires_at > NOW()
198198+ FOR UPDATE
199199+ "#,
200200+ signing_key_did
201201+ )
202202+ .fetch_optional(&mut *tx)
203203+ .await;
204204+205205+ match reserved {
206206+ Ok(Some(row)) => (row.private_key_bytes, Some(row.id)),
207207+ Ok(None) => {
208208+ return (
209209+ StatusCode::BAD_REQUEST,
210210+ Json(json!({
211211+ "error": "InvalidSigningKey",
212212+ "message": "Signing key not found, already used, or expired"
213213+ })),
214214+ )
215215+ .into_response();
216216+ }
217217+ Err(e) => {
218218+ error!("Error looking up reserved signing key: {:?}", e);
219219+ return (
220220+ StatusCode::INTERNAL_SERVER_ERROR,
221221+ Json(json!({"error": "InternalError"})),
222222+ )
223223+ .into_response();
224224+ }
225225+ }
226226+ } else {
227227+ let secret_key = SecretKey::random(&mut OsRng);
228228+ (secret_key.to_bytes().to_vec(), None)
229229+ };
190230191191- let key_insert = sqlx::query!("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)", user_id, &secret_key_bytes[..])
192192- .execute(&mut *tx)
193193- .await;
231231+ let key_insert = sqlx::query!(
232232+ "INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)",
233233+ user_id,
234234+ &secret_key_bytes[..]
235235+ )
236236+ .execute(&mut *tx)
237237+ .await;
194238195239 if let Err(e) = key_insert {
196240 error!("Error inserting user key: {:?}", e);
···199243 Json(json!({"error": "InternalError"})),
200244 )
201245 .into_response();
246246+ }
247247+248248+ if let Some(key_id) = reserved_key_id {
249249+ let mark_used = sqlx::query!(
250250+ "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1",
251251+ key_id
252252+ )
253253+ .execute(&mut *tx)
254254+ .await;
255255+256256+ if let Err(e) = mark_used {
257257+ error!("Error marking reserved key as used: {:?}", e);
258258+ return (
259259+ StatusCode::INTERNAL_SERVER_ERROR,
260260+ Json(json!({"error": "InternalError"})),
261261+ )
262262+ .into_response();
263263+ }
202264 }
203265204266 let mst = Mst::new(Arc::new(state.block_store.clone()));
···55pub mod meta;
66pub mod password;
77pub mod session;
88+pub mod signing_key;
89910pub use account_status::{
1011 activate_account, check_account_status, deactivate_account, request_account_delete,
1112};
1213pub use app_password::{create_app_password, list_app_passwords, revoke_app_password};
1313-pub use email::{confirm_email, request_email_update};
1414+pub use email::{confirm_email, request_email_update, update_email};
1415pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes};
1516pub use meta::{describe_server, health};
1617pub use password::{request_password_reset, reset_password};
1718pub use session::{
1819 create_session, delete_session, get_service_auth, get_session, refresh_session,
1920};
2121+pub use signing_key::reserve_signing_key;