1use 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}