use serde_json::{Value, json};
use super::artifact::PackageArtifact;
use super::{Error, Reporter};
use crate::signatures::{Kind, Outcome, SignatureDir, Subject};
pub fn signature_subjects(artifact: &PackageArtifact) -> Result<Vec<Subject>, Error> {
let file_name = |path: &str| {
std::path::Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
};
let carried = |entry: &Value| {
entry["signature"]
.as_str()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
};
let mut subjects = Vec::new();
for entry in &artifact.plugins {
let content = super::artifact::plugin_import_content(entry)?;
let manifest: crate::plugin::Manifest =
serde_json::from_value(content["manifest"].clone())?;
subjects.push(Subject {
kind: Kind::Plugin,
file_names: manifest
.component
.as_deref()
.and_then(file_name)
.into_iter()
.collect(),
id: manifest.name,
digest: content["digest"].as_str().unwrap_or_default().to_string(),
carried: carried(entry),
});
}
for entry in &artifact.models {
subjects.push(Subject {
kind: Kind::Model,
id: entry["model_id"].as_str().unwrap_or_default().to_string(),
file_names: entry["artifact"]["key"]
.as_str()
.and_then(file_name)
.into_iter()
.collect(),
digest: entry["artifact"]["digest"]
.as_str()
.unwrap_or_default()
.to_string(),
carried: carried(entry),
});
}
Ok(subjects)
}
pub fn attach_signatures(
artifact: &mut PackageArtifact,
dir: &std::path::Path,
more_names: &dyn Fn(&Subject) -> Vec<String>,
report: &dyn Reporter,
) -> Result<Vec<(Subject, Outcome)>, Error> {
let sigs = SignatureDir::open(dir)?;
let mut subjects = signature_subjects(artifact)?;
for subject in &mut subjects {
for name in more_names(subject) {
if !subject.file_names.contains(&name) {
subject.file_names.push(name);
}
}
}
let outcomes = sigs.resolve(&subjects).map_err(|errors| {
for error in &errors {
report.err(&format!("error: {error}"));
}
format!(
"{} problem(s) with the signatures in {} — nothing was sent",
errors.len(),
dir.display()
)
})?;
let entries = artifact
.plugins
.iter_mut()
.chain(artifact.models.iter_mut());
for (entry, outcome) in entries.zip(&outcomes) {
if let Outcome::Signed { signature, .. } = outcome
&& let Some(obj) = entry.as_object_mut()
{
obj.insert("signature".to_string(), json!(signature));
}
}
Ok(subjects.into_iter().zip(outcomes).collect())
}
pub fn print_signature_report(
signed: &[(Subject, Outcome)],
dir: &std::path::Path,
report: &dyn Reporter,
) {
let width = signed.iter().map(|(s, _)| s.id.len()).max().unwrap_or(0);
for (subject, outcome) in signed {
match outcome {
Outcome::Signed {
file,
replaced_carried,
..
} => report.out(&format!(
"signed {:width$} <- {}{}",
subject.id,
file.display(),
if *replaced_carried {
" (replaces the signature the artifact carried)"
} else {
""
}
)),
Outcome::Carried => report.out(&format!(
"carried {:width$} (signature from the artifact; none in {})",
subject.id,
dir.display()
)),
Outcome::Unsigned { looked_for } => report.out(&format!(
"unsigned {:width$} (no {})",
subject.id,
looked_for.join(" or ")
)),
}
}
}
pub fn attach_from_dir(
artifact: &mut PackageArtifact,
signatures: Option<&str>,
report: &dyn Reporter,
) -> Result<Vec<(Subject, Outcome)>, Error> {
let Some(dir) = signatures else {
return Ok(Vec::new());
};
let dir = std::path::Path::new(dir);
let signed = attach_signatures(artifact, dir, &|_| Vec::new(), report)?;
print_signature_report(&signed, dir, report);
Ok(signed)
}
#[cfg(test)]
mod tests {
use super::super::artifact::artifact_content_hash;
use super::super::artifact::test_support::*;
use super::*;
#[test]
fn signature_subjects_read_either_manifest_form_and_attaching_keeps_the_hash() {
use base64::Engine as _;
let toml = include_str!("../../tests/fixtures/plugins/fixture-upload.toml");
let component = include_bytes!("../../tests/fixtures/plugins/fixture.wasm");
let digest = crate::crypto::sha256_digest(component);
let object = serde_json::to_value(crate::plugin::Manifest::parse(toml).expect("manifest"))
.expect("object");
let mut artifact = artifact(vec![model_entry()]);
artifact.plugins = vec![
json!({"plugin_id": "test.fixture", "manifest": toml,
"component": base64::engine::general_purpose::STANDARD.encode(component)}),
json!({"plugin_id": "test.fixture", "manifest": object, "digest": digest}),
];
let subjects = signature_subjects(&artifact).expect("subjects");
assert_eq!(subjects.len(), 3);
for plugin in &subjects[..2] {
assert_eq!(plugin.id, "test.fixture");
assert_eq!(plugin.file_names, ["fixture.wasm"]);
assert_eq!(plugin.digest, digest);
}
assert_eq!(subjects[2].id, "ada.c4-tiny");
assert_eq!(subjects[2].file_names, ["0.1.0.onnx"]);
let dir = std::env::temp_dir().join(format!("orion-attach-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("dir");
let key = crate::crypto::ed25519::SigningKey::generate();
std::fs::write(dir.join("test.fixture.sig"), key.sign(&digest)).expect("sig");
std::fs::write(dir.join("ada.c4-tiny.sig"), key.sign("sha256:abc")).expect("sig");
artifact.plugins.truncate(1);
let hash = artifact_content_hash(&artifact).expect("hash");
let report = attach_signatures(
&mut artifact,
&dir,
&|_| Vec::new(),
&crate::package::Console,
)
.expect("attach");
let _ = std::fs::remove_dir_all(&dir);
assert!(
report
.iter()
.all(|(_, o)| matches!(o, Outcome::Signed { .. }))
);
assert_eq!(artifact.plugins[0]["signature"], key.sign(&digest));
assert_eq!(artifact.models[0]["signature"], key.sign("sha256:abc"));
assert_eq!(artifact_content_hash(&artifact).expect("hash"), hash);
}
}