provenance/examples/sign_slsa.rs
//! Sign a SLSA Provenance v1 statement about one file as a DSSE envelope, the way
//! Holger's provenance scanner reads it (the 1.0 demo's online side).
//!
//! ```text
//! cargo run -p holger-provenance --example sign_slsa -- \
//! <ed25519-pkcs8.der> <keyid> <builder-id> <artifact> > <artifact>.intoto.jsonl
//! cargo run -p holger-provenance --example sign_slsa -- --new-key <ed25519-pkcs8.der>
//! ```
//!
//! `--new-key` writes a fresh Ed25519 PKCS#8 key and prints the RON `keys:` entry
//! for its public half. The private key never leaves the file it was written to.
use ring::signature::{Ed25519KeyPair, KeyPair as _};
use sha2::{Digest, Sha256};
fn b64(b: &[u8]) -> String {
holger_provenance::crypto::b64_encode(b)
}
fn ron_entry(keyid: &str, pair: &Ed25519KeyPair) -> String {
let mut spki = hex::decode("302a300506032b6570032100").unwrap();
spki.extend_from_slice(pair.public_key().as_ref());
format!(
"(id: \"{keyid}\", alg: ed25519, public_key: \"{}\")",
b64(&spki)
)
}
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.first().map(String::as_str) == Some("--new-key") {
let path = args
.get(1)
.ok_or_else(|| anyhow::anyhow!("--new-key <path>"))?;
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&ring::rand::SystemRandom::new())
.map_err(|e| anyhow::anyhow!("{e}"))?;
std::fs::write(path, pkcs8.as_ref())?;
let pair =
Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).map_err(|e| anyhow::anyhow!("{e}"))?;
println!(
"{}",
ron_entry(
args.get(2).map(String::as_str).unwrap_or("demo-release"),
&pair
)
);
return Ok(());
}
let [key, keyid, builder, artifact] = args.as_slice() else {
anyhow::bail!("usage: sign_slsa <ed25519-pkcs8.der> <keyid> <builder-id> <artifact>");
};
let pair = Ed25519KeyPair::from_pkcs8(&std::fs::read(key)?)
.map_err(|e| anyhow::anyhow!("the key: {e}"))?;
let bytes = std::fs::read(artifact)?;
let name = std::path::Path::new(artifact)
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let statement = serde_json::to_vec(&serde_json::json!({
"_type": "https://in-toto.io/Statement/v1",
"subject": [{"name": name, "digest": {"sha256": hex::encode(Sha256::digest(&bytes))}}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://holger.rs/demo-build/v1",
"externalParameters": {"artifact": name},
"resolvedDependencies": []
},
"runDetails": {"builder": {"id": builder}, "metadata": {"invocationId": format!("sign_slsa {}", chrono::Utc::now().to_rfc3339())}}
}
}))?;
let sig = pair.sign(&znippy_common::sign::dsse_pae(
holger_provenance::statement::PAYLOAD_TYPE_INTOTO,
&statement,
));
println!(
"{}",
serde_json::to_string(&serde_json::json!({
"payloadType": holger_provenance::statement::PAYLOAD_TYPE_INTOTO,
"payload": b64(&statement),
"signatures": [{"keyid": keyid, "sig": b64(sig.as_ref())}],
}))?
);
eprintln!("signed {name} for {}", ron_entry(keyid, &pair));
Ok(())
}