An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

feat(db): add oauth_signing_key DB functions

+75
+75
crates/relay/src/db/oauth.rs
··· 113 113 Ok(row.map(|(did,)| did)) 114 114 } 115 115 116 + /// A row from the `oauth_signing_key` table. 117 + pub struct OAuthSigningKeyRow { 118 + pub id: String, 119 + pub public_key_jwk: String, 120 + pub private_key_encrypted: String, 121 + } 122 + 123 + /// Load the server's OAuth signing key row. Returns `None` if no key has been generated yet. 124 + pub async fn get_oauth_signing_key( 125 + pool: &SqlitePool, 126 + ) -> Result<Option<OAuthSigningKeyRow>, sqlx::Error> { 127 + let row: Option<(String, String, String)> = sqlx::query_as( 128 + "SELECT id, public_key_jwk, private_key_encrypted FROM oauth_signing_key LIMIT 1", 129 + ) 130 + .fetch_optional(pool) 131 + .await?; 132 + 133 + Ok(row.map(|(id, public_key_jwk, private_key_encrypted)| OAuthSigningKeyRow { 134 + id, 135 + public_key_jwk, 136 + private_key_encrypted, 137 + })) 138 + } 139 + 140 + /// Persist a newly generated OAuth signing key. 141 + /// 142 + /// `id` is a UUID string. `public_key_jwk` is a JWK JSON string for the P-256 public key. 143 + /// `private_key_encrypted` is the AES-256-GCM-encrypted private key (base64, 80 chars). 144 + pub async fn store_oauth_signing_key( 145 + pool: &SqlitePool, 146 + id: &str, 147 + public_key_jwk: &str, 148 + private_key_encrypted: &str, 149 + ) -> Result<(), sqlx::Error> { 150 + sqlx::query( 151 + "INSERT INTO oauth_signing_key (id, public_key_jwk, private_key_encrypted, created_at) \ 152 + VALUES (?, ?, ?, datetime('now'))", 153 + ) 154 + .bind(id) 155 + .bind(public_key_jwk) 156 + .bind(private_key_encrypted) 157 + .execute(pool) 158 + .await?; 159 + Ok(()) 160 + } 161 + 116 162 #[cfg(test)] 117 163 mod tests { 118 164 use super::*; ··· 243 289 244 290 let result = get_single_account_did(&pool).await.unwrap(); 245 291 assert_eq!(result.as_deref(), Some(did)); 292 + } 293 + 294 + #[tokio::test] 295 + async fn store_and_retrieve_oauth_signing_key() { 296 + let pool = test_pool().await; 297 + store_oauth_signing_key( 298 + &pool, 299 + "test-key-uuid-01", 300 + r#"{"kty":"EC","crv":"P-256","x":"abc","y":"def","kid":"test-key-uuid-01"}"#, 301 + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 302 + ) 303 + .await 304 + .unwrap(); 305 + 306 + let row = get_oauth_signing_key(&pool) 307 + .await 308 + .unwrap() 309 + .expect("key should exist after storage"); 310 + 311 + assert_eq!(row.id, "test-key-uuid-01"); 312 + assert!(!row.public_key_jwk.is_empty()); 313 + assert!(!row.private_key_encrypted.is_empty()); 314 + } 315 + 316 + #[tokio::test] 317 + async fn get_oauth_signing_key_returns_none_when_empty() { 318 + let pool = test_pool().await; 319 + let result = get_oauth_signing_key(&pool).await.unwrap(); 320 + assert!(result.is_none()); 246 321 } 247 322 }