Skip to main content

treeship_core/attestation/
sign.rs

1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use serde::Serialize;
3
4use crate::attestation::{
5    pae,
6    artifact_id_from_pae, digest_from_pae, ArtifactId,
7    Signer,
8    Envelope, Signature,
9};
10
11/// The result of a successful `sign` call.
12#[derive(Debug)]
13pub struct SignResult {
14    /// The sealed DSSE envelope — ready to store or transmit.
15    pub envelope: Envelope,
16
17    /// The content-addressed artifact ID derived from the PAE bytes.
18    /// Stored alongside the envelope. Recomputed during verification —
19    /// if the content was tampered with, the recomputed ID will differ.
20    pub artifact_id: ArtifactId,
21
22    /// The full SHA-256 digest of the PAE bytes: "sha256:<hex>".
23    pub digest: String,
24}
25
26/// Signs a statement and returns a sealed DSSE envelope.
27///
28/// # Steps
29///
30/// 1. Serialize `statement` to compact JSON bytes.
31/// 2. Construct `PAE(payload_type, json_bytes)`.
32/// 3. Sign the PAE bytes with `signer` — **never** the raw JSON.
33/// 4. base64url-encode the payload and signature.
34/// 5. Derive the content-addressed artifact ID from the PAE bytes.
35///
36/// # Errors
37///
38/// Returns an error if serialization or signing fails.
39///
40/// # Examples
41///
42/// ```
43/// use serde::Serialize;
44/// use treeship_core::attestation::{sign, Ed25519Signer};
45///
46/// #[derive(Serialize)]
47/// struct Action { actor: String, action: String }
48///
49/// let signer = Ed25519Signer::generate("key_test").unwrap();
50/// let stmt   = Action { actor: "agent://test".into(), action: "tool.call".into() };
51/// let result = sign("application/vnd.treeship.action.v1+json", &stmt, &signer).unwrap();
52///
53/// assert!(result.artifact_id.starts_with("art_"));
54/// assert!(result.digest.starts_with("sha256:"));
55/// ```
56pub fn sign<T: Serialize>(
57    payload_type: &str,
58    statement:    &T,
59    signer:       &dyn Signer,
60) -> Result<SignResult, SignError> {
61    if payload_type.is_empty() {
62        return Err(SignError("payload_type must not be empty".into()));
63    }
64
65    // 1. Serialize to compact JSON — no indentation, deterministic field order
66    //    within a struct (Rust's serde_json serializes fields in declaration order).
67    let payload_bytes = serde_json::to_vec(statement)
68        .map_err(|e| SignError(format!("serialize statement: {}", e)))?;
69
70    // 2. Build the PAE byte string. This is what gets signed.
71    let pae_bytes = pae(payload_type, &payload_bytes);
72
73    // 3. Sign the PAE bytes.
74    let raw_sig = signer
75        .sign(&pae_bytes)
76        .map_err(|e| SignError(format!("sign: {}", e)))?;
77
78    // 4. Build the DSSE envelope.
79    let envelope = Envelope {
80        payload:      URL_SAFE_NO_PAD.encode(&payload_bytes),
81        payload_type: payload_type.to_string(),
82        signatures: vec![Signature {
83            keyid: signer.key_id().to_string(),
84            sig:   URL_SAFE_NO_PAD.encode(&raw_sig),
85        }],
86    };
87
88    // 5. Derive ID and digest from the PAE bytes — same bytes that were signed.
89    let artifact_id = artifact_id_from_pae(&pae_bytes);
90    let digest      = digest_from_pae(&pae_bytes);
91
92    Ok(SignResult { envelope, artifact_id, digest })
93}
94
95/// Error from the sign operation.
96#[derive(Debug)]
97pub struct SignError(pub String);
98
99impl std::fmt::Display for SignError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "attestation sign: {}", self.0)
102    }
103}
104
105impl std::error::Error for SignError {}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::attestation::{Ed25519Signer, Verifier};
111    use serde::{Deserialize, Serialize};
112
113    #[derive(Debug, PartialEq, Serialize, Deserialize)]
114    struct TestStmt {
115        #[serde(rename = "type")]
116        type_:  String,
117        actor:  String,
118        action: String,
119    }
120
121    fn make_signer() -> Ed25519Signer {
122        Ed25519Signer::generate("key_test_01").unwrap()
123    }
124
125    const TEST_PT: &str = "application/vnd.treeship.action.v1+json";
126
127    fn make_stmt() -> TestStmt {
128        TestStmt {
129            type_:  "treeship/action/v1".into(),
130            actor:  "agent://researcher".into(),
131            action: "tool.call".into(),
132        }
133    }
134
135    #[test]
136    fn sign_produces_envelope() {
137        let signer = make_signer();
138        let result = sign(TEST_PT, &make_stmt(), &signer).unwrap();
139        assert!(!result.envelope.payload.is_empty());
140        assert_eq!(result.envelope.payload_type, TEST_PT);
141        assert_eq!(result.envelope.signatures.len(), 1);
142        assert_eq!(result.envelope.signatures[0].keyid, "key_test_01");
143    }
144
145    #[test]
146    fn artifact_id_format() {
147        let signer = make_signer();
148        let r      = sign(TEST_PT, &make_stmt(), &signer).unwrap();
149        assert!(r.artifact_id.starts_with("art_"), "must start with art_: {}", r.artifact_id);
150        assert_eq!(r.artifact_id.len(), 36, "art_ + 32 hex: {}", r.artifact_id);
151    }
152
153    #[test]
154    fn digest_format() {
155        let signer = make_signer();
156        let r      = sign(TEST_PT, &make_stmt(), &signer).unwrap();
157        assert!(r.digest.starts_with("sha256:"), "must start with sha256:");
158        assert_eq!(r.digest.len(), 71, "sha256: + 64 hex: {}", r.digest);
159    }
160
161    #[test]
162    fn empty_payload_type_errors() {
163        let signer = make_signer();
164        assert!(sign("", &make_stmt(), &signer).is_err());
165    }
166
167    #[test]
168    fn id_matches_verify() {
169        // The ID derived during sign must equal the ID re-derived during verify.
170        let signer   = make_signer();
171        let verifier = Verifier::from_signer(&signer);
172        let signed   = sign(TEST_PT, &make_stmt(), &signer).unwrap();
173        let verified = verifier.verify(&signed.envelope).unwrap();
174        assert_eq!(signed.artifact_id, verified.artifact_id);
175    }
176
177    #[test]
178    fn digest_matches_verify() {
179        let signer   = make_signer();
180        let verifier = Verifier::from_signer(&signer);
181        let signed   = sign(TEST_PT, &make_stmt(), &signer).unwrap();
182        let verified = verifier.verify(&signed.envelope).unwrap();
183        assert_eq!(signed.digest, verified.digest);
184    }
185
186    #[test]
187    fn payload_roundtrip() {
188        let signer = make_signer();
189        let original = make_stmt();
190        let r      = sign(TEST_PT, &original, &signer).unwrap();
191        let decoded: TestStmt = r.envelope.unmarshal_statement().unwrap();
192        assert_eq!(decoded, original);
193    }
194
195    #[test]
196    fn id_deterministic() {
197        // Two calls with identical content must produce the same ID.
198        let signer = make_signer();
199        let r1     = sign(TEST_PT, &make_stmt(), &signer).unwrap();
200        let r2     = sign(TEST_PT, &make_stmt(), &signer).unwrap();
201        assert_eq!(r1.artifact_id, r2.artifact_id);
202    }
203
204    #[test]
205    fn id_differs_by_content() {
206        let signer = make_signer();
207        let s1     = TestStmt { type_: "treeship/action/v1".into(), actor: "a".into(), action: "x".into() };
208        let s2     = TestStmt { type_: "treeship/action/v1".into(), actor: "b".into(), action: "x".into() };
209        let r1     = sign(TEST_PT, &s1, &signer).unwrap();
210        let r2     = sign(TEST_PT, &s2, &signer).unwrap();
211        assert_ne!(r1.artifact_id, r2.artifact_id);
212    }
213
214    #[test]
215    fn id_differs_by_payload_type() {
216        let signer = make_signer();
217        let r1 = sign("application/vnd.treeship.action.v1+json",   &make_stmt(), &signer).unwrap();
218        let r2 = sign("application/vnd.treeship.approval.v1+json",  &make_stmt(), &signer).unwrap();
219        assert_ne!(r1.artifact_id, r2.artifact_id);
220    }
221
222    #[test]
223    fn json_serialization_roundtrip() {
224        let signer   = make_signer();
225        let verifier = Verifier::from_signer(&signer);
226        let signed   = sign(TEST_PT, &make_stmt(), &signer).unwrap();
227
228        let json     = signed.envelope.to_json().unwrap();
229        let restored = Envelope::from_json(&json).unwrap();
230
231        // The restored envelope must still verify correctly.
232        let result = verifier.verify(&restored).unwrap();
233        assert_eq!(result.artifact_id, signed.artifact_id);
234    }
235}