+2
-2
TODO.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
}