speccoder_runtime/attest/
dsse.rs1use base64::Engine as _;
8use base64::engine::general_purpose::STANDARD;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12pub const PAYLOAD_TYPE: &str = "application/vnd.in-toto+json";
14
15#[must_use]
18pub fn pae(payload_type: &str, body: &[u8]) -> Vec<u8> {
19 let mut out = format!(
20 "DSSEv1 {} {payload_type} {} ",
21 payload_type.len(),
22 body.len()
23 )
24 .into_bytes();
25 out.extend_from_slice(body);
26 out
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct Signature {
32 pub keyid: String,
34 pub sig: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Envelope {
41 #[serde(rename = "payloadType")]
43 pub payload_type: String,
44 pub payload: String,
46 pub signatures: Vec<Signature>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub sigstore: Option<Value>,
52}
53
54impl Envelope {
55 #[must_use]
57 pub fn unsigned(statement: &[u8]) -> Self {
58 Self {
59 payload_type: PAYLOAD_TYPE.to_owned(),
60 payload: STANDARD.encode(statement),
61 signatures: Vec::new(),
62 sigstore: None,
63 }
64 }
65
66 #[must_use]
68 pub fn body(&self) -> Option<Vec<u8>> {
69 STANDARD.decode(&self.payload).ok()
70 }
71
72 #[must_use]
74 pub fn signed_bytes(&self) -> Option<Vec<u8>> {
75 self.body().map(|b| pae(&self.payload_type, &b))
76 }
77
78 #[must_use]
80 pub fn statement(&self) -> Option<Value> {
81 serde_json::from_slice(&self.body()?).ok()
82 }
83}
84
85#[must_use]
87pub fn encode(lines: &[Envelope]) -> String {
88 lines
89 .iter()
90 .filter_map(|e| serde_json::to_string(e).ok())
91 .map(|l| l + "\n")
92 .collect()
93}
94
95#[must_use]
99pub fn decode(text: &str) -> (Vec<Envelope>, usize) {
100 let mut lines = Vec::new();
101 let mut unreadable = 0;
102 for line in text.lines().map(str::trim).filter(|l| !l.is_empty()) {
103 match serde_json::from_str::<Envelope>(line) {
104 Ok(e) => lines.push(e),
105 Err(_) => unreadable += 1,
106 }
107 }
108 (lines, unreadable)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn pae_matches_the_dsse_specification() {
117 assert_eq!(
119 pae("http://example.com/HelloWorld", b"hello world"),
120 b"DSSEv1 29 http://example.com/HelloWorld 11 hello world"
121 );
122 assert_eq!(pae("", b""), b"DSSEv1 0 0 ");
123 }
124
125 #[test]
126 fn a_bundle_round_trips_and_counts_what_it_cannot_read() {
127 let e = Envelope::unsigned(br#"{"a":1}"#);
128 let text = format!("{}not json\n\n", encode(&[e.clone(), e.clone()]));
129 let (lines, unreadable) = decode(&text);
130 assert_eq!((lines.len(), unreadable), (2, 1));
131 assert_eq!(lines[0].statement(), Some(serde_json::json!({ "a": 1 })));
132 assert_eq!(
133 lines[0].signed_bytes().unwrap(),
134 b"DSSEv1 28 application/vnd.in-toto+json 7 {\"a\":1}"
135 );
136 }
137}