Lines
100 %
Functions
57.69 %
Branches
//! Per-user JWT signing keypairs.
//!
//! Each user signs their own tokens with an RSA keypair: the private key lives
//! only in their per-user database (`user_auth_keys`), the public key in the
//! global `users.jwt_public_key` directory. Keys are stored as base64-encoded
//! PEM — the same wire form the web layer's token encode/decode already expects.
use crate::db::{DBError, get_connection};
use crate::user::User;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
use rsa::{RsaPrivateKey, RsaPublicKey};
use sqlx::types::Uuid;
/// RSA modulus size. 2048 is the standard floor for RS256 and far cheaper to
/// generate than 4096 — important because generation happens during user
/// provisioning and must not become a CPU-bound DoS lever on the request path.
const KEY_BITS: usize = 2048;
/// A freshly generated keypair, both halves base64-encoded PEM.
pub struct KeyPair {
pub private_pem_b64: String,
pub public_pem_b64: String,
}
/// Generates an RS256 keypair off the async runtime.
///
/// RSA keygen is CPU-bound (tens to hundreds of ms); running it on a
/// `spawn_blocking` thread keeps it off the request executor so it can't stall
/// the reactor or starve other tasks.
/// # Errors
/// [`DBError::KeyGen`] if generation or PEM encoding fails.
pub async fn generate() -> Result<KeyPair, DBError> {
tokio::task::spawn_blocking(generate_blocking)
.await
.map_err(|_| DBError::KeyGen)?
fn generate_blocking() -> Result<KeyPair, DBError> {
let mut rng = rand::thread_rng();
let private = RsaPrivateKey::new(&mut rng, KEY_BITS).map_err(|_| DBError::KeyGen)?;
let public = RsaPublicKey::from(&private);
let private_pem = private
.to_pkcs8_pem(LineEnding::LF)
.map_err(|_| DBError::KeyGen)?;
let public_pem = public
.to_public_key_pem(LineEnding::LF)
Ok(KeyPair {
private_pem_b64: STANDARD.encode(private_pem.as_bytes()),
public_pem_b64: STANDARD.encode(public_pem.as_bytes()),
})
/// Fetches a user's base64-PEM private signing key from their per-user
/// database (`user_auth_keys`, a single-row table). Used on the token MINT path
/// after the user has been identified.
/// [`DBError::Sqlx`] on a DB error; [`DBError::KeyGen`] if the user has no key
/// (a provisioning invariant violation — surfaced rather than silently signing
/// with nothing).
pub async fn private_key_for(user_id: Uuid) -> Result<String, DBError> {
let user = User { id: user_id };
let mut conn = user.get_connection().await?;
let key: Option<String> = sqlx::query_scalar("SELECT private_key FROM user_auth_keys LIMIT 1")
.fetch_optional(&mut *conn)
.await?;
key.ok_or(DBError::KeyGen)
/// Fetches a user's base64-PEM public verification key from the global `users`
/// directory. Used on the token VERIFY path, looked up by the (still-unverified)
/// `sub` claim — so the right key is fetched before the per-user DB is reachable.
/// Returns `None` if the user is unknown or has no key.
/// [`DBError::Sqlx`] on a DB error.
pub async fn public_key_for(user_id: Uuid) -> Result<Option<String>, DBError> {
let mut conn = get_connection().await?;
let key: Option<String> = sqlx::query_scalar("SELECT jwt_public_key FROM users WHERE id = $1")
.bind(user_id)
.await?
.flatten();
Ok(key)
#[cfg(test)]
mod tests {
use super::*;
use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey};
fn decode_pem(b64: &str) -> String {
String::from_utf8(STANDARD.decode(b64).expect("base64")).expect("utf8")
#[tokio::test]
async fn generate_produces_decodable_rsa_pem_pair() {
let pair = generate().await.expect("keygen");
let priv_pem = decode_pem(&pair.private_pem_b64);
let pub_pem = decode_pem(&pair.public_pem_b64);
assert!(priv_pem.contains("BEGIN PRIVATE KEY"));
assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
// The PEM must round-trip back into RSA keys (the web token layer parses
// the same base64-PEM form when signing/verifying), and the public half
// must match the private half.
let parsed_priv = RsaPrivateKey::from_pkcs8_pem(&priv_pem).expect("private parses");
let parsed_pub = RsaPublicKey::from_public_key_pem(&pub_pem).expect("public parses");
assert_eq!(RsaPublicKey::from(&parsed_priv), parsed_pub);
async fn generate_yields_distinct_keys() {
let a = generate().await.expect("keygen a");
let b = generate().await.expect("keygen b");
assert_ne!(a.private_pem_b64, b.private_pem_b64);
assert_ne!(a.public_pem_b64, b.public_pem_b64);