1
//! Per-user JWT signing keypairs.
2
//!
3
//! Each user signs their own tokens with an RSA keypair: the private key lives
4
//! only in their per-user database (`user_auth_keys`), the public key in the
5
//! global `users.jwt_public_key` directory. Keys are stored as base64-encoded
6
//! PEM — the same wire form the web layer's token encode/decode already expects.
7

            
8
use crate::db::{DBError, get_connection};
9
use crate::user::User;
10
use base64::{Engine as _, engine::general_purpose::STANDARD};
11
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
12
use rsa::{RsaPrivateKey, RsaPublicKey};
13
use sqlx::types::Uuid;
14

            
15
/// RSA modulus size. 2048 is the standard floor for RS256 and far cheaper to
16
/// generate than 4096 — important because generation happens during user
17
/// provisioning and must not become a CPU-bound DoS lever on the request path.
18
const KEY_BITS: usize = 2048;
19

            
20
/// A freshly generated keypair, both halves base64-encoded PEM.
21
pub struct KeyPair {
22
    pub private_pem_b64: String,
23
    pub public_pem_b64: String,
24
}
25

            
26
/// Generates an RS256 keypair off the async runtime.
27
///
28
/// RSA keygen is CPU-bound (tens to hundreds of ms); running it on a
29
/// `spawn_blocking` thread keeps it off the request executor so it can't stall
30
/// the reactor or starve other tasks.
31
///
32
/// # Errors
33
/// [`DBError::KeyGen`] if generation or PEM encoding fails.
34
354
pub async fn generate() -> Result<KeyPair, DBError> {
35
354
    tokio::task::spawn_blocking(generate_blocking)
36
354
        .await
37
354
        .map_err(|_| DBError::KeyGen)?
38
354
}
39

            
40
354
fn generate_blocking() -> Result<KeyPair, DBError> {
41
354
    let mut rng = rand::rng();
42
354
    let private = RsaPrivateKey::new(&mut rng, KEY_BITS).map_err(|_| DBError::KeyGen)?;
43
354
    let public = RsaPublicKey::from(&private);
44

            
45
354
    let private_pem = private
46
354
        .to_pkcs8_pem(LineEnding::LF)
47
354
        .map_err(|_| DBError::KeyGen)?;
48
354
    let public_pem = public
49
354
        .to_public_key_pem(LineEnding::LF)
50
354
        .map_err(|_| DBError::KeyGen)?;
51

            
52
354
    Ok(KeyPair {
53
354
        private_pem_b64: STANDARD.encode(private_pem.as_bytes()),
54
354
        public_pem_b64: STANDARD.encode(public_pem.as_bytes()),
55
354
    })
56
354
}
57

            
58
/// Fetches a user's base64-PEM private signing key from their per-user
59
/// database (`user_auth_keys`, a single-row table). Used on the token MINT path
60
/// after the user has been identified.
61
///
62
/// # Errors
63
/// [`DBError::Sqlx`] on a DB error; [`DBError::KeyGen`] if the user has no key
64
/// (a provisioning invariant violation — surfaced rather than silently signing
65
/// with nothing).
66
39
pub async fn private_key_for(user_id: Uuid) -> Result<String, DBError> {
67
39
    let user = User { id: user_id };
68
39
    let mut conn = user.get_connection().await?;
69
39
    let key: Option<String> = sqlx::query_scalar("SELECT private_key FROM user_auth_keys LIMIT 1")
70
39
        .fetch_optional(&mut *conn)
71
39
        .await?;
72
39
    key.ok_or(DBError::KeyGen)
73
39
}
74

            
75
pub struct TokenIdentity {
76
    pub public_key: String,
77
    pub verified: bool,
78
    pub generation: i32,
79
}
80

            
81
/// Fetches a user's base64-PEM public verification key from the global `users`
82
/// directory, with the account's current status. Used on the token VERIFY path,
83
/// looked up by the (still-unverified) `sub` claim — so the right key is fetched
84
/// before the per-user DB is reachable.
85
/// Returns `None` if the user is unknown or has no key.
86
///
87
/// # Errors
88
/// [`DBError::Sqlx`] on a DB error.
89
117
pub async fn public_key_for(user_id: Uuid) -> Result<Option<TokenIdentity>, DBError> {
90
117
    let mut conn = get_connection().await?;
91
117
    let row = sqlx::query!(
92
        "SELECT jwt_public_key, verified, session_generation FROM users WHERE id = $1",
93
        user_id
94
    )
95
117
    .fetch_optional(&mut *conn)
96
117
    .await?;
97

            
98
117
    Ok(row.and_then(|row| {
99
117
        row.jwt_public_key.map(|public_key| TokenIdentity {
100
117
            public_key,
101
117
            verified: row.verified,
102
117
            generation: row.session_generation,
103
117
        })
104
117
    }))
105
117
}
106

            
107
#[cfg(test)]
108
mod tests {
109
    use super::*;
110
    use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey};
111

            
112
2
    fn decode_pem(b64: &str) -> String {
113
2
        String::from_utf8(STANDARD.decode(b64).expect("base64")).expect("utf8")
114
2
    }
115

            
116
    #[tokio::test]
117
1
    async fn generate_produces_decodable_rsa_pem_pair() {
118
1
        let pair = generate().await.expect("keygen");
119

            
120
1
        let priv_pem = decode_pem(&pair.private_pem_b64);
121
1
        let pub_pem = decode_pem(&pair.public_pem_b64);
122
1
        assert!(priv_pem.contains("BEGIN PRIVATE KEY"));
123
1
        assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
124

            
125
        // The PEM must round-trip back into RSA keys (the web token layer parses
126
        // the same base64-PEM form when signing/verifying), and the public half
127
        // must match the private half.
128
1
        let parsed_priv = RsaPrivateKey::from_pkcs8_pem(&priv_pem).expect("private parses");
129
1
        let parsed_pub = RsaPublicKey::from_public_key_pem(&pub_pem).expect("public parses");
130
1
        assert_eq!(RsaPublicKey::from(&parsed_priv), parsed_pub);
131
1
    }
132

            
133
    #[tokio::test]
134
1
    async fn generate_yields_distinct_keys() {
135
1
        let a = generate().await.expect("keygen a");
136
1
        let b = generate().await.expect("keygen b");
137
1
        assert_ne!(a.private_pem_b64, b.private_pem_b64);
138
1
        assert_ne!(a.public_pem_b64, b.public_pem_b64);
139
1
    }
140
}