iicp-client 0.7.93

Use the open IICP AI mesh from Rust without running a node
Documentation
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::Deserialize;
const DOMAIN: &[u8] = b"iicp:dispatch-route-ticket:v1\n";
const AUDIENCE: &str = "iicp.directory.dispatch";
#[derive(Debug, Clone, Deserialize)]
pub struct DispatchRouteTicketClaims {
    pub v: u8,
    pub typ: String,
    pub iss: String,
    pub aud: String,
    pub jti: String,
    pub node_id: String,
    pub intent: String,
    pub iat: i64,
    pub exp: i64,
    #[serde(default)]
    pub policy_manifest_sha256: Option<String>,
}
pub fn verify_dispatch_route_ticket(
    token: &str,
    public_key_hex: &str,
    issuer: &str,
    node_id: &str,
    intent: &str,
    now_s: i64,
) -> Option<DispatchRouteTicketClaims> {
    let (payload, sig_hex) = token.split_once('.')?;
    if sig_hex.len() != 128 {
        return None;
    };
    let key = VerifyingKey::from_bytes(&hex::decode(public_key_hex).ok()?.try_into().ok()?).ok()?;
    let signature = Signature::from_bytes(&hex::decode(sig_hex).ok()?.try_into().ok()?);
    key.verify(&[DOMAIN, payload.as_bytes()].concat(), &signature)
        .ok()?;
    let claims: DispatchRouteTicketClaims =
        serde_json::from_slice(&URL_SAFE_NO_PAD.decode(payload).ok()?).ok()?;
    (claims.v == 1
        && claims.typ == "dispatch-route-ticket"
        && claims.iss == issuer
        && claims.aud == AUDIENCE
        && claims.node_id == node_id
        && claims.intent == intent
        && claims.exp > now_s
        && claims.jti.len() == 24
        && claims.policy_manifest_sha256.as_ref().is_none_or(|digest| {
            digest.len() == 64
                && digest
                    .bytes()
                    .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
        })
        && claims
            .jti
            .bytes()
            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()))
    .then_some(claims)
}

pub fn policy_manifest_binding_matches(
    claims: &DispatchRouteTicketClaims,
    route: &serde_json::Value,
) -> bool {
    let Some(expected) = claims.policy_manifest_sha256.as_deref() else {
        return true;
    };
    route
        .pointer("/node_policy_manifest/verification/canonical_sha256")
        .and_then(serde_json::Value::as_str)
        == Some(expected)
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn fixture_verifies() {
        let f: serde_json::Value =
            serde_json::from_str(include_str!("../parity/dispatch-route-ticket-v1.json")).unwrap();
        let c = &f["valid"]["claims"];
        assert!(verify_dispatch_route_ticket(
            f["valid"]["token"].as_str().unwrap(),
            f["public_key_hex"].as_str().unwrap(),
            c["iss"].as_str().unwrap(),
            c["node_id"].as_str().unwrap(),
            c["intent"].as_str().unwrap(),
            1_800_000_000
        )
        .is_some());
    }

    #[test]
    fn policy_manifest_binding_is_additive_and_fail_closed_when_present() {
        let claims = DispatchRouteTicketClaims {
            v: 1,
            typ: "dispatch-route-ticket".into(),
            iss: "https://directory.example".into(),
            aud: AUDIENCE.into(),
            jti: "0".repeat(24),
            node_id: "node".into(),
            intent: "urn:iicp:intent:llm:chat:v1".into(),
            iat: 1,
            exp: 2,
            policy_manifest_sha256: Some("a".repeat(64)),
        };
        let matching = serde_json::json!({"node_policy_manifest":{"verification":{"canonical_sha256":"a".repeat(64)}}});
        let altered = serde_json::json!({"node_policy_manifest":{"verification":{"canonical_sha256":"b".repeat(64)}}});
        assert!(policy_manifest_binding_matches(&claims, &matching));
        assert!(!policy_manifest_binding_matches(&claims, &altered));
        assert!(!policy_manifest_binding_matches(
            &claims,
            &serde_json::json!({})
        ));
        assert!(policy_manifest_binding_matches(
            &DispatchRouteTicketClaims {
                policy_manifest_sha256: None,
                ..claims
            },
            &serde_json::json!({})
        ));
    }
}

#[test]
fn canonical_fixture_vectors_fail_closed() {
    let f: serde_json::Value =
        serde_json::from_str(include_str!("../parity/dispatch-route-ticket-v1.json")).unwrap();
    for vector in f["validation_vectors"].as_array().unwrap() {
        let token = match vector["token"].as_str().unwrap() {
            "valid" => f["valid"]["token"].as_str().unwrap().to_string(),
            "valid+0" => format!("{}0", f["valid"]["token"].as_str().unwrap()),
            "wrong_audience" => f["wrong_audience"]["token"].as_str().unwrap().to_string(),
            other => other.to_string(),
        };
        let result = verify_dispatch_route_ticket(
            &token,
            f["public_key_hex"].as_str().unwrap(),
            vector["issuer"].as_str().unwrap(),
            vector["node_id"].as_str().unwrap(),
            vector["intent"].as_str().unwrap(),
            vector["now_s"].as_i64().unwrap(),
        );
        assert_eq!(
            result.is_some(),
            vector["expected"] == "valid",
            "{}",
            vector["name"].as_str().unwrap()
        );
    }
}