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}