this repo has no description
1use reqwest::Client; 2use serde::{Deserialize, Serialize}; 3use std::collections::HashMap; 4use std::sync::Arc; 5use tokio::sync::RwLock; 6 7use super::OAuthError; 8 9#[derive(Debug, Clone, Serialize, Deserialize)] 10pub struct ClientMetadata { 11 pub client_id: String, 12 #[serde(skip_serializing_if = "Option::is_none")] 13 pub client_name: Option<String>, 14 #[serde(skip_serializing_if = "Option::is_none")] 15 pub client_uri: Option<String>, 16 #[serde(skip_serializing_if = "Option::is_none")] 17 pub logo_uri: Option<String>, 18 pub redirect_uris: Vec<String>, 19 #[serde(default)] 20 pub grant_types: Vec<String>, 21 #[serde(default)] 22 pub response_types: Vec<String>, 23 #[serde(skip_serializing_if = "Option::is_none")] 24 pub scope: Option<String>, 25 #[serde(skip_serializing_if = "Option::is_none")] 26 pub token_endpoint_auth_method: Option<String>, 27 #[serde(skip_serializing_if = "Option::is_none")] 28 pub dpop_bound_access_tokens: Option<bool>, 29 #[serde(skip_serializing_if = "Option::is_none")] 30 pub jwks: Option<serde_json::Value>, 31 #[serde(skip_serializing_if = "Option::is_none")] 32 pub jwks_uri: Option<String>, 33 #[serde(skip_serializing_if = "Option::is_none")] 34 pub application_type: Option<String>, 35} 36 37impl Default for ClientMetadata { 38 fn default() -> Self { 39 Self { 40 client_id: String::new(), 41 client_name: None, 42 client_uri: None, 43 logo_uri: None, 44 redirect_uris: Vec::new(), 45 grant_types: vec!["authorization_code".to_string()], 46 response_types: vec!["code".to_string()], 47 scope: None, 48 token_endpoint_auth_method: Some("none".to_string()), 49 dpop_bound_access_tokens: None, 50 jwks: None, 51 jwks_uri: None, 52 application_type: None, 53 } 54 } 55} 56 57#[derive(Clone)] 58pub struct ClientMetadataCache { 59 cache: Arc<RwLock<HashMap<String, CachedMetadata>>>, 60 jwks_cache: Arc<RwLock<HashMap<String, CachedJwks>>>, 61 http_client: Client, 62 cache_ttl_secs: u64, 63} 64 65struct CachedMetadata { 66 metadata: ClientMetadata, 67 cached_at: std::time::Instant, 68} 69 70struct CachedJwks { 71 jwks: serde_json::Value, 72 cached_at: std::time::Instant, 73} 74 75impl ClientMetadataCache { 76 pub fn new(cache_ttl_secs: u64) -> Self { 77 Self { 78 cache: Arc::new(RwLock::new(HashMap::new())), 79 jwks_cache: Arc::new(RwLock::new(HashMap::new())), 80 http_client: Client::builder() 81 .timeout(std::time::Duration::from_secs(30)) 82 .connect_timeout(std::time::Duration::from_secs(10)) 83 .pool_max_idle_per_host(10) 84 .pool_idle_timeout(std::time::Duration::from_secs(90)) 85 .user_agent( 86 "Tranquil-PDS/1.0 (ATProto; +https://tangled.org/lewis.moe/bspds-sandbox)", 87 ) 88 .build() 89 .unwrap_or_else(|_| Client::new()), 90 cache_ttl_secs, 91 } 92 } 93 94 fn is_loopback_client(client_id: &str) -> bool { 95 if let Ok(url) = reqwest::Url::parse(client_id) { 96 url.scheme() == "http" 97 && url.host_str() == Some("localhost") 98 && url.port().is_none() 99 // empty path 100 && url.path() == "/" 101 } else { 102 false 103 } 104 } 105 106 fn build_loopback_metadata(client_id: &str) -> Result<ClientMetadata, OAuthError> { 107 let url = reqwest::Url::parse(client_id) 108 .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".into()))?; 109 let mut redirect_uris = Vec::<String>::new(); 110 let mut scope: Option<String> = None; 111 for (key, value) in url.query_pairs() { 112 if key == "redirect_uri" { 113 redirect_uris.push(value.to_string()); 114 break; 115 } 116 if key == "scope" { 117 scope = Some(value.into()); 118 break; 119 } 120 } 121 if redirect_uris.is_empty() { 122 redirect_uris.push("http://127.0.0.1/".into()); 123 redirect_uris.push("http://[::1]/".into()); 124 } 125 if scope.is_none() { 126 scope = Some("atproto".into()); 127 } 128 Ok(ClientMetadata { 129 client_id: client_id.into(), 130 client_name: Some("Loopback Client".into()), 131 client_uri: None, 132 logo_uri: None, 133 redirect_uris, 134 grant_types: vec!["authorization_code".into(), "refresh_token".into()], 135 response_types: vec!["code".into()], 136 scope, 137 token_endpoint_auth_method: Some("none".into()), 138 dpop_bound_access_tokens: Some(false), 139 jwks: None, 140 jwks_uri: None, 141 application_type: Some("native".into()), 142 }) 143 } 144 145 pub async fn get(&self, client_id: &str) -> Result<ClientMetadata, OAuthError> { 146 if Self::is_loopback_client(client_id) { 147 return Self::build_loopback_metadata(client_id); 148 } 149 { 150 let cache = self.cache.read().await; 151 if let Some(cached) = cache.get(client_id) 152 && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs 153 { 154 return Ok(cached.metadata.clone()); 155 } 156 } 157 let metadata = self.fetch_metadata(client_id).await?; 158 { 159 let mut cache = self.cache.write().await; 160 cache.insert( 161 client_id.to_string(), 162 CachedMetadata { 163 metadata: metadata.clone(), 164 cached_at: std::time::Instant::now(), 165 }, 166 ); 167 } 168 Ok(metadata) 169 } 170 171 pub async fn get_jwks( 172 &self, 173 metadata: &ClientMetadata, 174 ) -> Result<serde_json::Value, OAuthError> { 175 if let Some(jwks) = &metadata.jwks { 176 return Ok(jwks.clone()); 177 } 178 let jwks_uri = metadata.jwks_uri.as_ref().ok_or_else(|| { 179 OAuthError::InvalidClient( 180 "Client using private_key_jwt must have jwks or jwks_uri".to_string(), 181 ) 182 })?; 183 { 184 let cache = self.jwks_cache.read().await; 185 if let Some(cached) = cache.get(jwks_uri) 186 && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs 187 { 188 return Ok(cached.jwks.clone()); 189 } 190 } 191 let jwks = self.fetch_jwks(jwks_uri).await?; 192 { 193 let mut cache = self.jwks_cache.write().await; 194 cache.insert( 195 jwks_uri.clone(), 196 CachedJwks { 197 jwks: jwks.clone(), 198 cached_at: std::time::Instant::now(), 199 }, 200 ); 201 } 202 Ok(jwks) 203 } 204 205 async fn fetch_jwks(&self, jwks_uri: &str) -> Result<serde_json::Value, OAuthError> { 206 if !jwks_uri.starts_with("https://") 207 && (!jwks_uri.starts_with("http://") 208 || (!jwks_uri.contains("localhost") && !jwks_uri.contains("127.0.0.1"))) 209 { 210 return Err(OAuthError::InvalidClient( 211 "jwks_uri must use https (except for localhost)".to_string(), 212 )); 213 } 214 let response = self 215 .http_client 216 .get(jwks_uri) 217 .header("Accept", "application/json") 218 .send() 219 .await 220 .map_err(|e| { 221 OAuthError::InvalidClient(format!("Failed to fetch JWKS from {}: {}", jwks_uri, e)) 222 })?; 223 if !response.status().is_success() { 224 return Err(OAuthError::InvalidClient(format!( 225 "Failed to fetch JWKS: HTTP {}", 226 response.status() 227 ))); 228 } 229 let jwks: serde_json::Value = response 230 .json() 231 .await 232 .map_err(|e| OAuthError::InvalidClient(format!("Invalid JWKS JSON: {}", e)))?; 233 if jwks.get("keys").and_then(|k| k.as_array()).is_none() { 234 return Err(OAuthError::InvalidClient( 235 "JWKS must contain a 'keys' array".to_string(), 236 )); 237 } 238 Ok(jwks) 239 } 240 241 async fn fetch_metadata(&self, client_id: &str) -> Result<ClientMetadata, OAuthError> { 242 if !client_id.starts_with("http://") && !client_id.starts_with("https://") { 243 return Err(OAuthError::InvalidClient( 244 "client_id must be a URL".to_string(), 245 )); 246 } 247 if client_id.starts_with("http://") 248 && !client_id.contains("localhost") 249 && !client_id.contains("127.0.0.1") 250 { 251 return Err(OAuthError::InvalidClient( 252 "Non-localhost client_id must use https".to_string(), 253 )); 254 } 255 let response = self 256 .http_client 257 .get(client_id) 258 .header("Accept", "application/json") 259 .send() 260 .await 261 .map_err(|e| { 262 tracing::warn!(client_id = %client_id, error = %e, "Failed to fetch client metadata"); 263 OAuthError::InvalidClient(format!("Failed to fetch client metadata: {}", e)) 264 })?; 265 if !response.status().is_success() { 266 tracing::warn!(client_id = %client_id, status = %response.status(), "Failed to fetch client metadata"); 267 return Err(OAuthError::InvalidClient(format!( 268 "Failed to fetch client metadata: HTTP {}", 269 response.status() 270 ))); 271 } 272 let mut metadata: ClientMetadata = response.json().await.map_err(|e| { 273 OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e)) 274 })?; 275 if metadata.client_id.is_empty() { 276 metadata.client_id = client_id.to_string(); 277 } else if metadata.client_id != client_id { 278 return Err(OAuthError::InvalidClient( 279 "client_id in metadata does not match request".to_string(), 280 )); 281 } 282 self.validate_metadata(&metadata)?; 283 Ok(metadata) 284 } 285 286 fn validate_metadata(&self, metadata: &ClientMetadata) -> Result<(), OAuthError> { 287 if metadata.redirect_uris.is_empty() { 288 return Err(OAuthError::InvalidClient( 289 "redirect_uris is required".to_string(), 290 )); 291 } 292 for uri in &metadata.redirect_uris { 293 self.validate_redirect_uri_format(uri)?; 294 } 295 if !metadata.grant_types.is_empty() 296 && !metadata 297 .grant_types 298 .contains(&"authorization_code".to_string()) 299 { 300 return Err(OAuthError::InvalidClient( 301 "authorization_code grant type is required".to_string(), 302 )); 303 } 304 if !metadata.response_types.is_empty() 305 && !metadata.response_types.contains(&"code".to_string()) 306 { 307 return Err(OAuthError::InvalidClient( 308 "code response type is required".to_string(), 309 )); 310 } 311 Ok(()) 312 } 313 314 pub fn validate_redirect_uri( 315 &self, 316 metadata: &ClientMetadata, 317 redirect_uri: &str, 318 ) -> Result<(), OAuthError> { 319 if metadata.redirect_uris.contains(&redirect_uri.to_string()) { 320 return Ok(()); 321 } 322 if Self::is_loopback_client(&metadata.client_id) 323 && let Ok(req_url) = reqwest::Url::parse(redirect_uri) 324 { 325 let req_host = req_url.host_str().unwrap_or(""); 326 let is_loopback_redirect = req_url.scheme() == "http" 327 && (req_host == "localhost" || req_host == "127.0.0.1" || req_host == "[::1]"); 328 if is_loopback_redirect { 329 return Ok(()); 330 } 331 } 332 Err(OAuthError::InvalidRequest( 333 "redirect_uri not registered for client".to_string(), 334 )) 335 } 336 337 fn validate_redirect_uri_format(&self, uri: &str) -> Result<(), OAuthError> { 338 if uri.contains('#') { 339 return Err(OAuthError::InvalidClient( 340 "redirect_uri must not contain a fragment".to_string(), 341 )); 342 } 343 let parsed = reqwest::Url::parse(uri) 344 .map_err(|_| OAuthError::InvalidClient(format!("Invalid redirect_uri: {}", uri)))?; 345 let scheme = parsed.scheme(); 346 if scheme == "http" { 347 let host = parsed.host_str().unwrap_or(""); 348 if host != "localhost" && host != "127.0.0.1" && host != "[::1]" { 349 return Err(OAuthError::InvalidClient( 350 "http redirect_uri only allowed for localhost".to_string(), 351 )); 352 } 353 } else if scheme == "https" { 354 } else if scheme.chars().all(|c| { 355 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '+' || c == '.' || c == '-' 356 }) { 357 if !scheme 358 .chars() 359 .next() 360 .map(|c| c.is_ascii_lowercase()) 361 .unwrap_or(false) 362 { 363 return Err(OAuthError::InvalidClient(format!( 364 "Invalid redirect_uri scheme: {}", 365 scheme 366 ))); 367 } 368 } else { 369 return Err(OAuthError::InvalidClient(format!( 370 "Invalid redirect_uri scheme: {}", 371 scheme 372 ))); 373 } 374 Ok(()) 375 } 376} 377 378impl ClientMetadata { 379 pub fn requires_dpop(&self) -> bool { 380 self.dpop_bound_access_tokens.unwrap_or(false) 381 } 382 383 pub fn auth_method(&self) -> &str { 384 self.token_endpoint_auth_method.as_deref().unwrap_or("none") 385 } 386} 387 388pub async fn verify_client_auth( 389 cache: &ClientMetadataCache, 390 metadata: &ClientMetadata, 391 client_auth: &super::ClientAuth, 392) -> Result<(), OAuthError> { 393 let expected_method = metadata.auth_method(); 394 match (expected_method, client_auth) { 395 ("none", super::ClientAuth::None) => Ok(()), 396 ("none", _) => Err(OAuthError::InvalidClient( 397 "Client is configured for no authentication, but credentials were provided".to_string(), 398 )), 399 ("private_key_jwt", super::ClientAuth::PrivateKeyJwt { client_assertion }) => { 400 verify_private_key_jwt_async(cache, metadata, client_assertion).await 401 } 402 ("private_key_jwt", _) => Err(OAuthError::InvalidClient( 403 "Client requires private_key_jwt authentication".to_string(), 404 )), 405 ("client_secret_post", super::ClientAuth::SecretPost { .. }) => { 406 Err(OAuthError::InvalidClient( 407 "client_secret_post is not supported for ATProto OAuth".to_string(), 408 )) 409 } 410 ("client_secret_basic", super::ClientAuth::SecretBasic { .. }) => { 411 Err(OAuthError::InvalidClient( 412 "client_secret_basic is not supported for ATProto OAuth".to_string(), 413 )) 414 } 415 (method, _) => Err(OAuthError::InvalidClient(format!( 416 "Unsupported or mismatched authentication method: {}", 417 method 418 ))), 419 } 420} 421 422async fn verify_private_key_jwt_async( 423 cache: &ClientMetadataCache, 424 metadata: &ClientMetadata, 425 client_assertion: &str, 426) -> Result<(), OAuthError> { 427 use base64::{ 428 Engine as _, 429 engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, 430 }; 431 let parts: Vec<&str> = client_assertion.split('.').collect(); 432 if parts.len() != 3 { 433 return Err(OAuthError::InvalidClient( 434 "Invalid client_assertion format".to_string(), 435 )); 436 } 437 let header_bytes = URL_SAFE_NO_PAD 438 .decode(parts[0]) 439 .or_else(|_| STANDARD.decode(parts[0])) 440 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header encoding".to_string()))?; 441 let header: serde_json::Value = serde_json::from_slice(&header_bytes) 442 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header JSON".to_string()))?; 443 let alg = header 444 .get("alg") 445 .and_then(|a| a.as_str()) 446 .ok_or_else(|| OAuthError::InvalidClient("Missing alg in client_assertion".to_string()))?; 447 if !matches!( 448 alg, 449 "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA" 450 ) { 451 return Err(OAuthError::InvalidClient(format!( 452 "Unsupported client_assertion algorithm: {}", 453 alg 454 ))); 455 } 456 let kid = header.get("kid").and_then(|k| k.as_str()); 457 let payload_bytes = URL_SAFE_NO_PAD 458 .decode(parts[1]) 459 .or_else(|_| STANDARD.decode(parts[1])) 460 .map_err(|e| { 461 tracing::warn!(error = %e, payload_part = parts[1], "Invalid assertion payload encoding"); 462 OAuthError::InvalidClient("Invalid assertion payload encoding".to_string()) 463 })?; 464 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes) 465 .map_err(|_| OAuthError::InvalidClient("Invalid assertion payload JSON".to_string()))?; 466 let iss = payload 467 .get("iss") 468 .and_then(|i| i.as_str()) 469 .ok_or_else(|| OAuthError::InvalidClient("Missing iss in client_assertion".to_string()))?; 470 if iss != metadata.client_id { 471 return Err(OAuthError::InvalidClient( 472 "client_assertion iss does not match client_id".to_string(), 473 )); 474 } 475 let sub = payload 476 .get("sub") 477 .and_then(|s| s.as_str()) 478 .ok_or_else(|| OAuthError::InvalidClient("Missing sub in client_assertion".to_string()))?; 479 if sub != metadata.client_id { 480 return Err(OAuthError::InvalidClient( 481 "client_assertion sub does not match client_id".to_string(), 482 )); 483 } 484 let now = chrono::Utc::now().timestamp(); 485 let exp = payload.get("exp").and_then(|e| e.as_i64()); 486 let iat = payload.get("iat").and_then(|i| i.as_i64()); 487 if let Some(exp) = exp { 488 if exp < now { 489 return Err(OAuthError::InvalidClient( 490 "client_assertion has expired".to_string(), 491 )); 492 } 493 } else if let Some(iat) = iat { 494 let max_age_secs = 300; 495 if now - iat > max_age_secs { 496 tracing::warn!( 497 iat = iat, 498 now = now, 499 "client_assertion too old (no exp, using iat)" 500 ); 501 return Err(OAuthError::InvalidClient( 502 "client_assertion is too old".to_string(), 503 )); 504 } 505 } else { 506 return Err(OAuthError::InvalidClient( 507 "client_assertion must have exp or iat claim".to_string(), 508 )); 509 } 510 if let Some(iat) = iat 511 && iat > now + 60 512 { 513 return Err(OAuthError::InvalidClient( 514 "client_assertion iat is in the future".to_string(), 515 )); 516 } 517 let jwks = cache.get_jwks(metadata).await?; 518 let keys = jwks 519 .get("keys") 520 .and_then(|k| k.as_array()) 521 .ok_or_else(|| OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string()))?; 522 let matching_keys: Vec<&serde_json::Value> = if let Some(kid) = kid { 523 keys.iter() 524 .filter(|k| k.get("kid").and_then(|v| v.as_str()) == Some(kid)) 525 .collect() 526 } else { 527 keys.iter().collect() 528 }; 529 if matching_keys.is_empty() { 530 return Err(OAuthError::InvalidClient( 531 "No matching key found in client JWKS".to_string(), 532 )); 533 } 534 let signing_input = format!("{}.{}", parts[0], parts[1]); 535 let signature_bytes = URL_SAFE_NO_PAD 536 .decode(parts[2]) 537 .map_err(|_| OAuthError::InvalidClient("Invalid signature encoding".to_string()))?; 538 for key in matching_keys { 539 let key_alg = key.get("alg").and_then(|a| a.as_str()); 540 if key_alg.is_some() && key_alg != Some(alg) { 541 continue; 542 } 543 let kty = key.get("kty").and_then(|k| k.as_str()).unwrap_or(""); 544 let verified = match (alg, kty) { 545 ("ES256", "EC") => verify_es256(key, &signing_input, &signature_bytes), 546 ("ES384", "EC") => verify_es384(key, &signing_input, &signature_bytes), 547 ("RS256" | "RS384" | "RS512", "RSA") => { 548 verify_rsa(alg, key, &signing_input, &signature_bytes) 549 } 550 ("EdDSA", "OKP") => verify_eddsa(key, &signing_input, &signature_bytes), 551 _ => continue, 552 }; 553 if verified.is_ok() { 554 return Ok(()); 555 } 556 } 557 Err(OAuthError::InvalidClient( 558 "client_assertion signature verification failed".to_string(), 559 )) 560} 561 562fn verify_es256( 563 key: &serde_json::Value, 564 signing_input: &str, 565 signature: &[u8], 566) -> Result<(), OAuthError> { 567 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 568 use p256::EncodedPoint; 569 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; 570 let x = key 571 .get("x") 572 .and_then(|v| v.as_str()) 573 .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?; 574 let y = key 575 .get("y") 576 .and_then(|v| v.as_str()) 577 .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?; 578 let x_bytes = URL_SAFE_NO_PAD 579 .decode(x) 580 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?; 581 let y_bytes = URL_SAFE_NO_PAD 582 .decode(y) 583 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?; 584 let mut point_bytes = vec![0x04]; 585 point_bytes.extend_from_slice(&x_bytes); 586 point_bytes.extend_from_slice(&y_bytes); 587 let point = EncodedPoint::from_bytes(&point_bytes) 588 .map_err(|_| OAuthError::InvalidClient("Invalid EC point".to_string()))?; 589 let verifying_key = VerifyingKey::from_encoded_point(&point) 590 .map_err(|_| OAuthError::InvalidClient("Invalid EC key".to_string()))?; 591 let sig = Signature::from_slice(signature) 592 .map_err(|_| OAuthError::InvalidClient("Invalid ES256 signature format".to_string()))?; 593 verifying_key 594 .verify(signing_input.as_bytes(), &sig) 595 .map_err(|_| OAuthError::InvalidClient("ES256 signature verification failed".to_string())) 596} 597 598fn verify_es384( 599 key: &serde_json::Value, 600 signing_input: &str, 601 signature: &[u8], 602) -> Result<(), OAuthError> { 603 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 604 use p384::EncodedPoint; 605 use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; 606 let x = key 607 .get("x") 608 .and_then(|v| v.as_str()) 609 .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?; 610 let y = key 611 .get("y") 612 .and_then(|v| v.as_str()) 613 .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?; 614 let x_bytes = URL_SAFE_NO_PAD 615 .decode(x) 616 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?; 617 let y_bytes = URL_SAFE_NO_PAD 618 .decode(y) 619 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?; 620 let mut point_bytes = vec![0x04]; 621 point_bytes.extend_from_slice(&x_bytes); 622 point_bytes.extend_from_slice(&y_bytes); 623 let point = EncodedPoint::from_bytes(&point_bytes) 624 .map_err(|_| OAuthError::InvalidClient("Invalid EC point".to_string()))?; 625 let verifying_key = VerifyingKey::from_encoded_point(&point) 626 .map_err(|_| OAuthError::InvalidClient("Invalid EC key".to_string()))?; 627 let sig = Signature::from_slice(signature) 628 .map_err(|_| OAuthError::InvalidClient("Invalid ES384 signature format".to_string()))?; 629 verifying_key 630 .verify(signing_input.as_bytes(), &sig) 631 .map_err(|_| OAuthError::InvalidClient("ES384 signature verification failed".to_string())) 632} 633 634fn verify_rsa( 635 _alg: &str, 636 _key: &serde_json::Value, 637 _signing_input: &str, 638 _signature: &[u8], 639) -> Result<(), OAuthError> { 640 Err(OAuthError::InvalidClient( 641 "RSA signature verification not yet supported - use EC keys".to_string(), 642 )) 643} 644 645fn verify_eddsa( 646 key: &serde_json::Value, 647 signing_input: &str, 648 signature: &[u8], 649) -> Result<(), OAuthError> { 650 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 651 use ed25519_dalek::{Signature, Verifier, VerifyingKey}; 652 let crv = key.get("crv").and_then(|c| c.as_str()).unwrap_or(""); 653 if crv != "Ed25519" { 654 return Err(OAuthError::InvalidClient(format!( 655 "Unsupported EdDSA curve: {}", 656 crv 657 ))); 658 } 659 let x = key 660 .get("x") 661 .and_then(|v| v.as_str()) 662 .ok_or_else(|| OAuthError::InvalidClient("Missing x in OKP key".to_string()))?; 663 let x_bytes = URL_SAFE_NO_PAD 664 .decode(x) 665 .map_err(|_| OAuthError::InvalidClient("Invalid x encoding".to_string()))?; 666 let key_bytes: [u8; 32] = x_bytes 667 .try_into() 668 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key length".to_string()))?; 669 let verifying_key = VerifyingKey::from_bytes(&key_bytes) 670 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key".to_string()))?; 671 let sig_bytes: [u8; 64] = signature 672 .try_into() 673 .map_err(|_| OAuthError::InvalidClient("Invalid EdDSA signature length".to_string()))?; 674 let sig = Signature::from_bytes(&sig_bytes); 675 verifying_key 676 .verify(signing_input.as_bytes(), &sig) 677 .map_err(|_| OAuthError::InvalidClient("EdDSA signature verification failed".to_string())) 678}