1#![forbid(unsafe_code)]
2use serde::{Deserialize, Serialize};
3use serde_json::Value as Json;
4use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64URL, Engine as _};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Claims {
8 pub iss: Option<String>,
9 pub sub: String,
10 pub aud: Option<Json>,
11 pub iat: Option<i64>,
12 pub exp: Option<i64>,
13 pub jti: Option<String>,
14 #[serde(flatten)]
15 pub extra: Json,
16}
17
18fn b64url_decode_to_vec(s: &str) -> Result<Vec<u8>, String> {
19 B64URL.decode(s.as_bytes()).map_err(|_| "bad b64".into())
20}
21
22pub fn verify_ed25519_jwt_with_jwks(token: &str, jwks_json: &str, expected_iss: Option<&str>, expected_aud: Option<&str>) -> Result<Claims, String> {
25 let parts: Vec<&str> = token.split('.').collect();
26 if parts.len() != 3 { return Err("bad jwt".into()); }
27 let header_json = String::from_utf8(b64url_decode_to_vec(parts[0]).map_err(|_| "bad header b64")?).map_err(|_| "utf8 header")?;
28 let payload_json = String::from_utf8(b64url_decode_to_vec(parts[1]).map_err(|_| "bad payload b64")?).map_err(|_| "utf8 payload")?;
29 let header: Json = serde_json::from_str(&header_json).map_err(|_| "json header")?;
30 let payload: Claims = serde_json::from_str(&payload_json).map_err(|_| "json payload")?;
31
32 if let Some(iss) = expected_iss {
33 if payload.iss.as_deref() != Some(iss) { return Err("iss mismatch".into()); }
34 }
35 if let Some(aud) = expected_aud {
36 let ok = match &payload.aud {
37 Some(Json::String(a)) => a == aud,
38 Some(Json::Array(arr)) => arr.iter().any(|v| v.as_str() == Some(aud)),
39 _ => false
40 };
41 if !ok { return Err("aud mismatch".into()); }
42 }
43
44 let kid = header.get("kid").and_then(|v| v.as_str()).ok_or_else(|| "missing kid".to_string())?;
46 let jwks: Json = serde_json::from_str(jwks_json).map_err(|_| "bad jwks")?;
47 let keys = jwks.get("keys").and_then(|v| v.as_array()).ok_or_else(|| "no keys".to_string())?;
48 let mut x_b64: Option<&str> = None;
49 for k in keys {
50 if k.get("kty").and_then(|v| v.as_str()) != Some("OKP") { continue; }
51 if k.get("crv").and_then(|v| v.as_str()) != Some("Ed25519") { continue; }
52 let x = k.get("x").and_then(|v| v.as_str()).unwrap_or("");
53 let this_kid = k.get("kid").and_then(|v| v.as_str()).unwrap_or("");
54 if this_kid == kid || this_kid.is_empty() {
55 x_b64 = Some(x);
56 break;
57 }
58 }
59 let x = x_b64.ok_or_else(|| "kid not found".to_string())?;
60 let pk_bytes = b64url_decode_to_vec(x).map_err(|_| "bad x b64")?;
61 if pk_bytes.len() != 32 { return Err("bad ed25519 pk".into()); }
62
63 use ed25519_dalek::{VerifyingKey, Signature};
65 let vk = VerifyingKey::from_bytes(pk_bytes.as_slice().try_into().map_err(|_| "pk size")?).map_err(|_| "pk parse")?;
66 let signing_input = format!("{}.{}", parts[0], parts[1]);
67 let sig_bytes = b64url_decode_to_vec(parts[2]).map_err(|_| "bad sig b64")?;
68 let sig = Signature::from_bytes(sig_bytes.as_slice().try_into().map_err(|_| "sig size")?);
69 vk.verify_strict(signing_input.as_bytes(), &sig).map_err(|_| "sig verify")?;
70
71 Ok(payload)
72}
73
74#[cfg(feature = "assets")]
75pub mod assets {
76 pub fn sqlite_migration_001() -> &'static str { include_str!("../assets/sql/001_add_did.sql") }
77 pub fn postgres_migration_001() -> &'static str { include_str!("../assets/sql/001_add_did.pg.sql") }
78 pub fn receipt_action_v1_schema() -> &'static str { include_str!("../assets/receipts/action.v1.schema.json") }
79}