agent-toolprint 0.1.0

Double-signed receipts for AI-agent tool invocations — DSSE + JCS + Ed25519, verifiable offline (Rust port of @p-vbordei/agent-toolprint)
Documentation
//! agent-toolprint quickstart: produce a receipt, double-sign, verify, tamper.
//!
//! Run with:
//!     cargo run --example quickstart

use agent_toolprint::{
    countersign_tool, did_key_from_ed25519_pubkey, sha256_hash, sign_agent, verify, DidKeyResolver,
    Plaintext, VerifyOptions,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use serde_json::{json, Value};

#[tokio::main]
async fn main() {
    let agent_sk = SigningKey::generate(&mut OsRng);
    let tool_sk = SigningKey::generate(&mut OsRng);
    let agent_did = did_key_from_ed25519_pubkey(&agent_sk.verifying_key().to_bytes()).unwrap();
    let tool_did = did_key_from_ed25519_pubkey(&tool_sk.verifying_key().to_bytes()).unwrap();
    let args = json!({"query": "bun docs"});
    let response = json!({"results": ["https://bun.sh/docs"]});

    let receipt = json!({
        "v": "tp/0.1",
        "id": uuid::Uuid::new_v4().to_string(),
        "ts": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
        "agent": {"did": agent_did, "key_id": "agent"},
        "tool":  {"did": tool_did,  "key_id": "tool"},
        "call":   {"name": "search", "args_hash":     sha256_hash(&args).unwrap()},
        "result": {"status": "ok",   "response_hash": sha256_hash(&response).unwrap()},
        "nonce":  STANDARD.encode([0u8; 32]),
    });

    let env = countersign_tool(
        &sign_agent(&receipt, &agent_sk.to_bytes()).unwrap(),
        &tool_sk.to_bytes(),
    )
    .unwrap();
    println!(
        "envelope signatures : {}",
        env["signatures"].as_array().unwrap().len()
    );

    let resolver = DidKeyResolver;
    let mut opts = VerifyOptions::new(&resolver);
    opts.plaintext = Some(Plaintext {
        args: Some(args.clone()),
        response: Some(response.clone()),
    });
    let res = verify(&env, &opts).await;
    println!(
        "original            : {}",
        if res.ok {
            "PASS".to_string()
        } else {
            format!("FAIL ({})", res.error.clone().unwrap_or_default())
        }
    );
    assert!(res.ok);

    // Flip one byte in the agent signature → verify must fail.
    let mut tampered = env.clone();
    let sig0 = tampered["signatures"][0]["sig"]
        .as_str()
        .unwrap()
        .to_string();
    let first = if sig0.starts_with('A') { 'B' } else { 'A' };
    let mut flipped = String::new();
    flipped.push(first);
    flipped.push_str(&sig0[1..]);
    tampered["signatures"][0]["sig"] = Value::String(flipped);

    let res_bad = verify(&tampered, &VerifyOptions::new(&resolver)).await;
    println!(
        "tampered            : {}",
        if res_bad.ok {
            "PASS".to_string()
        } else {
            format!("FAIL ({})", res_bad.error.clone().unwrap_or_default())
        }
    );
    assert!(!res_bad.ok);
}