fix(manager-core): F-P-002 move Argon2id verify off the Tokio async worker
verify_password (Argon2id, OWASP defaults m=19456 KiB, t=2) is CPU-bound at tens-to-hundreds of ms and was invoked synchronously on the Tokio worker. Worse, verify_api_key Argon2-verifies every candidate sharing the 8-char prefix — a hot user with N keys serialized every admin request behind N×Argon2. Wrap each call site in tokio::task::spawn_blocking: - auth_middleware::verify_api_key (per request carrying a Bearer key) - auth_api::login (admin login) - users_service::login (data-plane app-user login, both real-hash and TIMING_FLAT_DUMMY_HASH branches) Cold-cache login is now ~2× current latency due to one spawn_blocking hop, but the worker no longer parks on Argon2 so steady-state under load is dramatically better. The LRU cache for the hot-path (token → principal) is finding F-P-009 — separate commit. AUDIT.md anchor: F-P-002. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,17 @@ async fn login(State(state): State<AuthState>, Json(input): Json<LoginRequest>)
|
||||
None => (crate::auth::TIMING_FLAT_DUMMY_HASH.to_string(), None, false),
|
||||
};
|
||||
|
||||
let password_ok = verify_password(&stored_hash, &input.password);
|
||||
// F-P-002: Argon2id verify off the async worker.
|
||||
let password = input.password.clone();
|
||||
let password_ok = match tokio::task::spawn_blocking(move || verify_password(&stored_hash, &password))
|
||||
.await
|
||||
{
|
||||
Ok(b) => b,
|
||||
Err(err) => {
|
||||
tracing::error!(?err, "verify_password spawn_blocking join failed");
|
||||
return internal_error();
|
||||
}
|
||||
};
|
||||
if !password_ok || user_id.is_none() || !is_active {
|
||||
return invalid_credentials();
|
||||
}
|
||||
|
||||
@@ -203,9 +203,20 @@ async fn verify_api_key(state: &AuthState, rest: &str) -> Result<Option<Principa
|
||||
}
|
||||
};
|
||||
|
||||
let matched: Option<ApiKeyVerification> = candidates
|
||||
.into_iter()
|
||||
.find(|c| verify_password(&c.hash, rest));
|
||||
// F-P-002: Argon2id verify is CPU-bound (~tens of ms each at OWASP
|
||||
// defaults). Move it off the Tokio async worker so a hot user with
|
||||
// N prefix-colliding keys doesn't park the worker for N×Argon2.
|
||||
let rest_owned = rest.to_string();
|
||||
let matched: Option<ApiKeyVerification> = tokio::task::spawn_blocking(move || {
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|c| verify_password(&c.hash, &rest_owned))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = ?e, "verify_api_key spawn_blocking join failed");
|
||||
InternalError
|
||||
})?;
|
||||
let Some(matched) = matched else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -590,10 +590,15 @@ impl UsersService for UsersServiceImpl {
|
||||
.await?;
|
||||
// Both branches normalize the email so an attacker can't probe
|
||||
// existence via "" / "x" / very-long-string differences.
|
||||
let password_owned = password.to_string();
|
||||
let Ok(normalized) = validate_email(email) else {
|
||||
// Still run a dummy verify so the timing of an invalid
|
||||
// email shape matches the timing of a legitimate miss.
|
||||
let _ = verify_password(TIMING_FLAT_DUMMY_HASH, password);
|
||||
// F-P-002: off the async worker — Argon2 is CPU-bound.
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
verify_password(TIMING_FLAT_DUMMY_HASH, &password_owned)
|
||||
})
|
||||
.await;
|
||||
return Ok(None);
|
||||
};
|
||||
let creds = self
|
||||
@@ -604,7 +609,10 @@ impl UsersService for UsersServiceImpl {
|
||||
Some(c) => (c.password_hash, Some(c.id), Some(c.app_id)),
|
||||
None => (TIMING_FLAT_DUMMY_HASH.to_string(), None, None),
|
||||
};
|
||||
let password_ok = verify_password(&hash, password);
|
||||
// F-P-002: Argon2id verify off the async worker.
|
||||
let password_ok = tokio::task::spawn_blocking(move || verify_password(&hash, &password_owned))
|
||||
.await
|
||||
.map_err(|e| UsersError::Backend(format!("verify spawn_blocking join: {e}")))?;
|
||||
let (Some(user_id), Some(app_id)) = (user_id, app_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user