Skip to main content

agent_pay/
jws.rs

1//! Compact JWS over Ed25519 with JCS-canonical payload.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use base64::engine::general_purpose::URL_SAFE_NO_PAD;
8use base64::Engine;
9use serde_json::{json, Value};
10
11use crate::error::Error;
12use crate::jcs::canonical_json;
13use crate::keys::{ed25519_sign, ed25519_verify};
14
15pub type ResolveKey = Arc<
16    dyn (Fn(String) -> Pin<Box<dyn Future<Output = Result<[u8; 32], Error>> + Send>>) + Send + Sync,
17>;
18
19fn b64url_encode(data: &[u8]) -> String {
20    URL_SAFE_NO_PAD.encode(data)
21}
22
23fn b64url_decode(s: &str) -> Result<Vec<u8>, Error> {
24    URL_SAFE_NO_PAD
25        .decode(s)
26        .map_err(|e| Error::Jws(format!("base64url: {e}")))
27}
28
29pub async fn sign_compact(
30    payload: &Value,
31    private_key: &[u8; 32],
32    kid: &str,
33) -> Result<String, Error> {
34    let header = json!({ "alg": "EdDSA", "typ": "JWS", "kid": kid });
35    let header_bytes = canonical_json(&header)?;
36    let payload_bytes = canonical_json(payload)?;
37    let header_b64 = b64url_encode(&header_bytes);
38    let payload_b64 = b64url_encode(&payload_bytes);
39    let signing_input = format!("{header_b64}.{payload_b64}");
40    let sig = ed25519_sign(private_key, signing_input.as_bytes());
41    Ok(format!(
42        "{header_b64}.{payload_b64}.{}",
43        b64url_encode(&sig)
44    ))
45}
46
47pub async fn verify_compact(
48    token: &str,
49    resolve_key: &ResolveKey,
50) -> Result<(Value, String), Error> {
51    let parts: Vec<&str> = token.split('.').collect();
52    if parts.len() != 3 {
53        return Err(Error::Jws("compact JWS must have 3 parts".into()));
54    }
55    let header_b64 = parts[0];
56    let payload_b64 = parts[1];
57    let sig_b64 = parts[2];
58    let header_bytes = b64url_decode(header_b64)?;
59    let header: Value = serde_json::from_slice(&header_bytes)?;
60    let alg = header.get("alg").and_then(|v| v.as_str()).unwrap_or("");
61    if alg != "EdDSA" {
62        return Err(Error::Jws(format!("unsupported JWS alg: {alg}")));
63    }
64    let kid = header
65        .get("kid")
66        .and_then(|v| v.as_str())
67        .ok_or_else(|| Error::Jws("JWS header missing kid".into()))?
68        .to_string();
69    let public_key = (resolve_key)(kid.clone()).await?;
70    let signing_input = format!("{header_b64}.{payload_b64}");
71    let sig = b64url_decode(sig_b64)?;
72    if !ed25519_verify(&public_key, signing_input.as_bytes(), &sig) {
73        return Err(Error::Jws("JWS signature verification failed".into()));
74    }
75    let payload_bytes = b64url_decode(payload_b64)?;
76    let payload: Value = serde_json::from_slice(&payload_bytes)?;
77    Ok((payload, kid))
78}