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