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