this repo has no description

Gearing up for migration endpoints

lewis 98564b50 1fe38f49

+2 -2
TODO.md
··· 37 37 - [x] Implement `com.atproto.server.requestAccountDelete`. 38 38 - [x] Implement `com.atproto.server.requestEmailConfirmation` / `requestEmailUpdate`. 39 39 - [x] Implement `com.atproto.server.requestPasswordReset` / `resetPassword`. 40 - - [ ] Implement `com.atproto.server.reserveSigningKey`. 40 + - [x] Implement `com.atproto.server.reserveSigningKey`. 41 41 - [x] Implement `com.atproto.server.revokeAppPassword`. 42 - - [ ] Implement `com.atproto.server.updateEmail`. 42 + - [x] Implement `com.atproto.server.updateEmail`. 43 43 - [x] Implement `com.atproto.server.confirmEmail`. 44 44 45 45 ## Repository Operations (`com.atproto.repo`)
+12
migrations/202512211401_reserved_signing_keys.sql
··· 1 + CREATE TABLE IF NOT EXISTS reserved_signing_keys ( 2 + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 + did TEXT, 4 + public_key_did_key TEXT NOT NULL, 5 + private_key_bytes BYTEA NOT NULL, 6 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 7 + expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours', 8 + used_at TIMESTAMPTZ 9 + ); 10 + 11 + CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_did ON reserved_signing_keys(did) WHERE did IS NOT NULL; 12 + 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
··· 17 17 use tracing::{error, info, warn}; 18 18 19 19 #[derive(Deserialize)] 20 + #[serde(rename_all = "camelCase")] 20 21 pub struct CreateAccountInput { 21 22 pub handle: String, 22 23 pub email: String, 23 24 pub password: String, 24 - #[serde(rename = "inviteCode")] 25 25 pub invite_code: Option<String>, 26 26 pub did: Option<String>, 27 + pub signing_key: Option<String>, 27 28 } 28 29 29 30 #[derive(Serialize)] ··· 185 186 } 186 187 }; 187 188 188 - let secret_key = SecretKey::random(&mut OsRng); 189 - let secret_key_bytes = secret_key.to_bytes(); 189 + let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 190 + if let Some(signing_key_did) = &input.signing_key { 191 + let reserved = sqlx::query!( 192 + r#" 193 + SELECT id, private_key_bytes 194 + FROM reserved_signing_keys 195 + WHERE public_key_did_key = $1 196 + AND used_at IS NULL 197 + AND expires_at > NOW() 198 + FOR UPDATE 199 + "#, 200 + signing_key_did 201 + ) 202 + .fetch_optional(&mut *tx) 203 + .await; 204 + 205 + match reserved { 206 + Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 207 + Ok(None) => { 208 + return ( 209 + StatusCode::BAD_REQUEST, 210 + Json(json!({ 211 + "error": "InvalidSigningKey", 212 + "message": "Signing key not found, already used, or expired" 213 + })), 214 + ) 215 + .into_response(); 216 + } 217 + Err(e) => { 218 + error!("Error looking up reserved signing key: {:?}", e); 219 + return ( 220 + StatusCode::INTERNAL_SERVER_ERROR, 221 + Json(json!({"error": "InternalError"})), 222 + ) 223 + .into_response(); 224 + } 225 + } 226 + } else { 227 + let secret_key = SecretKey::random(&mut OsRng); 228 + (secret_key.to_bytes().to_vec(), None) 229 + }; 190 230 191 - let key_insert = sqlx::query!("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)", user_id, &secret_key_bytes[..]) 192 - .execute(&mut *tx) 193 - .await; 231 + let key_insert = sqlx::query!( 232 + "INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)", 233 + user_id, 234 + &secret_key_bytes[..] 235 + ) 236 + .execute(&mut *tx) 237 + .await; 194 238 195 239 if let Err(e) = key_insert { 196 240 error!("Error inserting user key: {:?}", e); ··· 199 243 Json(json!({"error": "InternalError"})), 200 244 ) 201 245 .into_response(); 246 + } 247 + 248 + if let Some(key_id) = reserved_key_id { 249 + let mark_used = sqlx::query!( 250 + "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 251 + key_id 252 + ) 253 + .execute(&mut *tx) 254 + .await; 255 + 256 + if let Err(e) = mark_used { 257 + error!("Error marking reserved key as used: {:?}", e); 258 + return ( 259 + StatusCode::INTERNAL_SERVER_ERROR, 260 + Json(json!({"error": "InternalError"})), 261 + ) 262 + .into_response(); 263 + } 202 264 } 203 265 204 266 let mst = Mst::new(Arc::new(state.block_store.clone()));
+209
src/api/server/email.rs
··· 286 286 287 287 (StatusCode::OK, Json(json!({}))).into_response() 288 288 } 289 + 290 + #[derive(Deserialize)] 291 + #[serde(rename_all = "camelCase")] 292 + pub struct UpdateEmailInput { 293 + pub email: String, 294 + #[serde(default)] 295 + pub email_auth_factor: Option<bool>, 296 + pub token: Option<String>, 297 + } 298 + 299 + pub async fn update_email( 300 + State(state): State<AppState>, 301 + headers: axum::http::HeaderMap, 302 + Json(input): Json<UpdateEmailInput>, 303 + ) -> Response { 304 + let auth_header = headers.get("Authorization"); 305 + if auth_header.is_none() { 306 + return ( 307 + StatusCode::UNAUTHORIZED, 308 + Json(json!({"error": "AuthenticationRequired"})), 309 + ) 310 + .into_response(); 311 + } 312 + 313 + let token = auth_header 314 + .unwrap() 315 + .to_str() 316 + .unwrap_or("") 317 + .replace("Bearer ", ""); 318 + 319 + let session = sqlx::query!( 320 + r#" 321 + SELECT s.did, k.key_bytes, u.id as user_id, u.email as current_email, 322 + u.email_confirmation_code, u.email_confirmation_code_expires_at, 323 + u.email_pending_verification 324 + FROM sessions s 325 + JOIN users u ON s.did = u.did 326 + JOIN user_keys k ON u.id = k.user_id 327 + WHERE s.access_jwt = $1 328 + "#, 329 + token 330 + ) 331 + .fetch_optional(&state.db) 332 + .await; 333 + 334 + let ( 335 + _did, 336 + key_bytes, 337 + user_id, 338 + current_email, 339 + stored_code, 340 + expires_at, 341 + email_pending_verification, 342 + ) = match session { 343 + Ok(Some(row)) => ( 344 + row.did, 345 + row.key_bytes, 346 + row.user_id, 347 + row.current_email, 348 + row.email_confirmation_code, 349 + row.email_confirmation_code_expires_at, 350 + row.email_pending_verification, 351 + ), 352 + Ok(None) => { 353 + return ( 354 + StatusCode::UNAUTHORIZED, 355 + Json(json!({"error": "AuthenticationFailed"})), 356 + ) 357 + .into_response(); 358 + } 359 + Err(e) => { 360 + error!("DB error in update_email: {:?}", e); 361 + return ( 362 + StatusCode::INTERNAL_SERVER_ERROR, 363 + Json(json!({"error": "InternalError"})), 364 + ) 365 + .into_response(); 366 + } 367 + }; 368 + 369 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 370 + return ( 371 + StatusCode::UNAUTHORIZED, 372 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 373 + ) 374 + .into_response(); 375 + } 376 + 377 + let new_email = input.email.trim().to_lowercase(); 378 + if new_email.is_empty() { 379 + return ( 380 + StatusCode::BAD_REQUEST, 381 + Json(json!({"error": "InvalidRequest", "message": "email is required"})), 382 + ) 383 + .into_response(); 384 + } 385 + 386 + if !new_email.contains('@') || !new_email.contains('.') { 387 + return ( 388 + StatusCode::BAD_REQUEST, 389 + Json(json!({"error": "InvalidRequest", "message": "Invalid email format"})), 390 + ) 391 + .into_response(); 392 + } 393 + 394 + if new_email == current_email.to_lowercase() { 395 + return (StatusCode::OK, Json(json!({}))).into_response(); 396 + } 397 + 398 + let email_confirmed = stored_code.is_some() && email_pending_verification.is_some(); 399 + 400 + if email_confirmed { 401 + let confirmation_token = match &input.token { 402 + Some(t) => t.trim(), 403 + None => { 404 + return ( 405 + StatusCode::BAD_REQUEST, 406 + Json(json!({"error": "TokenRequired", "message": "Token required for confirmed accounts. Call requestEmailUpdate first."})), 407 + ) 408 + .into_response(); 409 + } 410 + }; 411 + 412 + let pending_email = email_pending_verification.unwrap(); 413 + if pending_email.to_lowercase() != new_email { 414 + return ( 415 + StatusCode::BAD_REQUEST, 416 + Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 417 + ) 418 + .into_response(); 419 + } 420 + 421 + if stored_code.unwrap() != confirmation_token { 422 + return ( 423 + StatusCode::BAD_REQUEST, 424 + Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 425 + ) 426 + .into_response(); 427 + } 428 + 429 + if let Some(exp) = expires_at { 430 + if Utc::now() > exp { 431 + return ( 432 + StatusCode::BAD_REQUEST, 433 + Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 434 + ) 435 + .into_response(); 436 + } 437 + } 438 + } 439 + 440 + let exists = sqlx::query!( 441 + "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 442 + new_email, 443 + user_id 444 + ) 445 + .fetch_optional(&state.db) 446 + .await; 447 + 448 + if let Ok(Some(_)) = exists { 449 + return ( 450 + StatusCode::BAD_REQUEST, 451 + Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 452 + ) 453 + .into_response(); 454 + } 455 + 456 + let update = sqlx::query!( 457 + r#" 458 + UPDATE users 459 + SET email = $1, 460 + email_pending_verification = NULL, 461 + email_confirmation_code = NULL, 462 + email_confirmation_code_expires_at = NULL, 463 + updated_at = NOW() 464 + WHERE id = $2 465 + "#, 466 + new_email, 467 + user_id 468 + ) 469 + .execute(&state.db) 470 + .await; 471 + 472 + match update { 473 + Ok(_) => { 474 + info!("Email updated to {} for user {}", new_email, user_id); 475 + (StatusCode::OK, Json(json!({}))).into_response() 476 + } 477 + Err(e) => { 478 + error!("DB error finalizing email update: {:?}", e); 479 + if e.as_database_error() 480 + .map(|db_err| db_err.is_unique_violation()) 481 + .unwrap_or(false) 482 + { 483 + return ( 484 + StatusCode::BAD_REQUEST, 485 + Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 486 + ) 487 + .into_response(); 488 + } 489 + 490 + ( 491 + StatusCode::INTERNAL_SERVER_ERROR, 492 + Json(json!({"error": "InternalError"})), 493 + ) 494 + .into_response() 495 + } 496 + } 497 + }
+3 -1
src/api/server/mod.rs
··· 5 5 pub mod meta; 6 6 pub mod password; 7 7 pub mod session; 8 + pub mod signing_key; 8 9 9 10 pub use account_status::{ 10 11 activate_account, check_account_status, deactivate_account, request_account_delete, 11 12 }; 12 13 pub use app_password::{create_app_password, list_app_passwords, revoke_app_password}; 13 - pub use email::{confirm_email, request_email_update}; 14 + pub use email::{confirm_email, request_email_update, update_email}; 14 15 pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes}; 15 16 pub use meta::{describe_server, health}; 16 17 pub use password::{request_password_reset, reset_password}; 17 18 pub use session::{ 18 19 create_session, delete_session, get_service_auth, get_session, refresh_session, 19 20 }; 21 + pub use signing_key::reserve_signing_key;
+90
src/api/server/signing_key.rs
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use chrono::{Duration, Utc}; 9 + use k256::ecdsa::SigningKey; 10 + use serde::{Deserialize, Serialize}; 11 + use serde_json::json; 12 + use tracing::{error, info}; 13 + 14 + const SECP256K1_MULTICODEC_PREFIX: [u8; 2] = [0xe7, 0x01]; 15 + 16 + fn public_key_to_did_key(signing_key: &SigningKey) -> String { 17 + let verifying_key = signing_key.verifying_key(); 18 + let compressed_pubkey = verifying_key.to_sec1_bytes(); 19 + 20 + let mut multicodec_key = Vec::with_capacity(2 + compressed_pubkey.len()); 21 + multicodec_key.extend_from_slice(&SECP256K1_MULTICODEC_PREFIX); 22 + multicodec_key.extend_from_slice(&compressed_pubkey); 23 + 24 + let encoded = multibase::encode(multibase::Base::Base58Btc, &multicodec_key); 25 + 26 + format!("did:key:{}", encoded) 27 + } 28 + 29 + #[derive(Deserialize)] 30 + pub struct ReserveSigningKeyInput { 31 + pub did: Option<String>, 32 + } 33 + 34 + #[derive(Serialize)] 35 + #[serde(rename_all = "camelCase")] 36 + pub struct ReserveSigningKeyOutput { 37 + pub signing_key: String, 38 + } 39 + 40 + pub async fn reserve_signing_key( 41 + State(state): State<AppState>, 42 + Json(input): Json<ReserveSigningKeyInput>, 43 + ) -> Response { 44 + let signing_key = SigningKey::random(&mut rand::thread_rng()); 45 + let private_key_bytes = signing_key.to_bytes(); 46 + let public_key_did_key = public_key_to_did_key(&signing_key); 47 + 48 + let expires_at = Utc::now() + Duration::hours(24); 49 + 50 + let private_bytes: &[u8] = &private_key_bytes; 51 + 52 + let result = sqlx::query!( 53 + r#" 54 + INSERT INTO reserved_signing_keys (did, public_key_did_key, private_key_bytes, expires_at) 55 + VALUES ($1, $2, $3, $4) 56 + RETURNING id 57 + "#, 58 + input.did, 59 + public_key_did_key, 60 + private_bytes, 61 + expires_at 62 + ) 63 + .fetch_one(&state.db) 64 + .await; 65 + 66 + match result { 67 + Ok(row) => { 68 + info!( 69 + "Reserved signing key {} for did {:?}", 70 + row.id, 71 + input.did 72 + ); 73 + ( 74 + StatusCode::OK, 75 + Json(ReserveSigningKeyOutput { 76 + signing_key: public_key_did_key, 77 + }), 78 + ) 79 + .into_response() 80 + } 81 + Err(e) => { 82 + error!("DB error in reserve_signing_key: {:?}", e); 83 + ( 84 + StatusCode::INTERNAL_SERVER_ERROR, 85 + Json(json!({"error": "InternalError"})), 86 + ) 87 + .into_response() 88 + } 89 + } 90 + }
+8
src/lib.rs
··· 172 172 post(api::server::confirm_email), 173 173 ) 174 174 .route( 175 + "/xrpc/com.atproto.server.updateEmail", 176 + post(api::server::update_email), 177 + ) 178 + .route( 179 + "/xrpc/com.atproto.server.reserveSigningKey", 180 + post(api::server::reserve_signing_key), 181 + ) 182 + .route( 175 183 "/xrpc/com.atproto.identity.updateHandle", 176 184 post(api::identity::update_handle), 177 185 )
+324
tests/email_update.rs
··· 234 234 let body: Value = res.json().await.expect("Invalid JSON"); 235 235 assert_eq!(body["message"], "Email does not match pending update"); 236 236 } 237 + 238 + #[tokio::test] 239 + async fn test_update_email_success_no_token_required() { 240 + let client = common::client(); 241 + let base_url = common::base_url().await; 242 + let pool = get_pool().await; 243 + 244 + let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4()); 245 + let email = format!("{}@example.com", handle); 246 + let res = client 247 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 248 + .json(&json!({ 249 + "handle": handle, 250 + "email": email, 251 + "password": "password" 252 + })) 253 + .send() 254 + .await 255 + .expect("Failed to create account"); 256 + assert_eq!(res.status(), StatusCode::OK); 257 + let body: Value = res.json().await.expect("Invalid JSON"); 258 + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt"); 259 + 260 + let new_email = format!("direct_{}@example.com", handle); 261 + let res = client 262 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 263 + .bearer_auth(access_jwt) 264 + .json(&json!({ "email": new_email })) 265 + .send() 266 + .await 267 + .expect("Failed to update email"); 268 + 269 + assert_eq!(res.status(), StatusCode::OK); 270 + 271 + let user = sqlx::query!("SELECT email FROM users WHERE handle = $1", handle) 272 + .fetch_one(&pool) 273 + .await 274 + .expect("User not found"); 275 + assert_eq!(user.email, new_email); 276 + } 277 + 278 + #[tokio::test] 279 + async fn test_update_email_same_email_noop() { 280 + let client = common::client(); 281 + let base_url = common::base_url().await; 282 + 283 + let handle = format!("emailup_same_{}", uuid::Uuid::new_v4()); 284 + let email = format!("{}@example.com", handle); 285 + let res = client 286 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 287 + .json(&json!({ 288 + "handle": handle, 289 + "email": email, 290 + "password": "password" 291 + })) 292 + .send() 293 + .await 294 + .expect("Failed to create account"); 295 + assert_eq!(res.status(), StatusCode::OK); 296 + let body: Value = res.json().await.expect("Invalid JSON"); 297 + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt"); 298 + 299 + let res = client 300 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 301 + .bearer_auth(access_jwt) 302 + .json(&json!({ "email": email })) 303 + .send() 304 + .await 305 + .expect("Failed to update email"); 306 + 307 + assert_eq!(res.status(), StatusCode::OK, "Updating to same email should succeed as no-op"); 308 + } 309 + 310 + #[tokio::test] 311 + async fn test_update_email_requires_token_after_pending() { 312 + let client = common::client(); 313 + let base_url = common::base_url().await; 314 + 315 + let handle = format!("emailup_token_{}", uuid::Uuid::new_v4()); 316 + let email = format!("{}@example.com", handle); 317 + let res = client 318 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 319 + .json(&json!({ 320 + "handle": handle, 321 + "email": email, 322 + "password": "password" 323 + })) 324 + .send() 325 + .await 326 + .expect("Failed to create account"); 327 + assert_eq!(res.status(), StatusCode::OK); 328 + let body: Value = res.json().await.expect("Invalid JSON"); 329 + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt"); 330 + 331 + let new_email = format!("pending_{}@example.com", handle); 332 + let res = client 333 + .post(format!("{}/xrpc/com.atproto.server.requestEmailUpdate", base_url)) 334 + .bearer_auth(access_jwt) 335 + .json(&json!({"email": new_email})) 336 + .send() 337 + .await 338 + .expect("Failed to request email update"); 339 + assert_eq!(res.status(), StatusCode::OK); 340 + 341 + let res = client 342 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 343 + .bearer_auth(access_jwt) 344 + .json(&json!({ "email": new_email })) 345 + .send() 346 + .await 347 + .expect("Failed to attempt email update"); 348 + 349 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 350 + let body: Value = res.json().await.expect("Invalid JSON"); 351 + assert_eq!(body["error"], "TokenRequired"); 352 + } 353 + 354 + #[tokio::test] 355 + async fn test_update_email_with_valid_token() { 356 + let client = common::client(); 357 + let base_url = common::base_url().await; 358 + let pool = get_pool().await; 359 + 360 + let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4()); 361 + let email = format!("{}@example.com", handle); 362 + let res = client 363 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 364 + .json(&json!({ 365 + "handle": handle, 366 + "email": email, 367 + "password": "password" 368 + })) 369 + .send() 370 + .await 371 + .expect("Failed to create account"); 372 + assert_eq!(res.status(), StatusCode::OK); 373 + let body: Value = res.json().await.expect("Invalid JSON"); 374 + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt"); 375 + 376 + let new_email = format!("valid_{}@example.com", handle); 377 + let res = client 378 + .post(format!("{}/xrpc/com.atproto.server.requestEmailUpdate", base_url)) 379 + .bearer_auth(access_jwt) 380 + .json(&json!({"email": new_email})) 381 + .send() 382 + .await 383 + .expect("Failed to request email update"); 384 + assert_eq!(res.status(), StatusCode::OK); 385 + 386 + let user = sqlx::query!( 387 + "SELECT email_confirmation_code FROM users WHERE handle = $1", 388 + handle 389 + ) 390 + .fetch_one(&pool) 391 + .await 392 + .expect("User not found"); 393 + let code = user.email_confirmation_code.unwrap(); 394 + 395 + let res = client 396 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 397 + .bearer_auth(access_jwt) 398 + .json(&json!({ 399 + "email": new_email, 400 + "token": code 401 + })) 402 + .send() 403 + .await 404 + .expect("Failed to update email"); 405 + 406 + assert_eq!(res.status(), StatusCode::OK); 407 + 408 + let user = sqlx::query!("SELECT email, email_pending_verification FROM users WHERE handle = $1", handle) 409 + .fetch_one(&pool) 410 + .await 411 + .expect("User not found"); 412 + assert_eq!(user.email, new_email); 413 + assert!(user.email_pending_verification.is_none()); 414 + } 415 + 416 + #[tokio::test] 417 + async fn test_update_email_invalid_token() { 418 + let client = common::client(); 419 + let base_url = common::base_url().await; 420 + 421 + let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4()); 422 + let email = format!("{}@example.com", handle); 423 + let res = client 424 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 425 + .json(&json!({ 426 + "handle": handle, 427 + "email": email, 428 + "password": "password" 429 + })) 430 + .send() 431 + .await 432 + .expect("Failed to create account"); 433 + assert_eq!(res.status(), StatusCode::OK); 434 + let body: Value = res.json().await.expect("Invalid JSON"); 435 + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt"); 436 + 437 + let new_email = format!("badtok_{}@example.com", handle); 438 + let res = client 439 + .post(format!("{}/xrpc/com.atproto.server.requestEmailUpdate", base_url)) 440 + .bearer_auth(access_jwt) 441 + .json(&json!({"email": new_email})) 442 + .send() 443 + .await 444 + .expect("Failed to request email update"); 445 + assert_eq!(res.status(), StatusCode::OK); 446 + 447 + let res = client 448 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 449 + .bearer_auth(access_jwt) 450 + .json(&json!({ 451 + "email": new_email, 452 + "token": "wrong-token-12345" 453 + })) 454 + .send() 455 + .await 456 + .expect("Failed to attempt email update"); 457 + 458 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 459 + let body: Value = res.json().await.expect("Invalid JSON"); 460 + assert_eq!(body["error"], "InvalidToken"); 461 + } 462 + 463 + #[tokio::test] 464 + async fn test_update_email_already_taken() { 465 + let client = common::client(); 466 + let base_url = common::base_url().await; 467 + 468 + let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4()); 469 + let email1 = format!("{}@example.com", handle1); 470 + let res = client 471 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 472 + .json(&json!({ 473 + "handle": handle1, 474 + "email": email1, 475 + "password": "password" 476 + })) 477 + .send() 478 + .await 479 + .expect("Failed to create account 1"); 480 + assert_eq!(res.status(), StatusCode::OK); 481 + 482 + let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4()); 483 + let email2 = format!("{}@example.com", handle2); 484 + let res = client 485 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 486 + .json(&json!({ 487 + "handle": handle2, 488 + "email": email2, 489 + "password": "password" 490 + })) 491 + .send() 492 + .await 493 + .expect("Failed to create account 2"); 494 + assert_eq!(res.status(), StatusCode::OK); 495 + let body: Value = res.json().await.expect("Invalid JSON"); 496 + let access_jwt2 = body["accessJwt"].as_str().expect("No accessJwt"); 497 + 498 + let res = client 499 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 500 + .bearer_auth(access_jwt2) 501 + .json(&json!({ "email": email1 })) 502 + .send() 503 + .await 504 + .expect("Failed to attempt email update"); 505 + 506 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 507 + let body: Value = res.json().await.expect("Invalid JSON"); 508 + assert!(body["message"].as_str().unwrap().contains("already in use") || body["error"] == "InvalidRequest"); 509 + } 510 + 511 + #[tokio::test] 512 + async fn test_update_email_no_auth() { 513 + let client = common::client(); 514 + let base_url = common::base_url().await; 515 + 516 + let res = client 517 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 518 + .json(&json!({ "email": "test@example.com" })) 519 + .send() 520 + .await 521 + .expect("Failed to send request"); 522 + 523 + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 524 + let body: Value = res.json().await.expect("Invalid JSON"); 525 + assert_eq!(body["error"], "AuthenticationRequired"); 526 + } 527 + 528 + #[tokio::test] 529 + async fn test_update_email_invalid_format() { 530 + let client = common::client(); 531 + let base_url = common::base_url().await; 532 + 533 + let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4()); 534 + let email = format!("{}@example.com", handle); 535 + let res = client 536 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base_url)) 537 + .json(&json!({ 538 + "handle": handle, 539 + "email": email, 540 + "password": "password" 541 + })) 542 + .send() 543 + .await 544 + .expect("Failed to create account"); 545 + assert_eq!(res.status(), StatusCode::OK); 546 + let body: Value = res.json().await.expect("Invalid JSON"); 547 + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt"); 548 + 549 + let res = client 550 + .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) 551 + .bearer_auth(access_jwt) 552 + .json(&json!({ "email": "not-an-email" })) 553 + .send() 554 + .await 555 + .expect("Failed to send request"); 556 + 557 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 558 + let body: Value = res.json().await.expect("Invalid JSON"); 559 + assert_eq!(body["error"], "InvalidRequest"); 560 + }
+355
tests/signing_key.rs
··· 1 + mod common; 2 + 3 + use reqwest::StatusCode; 4 + use serde_json::{json, Value}; 5 + use sqlx::PgPool; 6 + 7 + async fn get_pool() -> PgPool { 8 + let conn_str = common::get_db_connection_string().await; 9 + sqlx::postgres::PgPoolOptions::new() 10 + .max_connections(5) 11 + .connect(&conn_str) 12 + .await 13 + .expect("Failed to connect to test database") 14 + } 15 + 16 + #[tokio::test] 17 + async fn test_reserve_signing_key_without_did() { 18 + let client = common::client(); 19 + let base_url = common::base_url().await; 20 + 21 + let res = client 22 + .post(format!( 23 + "{}/xrpc/com.atproto.server.reserveSigningKey", 24 + base_url 25 + )) 26 + .json(&json!({})) 27 + .send() 28 + .await 29 + .expect("Failed to send request"); 30 + 31 + assert_eq!(res.status(), StatusCode::OK); 32 + let body: Value = res.json().await.expect("Response was not valid JSON"); 33 + 34 + assert!(body["signingKey"].is_string()); 35 + let signing_key = body["signingKey"].as_str().unwrap(); 36 + assert!( 37 + signing_key.starts_with("did:key:z"), 38 + "Signing key should be in did:key format with multibase prefix" 39 + ); 40 + } 41 + 42 + #[tokio::test] 43 + async fn test_reserve_signing_key_with_did() { 44 + let client = common::client(); 45 + let base_url = common::base_url().await; 46 + let pool = get_pool().await; 47 + 48 + let target_did = "did:plc:test123456"; 49 + let res = client 50 + .post(format!( 51 + "{}/xrpc/com.atproto.server.reserveSigningKey", 52 + base_url 53 + )) 54 + .json(&json!({ "did": target_did })) 55 + .send() 56 + .await 57 + .expect("Failed to send request"); 58 + 59 + assert_eq!(res.status(), StatusCode::OK); 60 + let body: Value = res.json().await.expect("Response was not valid JSON"); 61 + 62 + let signing_key = body["signingKey"].as_str().unwrap(); 63 + assert!(signing_key.starts_with("did:key:z")); 64 + 65 + let row = sqlx::query!( 66 + "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1", 67 + signing_key 68 + ) 69 + .fetch_one(&pool) 70 + .await 71 + .expect("Reserved key not found in database"); 72 + 73 + assert_eq!(row.did.as_deref(), Some(target_did)); 74 + assert_eq!(row.public_key_did_key, signing_key); 75 + } 76 + 77 + #[tokio::test] 78 + async fn test_reserve_signing_key_stores_private_key() { 79 + let client = common::client(); 80 + let base_url = common::base_url().await; 81 + let pool = get_pool().await; 82 + 83 + let res = client 84 + .post(format!( 85 + "{}/xrpc/com.atproto.server.reserveSigningKey", 86 + base_url 87 + )) 88 + .json(&json!({})) 89 + .send() 90 + .await 91 + .expect("Failed to send request"); 92 + 93 + assert_eq!(res.status(), StatusCode::OK); 94 + let body: Value = res.json().await.expect("Response was not valid JSON"); 95 + let signing_key = body["signingKey"].as_str().unwrap(); 96 + 97 + let row = sqlx::query!( 98 + "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", 99 + signing_key 100 + ) 101 + .fetch_one(&pool) 102 + .await 103 + .expect("Reserved key not found in database"); 104 + 105 + assert_eq!(row.private_key_bytes.len(), 32, "Private key should be 32 bytes for secp256k1"); 106 + assert!(row.used_at.is_none(), "Reserved key should not be marked as used yet"); 107 + assert!(row.expires_at > chrono::Utc::now(), "Key should expire in the future"); 108 + } 109 + 110 + #[tokio::test] 111 + async fn test_reserve_signing_key_unique_keys() { 112 + let client = common::client(); 113 + let base_url = common::base_url().await; 114 + 115 + let res1 = client 116 + .post(format!( 117 + "{}/xrpc/com.atproto.server.reserveSigningKey", 118 + base_url 119 + )) 120 + .json(&json!({})) 121 + .send() 122 + .await 123 + .expect("Failed to send request 1"); 124 + assert_eq!(res1.status(), StatusCode::OK); 125 + let body1: Value = res1.json().await.unwrap(); 126 + let key1 = body1["signingKey"].as_str().unwrap(); 127 + 128 + let res2 = client 129 + .post(format!( 130 + "{}/xrpc/com.atproto.server.reserveSigningKey", 131 + base_url 132 + )) 133 + .json(&json!({})) 134 + .send() 135 + .await 136 + .expect("Failed to send request 2"); 137 + assert_eq!(res2.status(), StatusCode::OK); 138 + let body2: Value = res2.json().await.unwrap(); 139 + let key2 = body2["signingKey"].as_str().unwrap(); 140 + 141 + assert_ne!(key1, key2, "Each call should generate a unique signing key"); 142 + } 143 + 144 + #[tokio::test] 145 + async fn test_reserve_signing_key_is_public() { 146 + let client = common::client(); 147 + let base_url = common::base_url().await; 148 + 149 + let res = client 150 + .post(format!( 151 + "{}/xrpc/com.atproto.server.reserveSigningKey", 152 + base_url 153 + )) 154 + .json(&json!({})) 155 + .send() 156 + .await 157 + .expect("Failed to send request"); 158 + 159 + assert_eq!( 160 + res.status(), 161 + StatusCode::OK, 162 + "reserveSigningKey should work without authentication" 163 + ); 164 + } 165 + 166 + #[tokio::test] 167 + async fn test_create_account_with_reserved_signing_key() { 168 + let client = common::client(); 169 + let base_url = common::base_url().await; 170 + let pool = get_pool().await; 171 + 172 + let res = client 173 + .post(format!( 174 + "{}/xrpc/com.atproto.server.reserveSigningKey", 175 + base_url 176 + )) 177 + .json(&json!({})) 178 + .send() 179 + .await 180 + .expect("Failed to reserve signing key"); 181 + assert_eq!(res.status(), StatusCode::OK); 182 + let body: Value = res.json().await.unwrap(); 183 + let signing_key = body["signingKey"].as_str().unwrap(); 184 + 185 + let handle = format!("reserved_key_user_{}", uuid::Uuid::new_v4()); 186 + let res = client 187 + .post(format!( 188 + "{}/xrpc/com.atproto.server.createAccount", 189 + base_url 190 + )) 191 + .json(&json!({ 192 + "handle": handle, 193 + "email": format!("{}@example.com", handle), 194 + "password": "password", 195 + "signingKey": signing_key 196 + })) 197 + .send() 198 + .await 199 + .expect("Failed to create account"); 200 + 201 + assert_eq!(res.status(), StatusCode::OK); 202 + let body: Value = res.json().await.unwrap(); 203 + assert!(body["accessJwt"].is_string()); 204 + assert!(body["did"].is_string()); 205 + 206 + let reserved = sqlx::query!( 207 + "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", 208 + signing_key 209 + ) 210 + .fetch_one(&pool) 211 + .await 212 + .expect("Reserved key not found"); 213 + assert!( 214 + reserved.used_at.is_some(), 215 + "Reserved key should be marked as used" 216 + ); 217 + } 218 + 219 + #[tokio::test] 220 + async fn test_create_account_with_invalid_signing_key() { 221 + let client = common::client(); 222 + let base_url = common::base_url().await; 223 + 224 + let handle = format!("bad_key_user_{}", uuid::Uuid::new_v4()); 225 + let res = client 226 + .post(format!( 227 + "{}/xrpc/com.atproto.server.createAccount", 228 + base_url 229 + )) 230 + .json(&json!({ 231 + "handle": handle, 232 + "email": format!("{}@example.com", handle), 233 + "password": "password", 234 + "signingKey": "did:key:zNonExistentKey12345" 235 + })) 236 + .send() 237 + .await 238 + .expect("Failed to send request"); 239 + 240 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 241 + let body: Value = res.json().await.unwrap(); 242 + assert_eq!(body["error"], "InvalidSigningKey"); 243 + } 244 + 245 + #[tokio::test] 246 + async fn test_create_account_cannot_reuse_signing_key() { 247 + let client = common::client(); 248 + let base_url = common::base_url().await; 249 + 250 + let res = client 251 + .post(format!( 252 + "{}/xrpc/com.atproto.server.reserveSigningKey", 253 + base_url 254 + )) 255 + .json(&json!({})) 256 + .send() 257 + .await 258 + .expect("Failed to reserve signing key"); 259 + assert_eq!(res.status(), StatusCode::OK); 260 + let body: Value = res.json().await.unwrap(); 261 + let signing_key = body["signingKey"].as_str().unwrap(); 262 + 263 + let handle1 = format!("reuse_key_user1_{}", uuid::Uuid::new_v4()); 264 + let res = client 265 + .post(format!( 266 + "{}/xrpc/com.atproto.server.createAccount", 267 + base_url 268 + )) 269 + .json(&json!({ 270 + "handle": handle1, 271 + "email": format!("{}@example.com", handle1), 272 + "password": "password", 273 + "signingKey": signing_key 274 + })) 275 + .send() 276 + .await 277 + .expect("Failed to create first account"); 278 + assert_eq!(res.status(), StatusCode::OK); 279 + 280 + let handle2 = format!("reuse_key_user2_{}", uuid::Uuid::new_v4()); 281 + let res = client 282 + .post(format!( 283 + "{}/xrpc/com.atproto.server.createAccount", 284 + base_url 285 + )) 286 + .json(&json!({ 287 + "handle": handle2, 288 + "email": format!("{}@example.com", handle2), 289 + "password": "password", 290 + "signingKey": signing_key 291 + })) 292 + .send() 293 + .await 294 + .expect("Failed to send second request"); 295 + 296 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 297 + let body: Value = res.json().await.unwrap(); 298 + assert_eq!(body["error"], "InvalidSigningKey"); 299 + assert!(body["message"] 300 + .as_str() 301 + .unwrap() 302 + .contains("already used")); 303 + } 304 + 305 + #[tokio::test] 306 + async fn test_reserved_key_tokens_work() { 307 + let client = common::client(); 308 + let base_url = common::base_url().await; 309 + 310 + let res = client 311 + .post(format!( 312 + "{}/xrpc/com.atproto.server.reserveSigningKey", 313 + base_url 314 + )) 315 + .json(&json!({})) 316 + .send() 317 + .await 318 + .expect("Failed to reserve signing key"); 319 + assert_eq!(res.status(), StatusCode::OK); 320 + let body: Value = res.json().await.unwrap(); 321 + let signing_key = body["signingKey"].as_str().unwrap(); 322 + 323 + let handle = format!("token_test_user_{}", uuid::Uuid::new_v4()); 324 + let res = client 325 + .post(format!( 326 + "{}/xrpc/com.atproto.server.createAccount", 327 + base_url 328 + )) 329 + .json(&json!({ 330 + "handle": handle, 331 + "email": format!("{}@example.com", handle), 332 + "password": "password", 333 + "signingKey": signing_key 334 + })) 335 + .send() 336 + .await 337 + .expect("Failed to create account"); 338 + assert_eq!(res.status(), StatusCode::OK); 339 + let body: Value = res.json().await.unwrap(); 340 + let access_jwt = body["accessJwt"].as_str().unwrap(); 341 + 342 + let res = client 343 + .get(format!( 344 + "{}/xrpc/com.atproto.server.getSession", 345 + base_url 346 + )) 347 + .bearer_auth(access_jwt) 348 + .send() 349 + .await 350 + .expect("Failed to get session"); 351 + 352 + assert_eq!(res.status(), StatusCode::OK); 353 + let body: Value = res.json().await.unwrap(); 354 + assert_eq!(body["handle"], handle); 355 + }