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