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![ 130 "authorization_code".into(), 131 "refresh_token".into(), 132 ], 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 OAuthError::InvalidClient(format!("Failed to fetch client metadata: {}", e)) 261 })?; 262 if !response.status().is_success() { 263 return Err(OAuthError::InvalidClient(format!( 264 "Failed to fetch client metadata: HTTP {}", 265 response.status() 266 ))); 267 } 268 let mut metadata: ClientMetadata = response.json().await.map_err(|e| { 269 OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e)) 270 })?; 271 if metadata.client_id.is_empty() { 272 metadata.client_id = client_id.to_string(); 273 } else if metadata.client_id != client_id { 274 return Err(OAuthError::InvalidClient( 275 "client_id in metadata does not match request".to_string(), 276 )); 277 } 278 self.validate_metadata(&metadata)?; 279 Ok(metadata) 280 } 281 282 fn validate_metadata(&self, metadata: &ClientMetadata) -> Result<(), OAuthError> { 283 if metadata.redirect_uris.is_empty() { 284 return Err(OAuthError::InvalidClient( 285 "redirect_uris is required".to_string(), 286 )); 287 } 288 for uri in &metadata.redirect_uris { 289 self.validate_redirect_uri_format(uri)?; 290 } 291 if !metadata.grant_types.is_empty() 292 && !metadata 293 .grant_types 294 .contains(&"authorization_code".to_string()) 295 { 296 return Err(OAuthError::InvalidClient( 297 "authorization_code grant type is required".to_string(), 298 )); 299 } 300 if !metadata.response_types.is_empty() 301 && !metadata.response_types.contains(&"code".to_string()) 302 { 303 return Err(OAuthError::InvalidClient( 304 "code response type is required".to_string(), 305 )); 306 } 307 Ok(()) 308 } 309 310 pub fn validate_redirect_uri( 311 &self, 312 metadata: &ClientMetadata, 313 redirect_uri: &str, 314 ) -> Result<(), OAuthError> { 315 if metadata.redirect_uris.contains(&redirect_uri.to_string()) { 316 return Ok(()); 317 } 318 if Self::is_loopback_client(&metadata.client_id) 319 && let Ok(req_url) = reqwest::Url::parse(redirect_uri) 320 { 321 let req_host = req_url.host_str().unwrap_or(""); 322 let is_loopback_redirect = req_url.scheme() == "http" 323 && (req_host == "localhost" || req_host == "127.0.0.1" || req_host == "[::1]"); 324 if is_loopback_redirect { 325 return Ok(()); 326 } 327 } 328 Err(OAuthError::InvalidRequest( 329 "redirect_uri not registered for client".to_string(), 330 )) 331 } 332 333 fn validate_redirect_uri_format(&self, uri: &str) -> Result<(), OAuthError> { 334 if uri.contains('#') { 335 return Err(OAuthError::InvalidClient( 336 "redirect_uri must not contain a fragment".to_string(), 337 )); 338 } 339 let parsed = reqwest::Url::parse(uri) 340 .map_err(|_| OAuthError::InvalidClient(format!("Invalid redirect_uri: {}", uri)))?; 341 let scheme = parsed.scheme(); 342 if scheme == "http" { 343 let host = parsed.host_str().unwrap_or(""); 344 if host != "localhost" && host != "127.0.0.1" && host != "[::1]" { 345 return Err(OAuthError::InvalidClient( 346 "http redirect_uri only allowed for localhost".to_string(), 347 )); 348 } 349 } else if scheme == "https" { 350 } else if scheme.chars().all(|c| { 351 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '+' || c == '.' || c == '-' 352 }) { 353 if !scheme 354 .chars() 355 .next() 356 .map(|c| c.is_ascii_lowercase()) 357 .unwrap_or(false) 358 { 359 return Err(OAuthError::InvalidClient(format!( 360 "Invalid redirect_uri scheme: {}", 361 scheme 362 ))); 363 } 364 } else { 365 return Err(OAuthError::InvalidClient(format!( 366 "Invalid redirect_uri scheme: {}", 367 scheme 368 ))); 369 } 370 Ok(()) 371 } 372} 373 374impl ClientMetadata { 375 pub fn requires_dpop(&self) -> bool { 376 self.dpop_bound_access_tokens.unwrap_or(false) 377 } 378 379 pub fn auth_method(&self) -> &str { 380 self.token_endpoint_auth_method.as_deref().unwrap_or("none") 381 } 382} 383 384pub async fn verify_client_auth( 385 cache: &ClientMetadataCache, 386 metadata: &ClientMetadata, 387 client_auth: &super::ClientAuth, 388) -> Result<(), OAuthError> { 389 let expected_method = metadata.auth_method(); 390 match (expected_method, client_auth) { 391 ("none", super::ClientAuth::None) => Ok(()), 392 ("none", _) => Err(OAuthError::InvalidClient( 393 "Client is configured for no authentication, but credentials were provided".to_string(), 394 )), 395 ("private_key_jwt", super::ClientAuth::PrivateKeyJwt { client_assertion }) => { 396 verify_private_key_jwt_async(cache, metadata, client_assertion).await 397 } 398 ("private_key_jwt", _) => Err(OAuthError::InvalidClient( 399 "Client requires private_key_jwt authentication".to_string(), 400 )), 401 ("client_secret_post", super::ClientAuth::SecretPost { .. }) => { 402 Err(OAuthError::InvalidClient( 403 "client_secret_post is not supported for ATProto OAuth".to_string(), 404 )) 405 } 406 ("client_secret_basic", super::ClientAuth::SecretBasic { .. }) => { 407 Err(OAuthError::InvalidClient( 408 "client_secret_basic is not supported for ATProto OAuth".to_string(), 409 )) 410 } 411 (method, _) => Err(OAuthError::InvalidClient(format!( 412 "Unsupported or mismatched authentication method: {}", 413 method 414 ))), 415 } 416} 417 418async fn verify_private_key_jwt_async( 419 cache: &ClientMetadataCache, 420 metadata: &ClientMetadata, 421 client_assertion: &str, 422) -> Result<(), OAuthError> { 423 use base64::{ 424 Engine as _, 425 engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, 426 }; 427 let parts: Vec<&str> = client_assertion.split('.').collect(); 428 if parts.len() != 3 { 429 return Err(OAuthError::InvalidClient( 430 "Invalid client_assertion format".to_string(), 431 )); 432 } 433 let header_bytes = URL_SAFE_NO_PAD 434 .decode(parts[0]) 435 .or_else(|_| STANDARD.decode(parts[0])) 436 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header encoding".to_string()))?; 437 let header: serde_json::Value = serde_json::from_slice(&header_bytes) 438 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header JSON".to_string()))?; 439 let alg = header 440 .get("alg") 441 .and_then(|a| a.as_str()) 442 .ok_or_else(|| OAuthError::InvalidClient("Missing alg in client_assertion".to_string()))?; 443 if !matches!( 444 alg, 445 "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA" 446 ) { 447 return Err(OAuthError::InvalidClient(format!( 448 "Unsupported client_assertion algorithm: {}", 449 alg 450 ))); 451 } 452 let kid = header.get("kid").and_then(|k| k.as_str()); 453 let payload_bytes = URL_SAFE_NO_PAD 454 .decode(parts[1]) 455 .or_else(|_| STANDARD.decode(parts[1])) 456 .map_err(|e| { 457 tracing::warn!(error = %e, payload_part = parts[1], "Invalid assertion payload encoding"); 458 OAuthError::InvalidClient("Invalid assertion payload encoding".to_string()) 459 })?; 460 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes) 461 .map_err(|_| OAuthError::InvalidClient("Invalid assertion payload JSON".to_string()))?; 462 let iss = payload 463 .get("iss") 464 .and_then(|i| i.as_str()) 465 .ok_or_else(|| OAuthError::InvalidClient("Missing iss in client_assertion".to_string()))?; 466 if iss != metadata.client_id { 467 return Err(OAuthError::InvalidClient( 468 "client_assertion iss does not match client_id".to_string(), 469 )); 470 } 471 let sub = payload 472 .get("sub") 473 .and_then(|s| s.as_str()) 474 .ok_or_else(|| OAuthError::InvalidClient("Missing sub in client_assertion".to_string()))?; 475 if sub != metadata.client_id { 476 return Err(OAuthError::InvalidClient( 477 "client_assertion sub does not match client_id".to_string(), 478 )); 479 } 480 let now = chrono::Utc::now().timestamp(); 481 let exp = payload.get("exp").and_then(|e| e.as_i64()); 482 let iat = payload.get("iat").and_then(|i| i.as_i64()); 483 if let Some(exp) = exp { 484 if exp < now { 485 return Err(OAuthError::InvalidClient( 486 "client_assertion has expired".to_string(), 487 )); 488 } 489 } else if let Some(iat) = iat { 490 let max_age_secs = 300; 491 if now - iat > max_age_secs { 492 tracing::warn!( 493 iat = iat, 494 now = now, 495 "client_assertion too old (no exp, using iat)" 496 ); 497 return Err(OAuthError::InvalidClient( 498 "client_assertion is too old".to_string(), 499 )); 500 } 501 } else { 502 return Err(OAuthError::InvalidClient( 503 "client_assertion must have exp or iat claim".to_string(), 504 )); 505 } 506 if let Some(iat) = iat 507 && iat > now + 60 508 { 509 return Err(OAuthError::InvalidClient( 510 "client_assertion iat is in the future".to_string(), 511 )); 512 } 513 let jwks = cache.get_jwks(metadata).await?; 514 let keys = jwks 515 .get("keys") 516 .and_then(|k| k.as_array()) 517 .ok_or_else(|| OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string()))?; 518 let matching_keys: Vec<&serde_json::Value> = if let Some(kid) = kid { 519 keys.iter() 520 .filter(|k| k.get("kid").and_then(|v| v.as_str()) == Some(kid)) 521 .collect() 522 } else { 523 keys.iter().collect() 524 }; 525 if matching_keys.is_empty() { 526 return Err(OAuthError::InvalidClient( 527 "No matching key found in client JWKS".to_string(), 528 )); 529 } 530 let signing_input = format!("{}.{}", parts[0], parts[1]); 531 let signature_bytes = URL_SAFE_NO_PAD 532 .decode(parts[2]) 533 .map_err(|_| OAuthError::InvalidClient("Invalid signature encoding".to_string()))?; 534 for key in matching_keys { 535 let key_alg = key.get("alg").and_then(|a| a.as_str()); 536 if key_alg.is_some() && key_alg != Some(alg) { 537 continue; 538 } 539 let kty = key.get("kty").and_then(|k| k.as_str()).unwrap_or(""); 540 let verified = match (alg, kty) { 541 ("ES256", "EC") => verify_es256(key, &signing_input, &signature_bytes), 542 ("ES384", "EC") => verify_es384(key, &signing_input, &signature_bytes), 543 ("RS256" | "RS384" | "RS512", "RSA") => { 544 verify_rsa(alg, key, &signing_input, &signature_bytes) 545 } 546 ("EdDSA", "OKP") => verify_eddsa(key, &signing_input, &signature_bytes), 547 _ => continue, 548 }; 549 if verified.is_ok() { 550 return Ok(()); 551 } 552 } 553 Err(OAuthError::InvalidClient( 554 "client_assertion signature verification failed".to_string(), 555 )) 556} 557 558fn verify_es256( 559 key: &serde_json::Value, 560 signing_input: &str, 561 signature: &[u8], 562) -> Result<(), OAuthError> { 563 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 564 use p256::EncodedPoint; 565 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; 566 let x = key 567 .get("x") 568 .and_then(|v| v.as_str()) 569 .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?; 570 let y = key 571 .get("y") 572 .and_then(|v| v.as_str()) 573 .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?; 574 let x_bytes = URL_SAFE_NO_PAD 575 .decode(x) 576 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?; 577 let y_bytes = URL_SAFE_NO_PAD 578 .decode(y) 579 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?; 580 let mut point_bytes = vec![0x04]; 581 point_bytes.extend_from_slice(&x_bytes); 582 point_bytes.extend_from_slice(&y_bytes); 583 let point = EncodedPoint::from_bytes(&point_bytes) 584 .map_err(|_| OAuthError::InvalidClient("Invalid EC point".to_string()))?; 585 let verifying_key = VerifyingKey::from_encoded_point(&point) 586 .map_err(|_| OAuthError::InvalidClient("Invalid EC key".to_string()))?; 587 let sig = Signature::from_slice(signature) 588 .map_err(|_| OAuthError::InvalidClient("Invalid ES256 signature format".to_string()))?; 589 verifying_key 590 .verify(signing_input.as_bytes(), &sig) 591 .map_err(|_| OAuthError::InvalidClient("ES256 signature verification failed".to_string())) 592} 593 594fn verify_es384( 595 key: &serde_json::Value, 596 signing_input: &str, 597 signature: &[u8], 598) -> Result<(), OAuthError> { 599 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 600 use p384::EncodedPoint; 601 use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; 602 let x = key 603 .get("x") 604 .and_then(|v| v.as_str()) 605 .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?; 606 let y = key 607 .get("y") 608 .and_then(|v| v.as_str()) 609 .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?; 610 let x_bytes = URL_SAFE_NO_PAD 611 .decode(x) 612 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?; 613 let y_bytes = URL_SAFE_NO_PAD 614 .decode(y) 615 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?; 616 let mut point_bytes = vec![0x04]; 617 point_bytes.extend_from_slice(&x_bytes); 618 point_bytes.extend_from_slice(&y_bytes); 619 let point = EncodedPoint::from_bytes(&point_bytes) 620 .map_err(|_| OAuthError::InvalidClient("Invalid EC point".to_string()))?; 621 let verifying_key = VerifyingKey::from_encoded_point(&point) 622 .map_err(|_| OAuthError::InvalidClient("Invalid EC key".to_string()))?; 623 let sig = Signature::from_slice(signature) 624 .map_err(|_| OAuthError::InvalidClient("Invalid ES384 signature format".to_string()))?; 625 verifying_key 626 .verify(signing_input.as_bytes(), &sig) 627 .map_err(|_| OAuthError::InvalidClient("ES384 signature verification failed".to_string())) 628} 629 630fn verify_rsa( 631 _alg: &str, 632 _key: &serde_json::Value, 633 _signing_input: &str, 634 _signature: &[u8], 635) -> Result<(), OAuthError> { 636 Err(OAuthError::InvalidClient( 637 "RSA signature verification not yet supported - use EC keys".to_string(), 638 )) 639} 640 641fn verify_eddsa( 642 key: &serde_json::Value, 643 signing_input: &str, 644 signature: &[u8], 645) -> Result<(), OAuthError> { 646 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 647 use ed25519_dalek::{Signature, Verifier, VerifyingKey}; 648 let crv = key.get("crv").and_then(|c| c.as_str()).unwrap_or(""); 649 if crv != "Ed25519" { 650 return Err(OAuthError::InvalidClient(format!( 651 "Unsupported EdDSA curve: {}", 652 crv 653 ))); 654 } 655 let x = key 656 .get("x") 657 .and_then(|v| v.as_str()) 658 .ok_or_else(|| OAuthError::InvalidClient("Missing x in OKP key".to_string()))?; 659 let x_bytes = URL_SAFE_NO_PAD 660 .decode(x) 661 .map_err(|_| OAuthError::InvalidClient("Invalid x encoding".to_string()))?; 662 let key_bytes: [u8; 32] = x_bytes 663 .try_into() 664 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key length".to_string()))?; 665 let verifying_key = VerifyingKey::from_bytes(&key_bytes) 666 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key".to_string()))?; 667 let sig_bytes: [u8; 64] = signature 668 .try_into() 669 .map_err(|_| OAuthError::InvalidClient("Invalid EdDSA signature length".to_string()))?; 670 let sig = Signature::from_bytes(&sig_bytes); 671 verifying_key 672 .verify(signing_input.as_bytes(), &sig) 673 .map_err(|_| OAuthError::InvalidClient("EdDSA signature verification failed".to_string())) 674}