use anyhow::{Context, Result};
use assay_evidence::attestation::{sign_statement, statement_from_manifest};
use assay_evidence::bundle::BundleReader;
use clap::Args;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::SigningKey;
use std::fs::File;
use std::path::PathBuf;
#[derive(Debug, Args, Clone)]
pub struct AttestArgs {
#[arg(long)]
pub bundle: PathBuf,
#[arg(long)]
pub key: PathBuf,
#[arg(long)]
pub predicate: Option<PathBuf>,
#[arg(long)]
pub out: Option<PathBuf>,
}
pub fn cmd_attest(args: AttestArgs) -> Result<i32> {
run(args)?;
Ok(0)
}
fn run(args: AttestArgs) -> Result<()> {
let file = File::open(&args.bundle)
.with_context(|| format!("open bundle {}", args.bundle.display()))?;
let reader = BundleReader::open(file).context("verify/open bundle")?;
let manifest = reader.manifest().clone();
let pem = std::fs::read_to_string(&args.key)
.with_context(|| format!("read key {}", args.key.display()))?;
let key = SigningKey::from_pkcs8_pem(&pem).context("parse Ed25519 PKCS#8 PEM key")?;
let predicate = match &args.predicate {
Some(p) => {
let raw = std::fs::read_to_string(p)
.with_context(|| format!("read predicate {}", p.display()))?;
serde_json::from_str(&raw).context("parse predicate JSON")?
}
None => serde_json::json!({
"run_id": manifest.run_id,
"event_count": manifest.event_count,
}),
};
let statement = statement_from_manifest(&manifest, predicate);
let envelope = sign_statement(&statement, &key).context("sign in-toto statement")?;
let json = serde_json::to_string_pretty(&envelope).context("serialize DSSE envelope")?;
match &args.out {
Some(p) => {
std::fs::write(p, format!("{json}\n"))
.with_context(|| format!("write {}", p.display()))?;
eprintln!("Attestation: {}", p.display());
}
None => println!("{json}"),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use assay_evidence::attestation::{verify_envelope, DsseEnvelope};
use assay_evidence::bundle::BundleWriter;
use assay_evidence::types::{EvidenceEvent, ProducerMeta};
use ed25519_dalek::pkcs8::{spki::der::pem::LineEnding, EncodePrivateKey};
#[test]
fn attest_produces_a_verifiable_envelope() {
let dir = std::env::temp_dir().join(format!("assay-attest-cli-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bundle_path = dir.join("bundle.tar.gz");
let key_path = dir.join("private_key.pem");
let out_path = dir.join("attestation.json");
let producer = ProducerMeta {
name: "assay-cli".into(),
version: "test".into(),
git: None,
};
let file = File::create(&bundle_path).unwrap();
let mut writer = BundleWriter::new(file).with_producer(producer.clone());
writer.add_event(
EvidenceEvent::new(
"assay.test.event",
"urn:assay:test",
"attest_run",
0,
serde_json::json!({}),
)
.with_producer(&producer),
);
writer.finish().unwrap();
let signing = SigningKey::from_bytes(&[7u8; 32]);
std::fs::write(
&key_path,
signing.to_pkcs8_pem(LineEnding::LF).unwrap().as_bytes(),
)
.unwrap();
run(AttestArgs {
bundle: bundle_path,
key: key_path,
predicate: None,
out: Some(out_path.clone()),
})
.expect("attest");
let raw = std::fs::read_to_string(&out_path).unwrap();
let envelope: DsseEnvelope = serde_json::from_str(&raw).unwrap();
let statement = verify_envelope(&envelope, &signing.verifying_key()).expect("verify");
assert_eq!(statement.type_, "https://in-toto.io/Statement/v1");
std::fs::remove_dir_all(&dir).ok();
}
}