agent-pay 0.1.0

L402 + DID-signed invoices: agent-to-agent Lightning payments (Rust port of @p-vbordei/agent-pay)
Documentation
use std::sync::Arc;

use agent_pay::{
    did_key_from_public_key, generate_key_pair, public_key_from_did_key, sign_compact,
    verification_method_id, verify_compact, Error, ResolveKey,
};
use serde_json::{json, Value};

fn make_resolver(did: String) -> ResolveKey {
    Arc::new(move |kid: String| {
        let did = did.clone();
        Box::pin(async move {
            if !kid.starts_with(&did) {
                return Err(Error::Other(format!("unknown kid {kid}")));
            }
            public_key_from_did_key(&did)
        })
    })
}

#[tokio::test]
async fn compact_jws_roundtrips_a_json_payload() {
    let kp = generate_key_pair();
    let did = did_key_from_public_key(&kp.public_key).unwrap();
    let kid = verification_method_id(&did).unwrap();
    let payload = json!({"v": "agent-pay/0.1", "hello": "world"});
    let token = sign_compact(&payload, &kp.private_key, &kid).await.unwrap();
    assert_eq!(token.split('.').count(), 3);
    let resolver = make_resolver(did);
    let (p, k) = verify_compact(&token, &resolver).await.unwrap();
    assert_eq!(p, payload);
    assert_eq!(k, kid);
}

#[tokio::test]
async fn verify_compact_rejects_tampered_payload() {
    let kp = generate_key_pair();
    let did = did_key_from_public_key(&kp.public_key).unwrap();
    let kid = verification_method_id(&did).unwrap();
    let token = sign_compact(&json!({"a": 1}), &kp.private_key, &kid)
        .await
        .unwrap();
    let parts: Vec<&str> = token.split('.').collect();
    let bad = format!("{}.{}AA.{}", parts[0], parts[1], parts[2]);
    let resolver = make_resolver(did);
    let err = verify_compact(&bad, &resolver).await.unwrap_err();
    let msg = format!("{err}").to_lowercase();
    assert!(msg.contains("signature"), "msg={msg}");
}

#[tokio::test]
async fn verify_compact_rejects_unsupported_alg() {
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
    use base64::Engine;
    let header =
        URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json!({"alg": "HS256", "kid": "x"})).unwrap());
    let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json!({"a": 1})).unwrap());
    let token = format!("{header}.{payload}.AAAA");
    let resolver: ResolveKey = Arc::new(|_| {
        Box::pin(async move {
            let _ = Value::Null;
            Ok([0u8; 32])
        })
    });
    let err = verify_compact(&token, &resolver).await.unwrap_err();
    let msg = format!("{err}").to_lowercase();
    assert!(msg.contains("alg"), "msg={msg}");
}