Skip to main content

agent_toolprint/
sign.rs

1//! Agent and tool signing operations.
2
3use base64::{engine::general_purpose::STANDARD, Engine};
4use ed25519_dalek::{Signer, SigningKey};
5use serde_json::{json, Value};
6
7use crate::canonical::canonical;
8use crate::envelope::{new_envelope, pae_encode};
9use crate::error::Error;
10use crate::types::{validate_receipt, PAYLOAD_TYPE};
11
12pub fn ed25519_sign(sk: &[u8; 32], message: &[u8]) -> Vec<u8> {
13    SigningKey::from_bytes(sk).sign(message).to_bytes().to_vec()
14}
15
16pub fn sign_agent(receipt: &Value, sk: &[u8; 32]) -> Result<Value, Error> {
17    validate_receipt(receipt)?;
18    let body = canonical(receipt)?;
19    let pae = pae_encode(PAYLOAD_TYPE, &body);
20    let sig = ed25519_sign(sk, &pae);
21    let mut env = new_envelope(&body);
22    let agent_keyid = receipt["agent"]["key_id"]
23        .as_str()
24        .ok_or_else(|| Error::Invalid("receipt.agent.key_id missing".into()))?
25        .to_string();
26    env["signatures"] = json!([{
27        "keyid": agent_keyid,
28        "sig": STANDARD.encode(&sig),
29    }]);
30    Ok(env)
31}
32
33pub fn countersign_tool(envelope: &Value, sk: &[u8; 32]) -> Result<Value, Error> {
34    let sigs = envelope["signatures"]
35        .as_array()
36        .ok_or_else(|| Error::Invalid("envelope.signatures missing".into()))?;
37    if sigs.len() != 1 {
38        return Err(Error::Sign(format!(
39            "countersign_tool expects exactly 1 existing signature, got {}",
40            sigs.len()
41        )));
42    }
43    let payload_b64 = envelope["payload"]
44        .as_str()
45        .ok_or_else(|| Error::Invalid("envelope.payload missing".into()))?;
46    let payload_bytes = STANDARD
47        .decode(payload_b64)
48        .map_err(|e| Error::Invalid(format!("envelope.payload base64: {e}")))?;
49    let receipt: Value = serde_json::from_slice(&payload_bytes)?;
50    validate_receipt(&receipt)?;
51    let canonical_bytes = canonical(&receipt)?;
52    if payload_bytes != canonical_bytes {
53        return Err(Error::Sign("envelope payload is not JCS-canonical".into()));
54    }
55    let pae = pae_encode(PAYLOAD_TYPE, &canonical_bytes);
56    let sig = ed25519_sign(sk, &pae);
57    let tool_keyid = receipt["tool"]["key_id"]
58        .as_str()
59        .ok_or_else(|| Error::Invalid("receipt.tool.key_id missing".into()))?
60        .to_string();
61    let mut out = envelope.clone();
62    let mut sigs_out = sigs.clone();
63    sigs_out.push(json!({
64        "keyid": tool_keyid,
65        "sig": STANDARD.encode(&sig),
66    }));
67    out["signatures"] = Value::Array(sigs_out);
68    Ok(out)
69}