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 and tool signing operations.

use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::{Signer, SigningKey};
use serde_json::{json, Value};

use crate::canonical::canonical;
use crate::envelope::{new_envelope, pae_encode};
use crate::error::Error;
use crate::types::{validate_receipt, PAYLOAD_TYPE};

pub fn ed25519_sign(sk: &[u8; 32], message: &[u8]) -> Vec<u8> {
    SigningKey::from_bytes(sk).sign(message).to_bytes().to_vec()
}

pub fn sign_agent(receipt: &Value, sk: &[u8; 32]) -> Result<Value, Error> {
    validate_receipt(receipt)?;
    let body = canonical(receipt)?;
    let pae = pae_encode(PAYLOAD_TYPE, &body);
    let sig = ed25519_sign(sk, &pae);
    let mut env = new_envelope(&body);
    let agent_keyid = receipt["agent"]["key_id"]
        .as_str()
        .ok_or_else(|| Error::Invalid("receipt.agent.key_id missing".into()))?
        .to_string();
    env["signatures"] = json!([{
        "keyid": agent_keyid,
        "sig": STANDARD.encode(&sig),
    }]);
    Ok(env)
}

pub fn countersign_tool(envelope: &Value, sk: &[u8; 32]) -> Result<Value, Error> {
    let sigs = envelope["signatures"]
        .as_array()
        .ok_or_else(|| Error::Invalid("envelope.signatures missing".into()))?;
    if sigs.len() != 1 {
        return Err(Error::Sign(format!(
            "countersign_tool expects exactly 1 existing signature, got {}",
            sigs.len()
        )));
    }
    let payload_b64 = envelope["payload"]
        .as_str()
        .ok_or_else(|| Error::Invalid("envelope.payload missing".into()))?;
    let payload_bytes = STANDARD
        .decode(payload_b64)
        .map_err(|e| Error::Invalid(format!("envelope.payload base64: {e}")))?;
    let receipt: Value = serde_json::from_slice(&payload_bytes)?;
    validate_receipt(&receipt)?;
    let canonical_bytes = canonical(&receipt)?;
    if payload_bytes != canonical_bytes {
        return Err(Error::Sign("envelope payload is not JCS-canonical".into()));
    }
    let pae = pae_encode(PAYLOAD_TYPE, &canonical_bytes);
    let sig = ed25519_sign(sk, &pae);
    let tool_keyid = receipt["tool"]["key_id"]
        .as_str()
        .ok_or_else(|| Error::Invalid("receipt.tool.key_id missing".into()))?
        .to_string();
    let mut out = envelope.clone();
    let mut sigs_out = sigs.clone();
    sigs_out.push(json!({
        "keyid": tool_keyid,
        "sig": STANDARD.encode(&sig),
    }));
    out["signatures"] = Value::Array(sigs_out);
    Ok(out)
}