use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const PAYLOAD_TYPE: &str = "application/vnd.in-toto+json";
#[must_use]
pub fn pae(payload_type: &str, body: &[u8]) -> Vec<u8> {
let mut out = format!(
"DSSEv1 {} {payload_type} {} ",
payload_type.len(),
body.len()
)
.into_bytes();
out.extend_from_slice(body);
out
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature {
pub keyid: String,
pub sig: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Envelope {
#[serde(rename = "payloadType")]
pub payload_type: String,
pub payload: String,
pub signatures: Vec<Signature>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sigstore: Option<Value>,
}
impl Envelope {
#[must_use]
pub fn unsigned(statement: &[u8]) -> Self {
Self {
payload_type: PAYLOAD_TYPE.to_owned(),
payload: STANDARD.encode(statement),
signatures: Vec::new(),
sigstore: None,
}
}
#[must_use]
pub fn body(&self) -> Option<Vec<u8>> {
STANDARD.decode(&self.payload).ok()
}
#[must_use]
pub fn signed_bytes(&self) -> Option<Vec<u8>> {
self.body().map(|b| pae(&self.payload_type, &b))
}
#[must_use]
pub fn statement(&self) -> Option<Value> {
serde_json::from_slice(&self.body()?).ok()
}
}
#[must_use]
pub fn encode(lines: &[Envelope]) -> String {
lines
.iter()
.filter_map(|e| serde_json::to_string(e).ok())
.map(|l| l + "\n")
.collect()
}
#[must_use]
pub fn decode(text: &str) -> (Vec<Envelope>, usize) {
let mut lines = Vec::new();
let mut unreadable = 0;
for line in text.lines().map(str::trim).filter(|l| !l.is_empty()) {
match serde_json::from_str::<Envelope>(line) {
Ok(e) => lines.push(e),
Err(_) => unreadable += 1,
}
}
(lines, unreadable)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pae_matches_the_dsse_specification() {
assert_eq!(
pae("http://example.com/HelloWorld", b"hello world"),
b"DSSEv1 29 http://example.com/HelloWorld 11 hello world"
);
assert_eq!(pae("", b""), b"DSSEv1 0 0 ");
}
#[test]
fn a_bundle_round_trips_and_counts_what_it_cannot_read() {
let e = Envelope::unsigned(br#"{"a":1}"#);
let text = format!("{}not json\n\n", encode(&[e.clone(), e.clone()]));
let (lines, unreadable) = decode(&text);
assert_eq!((lines.len(), unreadable), (2, 1));
assert_eq!(lines[0].statement(), Some(serde_json::json!({ "a": 1 })));
assert_eq!(
lines[0].signed_bytes().unwrap(),
b"DSSEv1 28 application/vnd.in-toto+json 7 {\"a\":1}"
);
}
}