use std::{fs, path::PathBuf};
use clap::{Parser, Subcommand};
use ed25519_dalek::{SigningKey, VerifyingKey};
use rill_runtime::{
TrustStore,
handler_package::{HandlerPackError, build_signed_handler_pack, load_handler_pack},
package::{build_signed_model_pack, load_model_pack, sign_release_index, verify_release_index},
};
use rill_runtime_protocol::{
HandlerPackManifest, ModelPackManifest, ReleaseIndexPayload, SignedReleaseIndex,
};
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Parser)]
#[command(
name = "rill-pack",
version,
about = "Build and verify signed Rill model and handler packages"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Create {
#[arg(long)]
manifest: PathBuf,
#[arg(long)]
model: PathBuf,
#[arg(long)]
output: PathBuf,
},
Verify {
#[arg(long)]
pack: PathBuf,
#[arg(long)]
key_id: String,
#[arg(long)]
public_key_hex: String,
},
CreateHandler {
#[arg(long)]
manifest: PathBuf,
#[arg(long)]
module: PathBuf,
#[arg(long)]
output: PathBuf,
},
InspectHandler {
#[arg(long)]
handler: PathBuf,
#[arg(long)]
key_id: String,
#[arg(long)]
public_key_hex: String,
},
SignIndex {
#[arg(long)]
payload: PathBuf,
#[arg(long)]
output: PathBuf,
},
VerifyIndex {
#[arg(long)]
index: PathBuf,
#[arg(long)]
key_id: String,
#[arg(long)]
public_key_hex: String,
},
}
#[derive(Debug, Error)]
enum CliError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("model package error: {0}")]
Package(#[from] rill_runtime::package::ModelPackError),
#[error("handler package error: {0}")]
HandlerPackage(#[from] HandlerPackError),
#[error("release-index error: {0}")]
ReleaseIndex(#[from] rill_runtime::archive::ReleaseIndexError),
#[error("RILL_SIGNING_KEY_HEX must contain exactly one 32-byte hexadecimal signing seed")]
SigningKey,
#[error("public key must contain exactly 32 hexadecimal bytes")]
PublicKey,
}
fn main() {
if let Err(error) = run(Cli::parse()) {
eprintln!("rill-pack: {error}");
std::process::exit(1);
}
}
fn run(cli: Cli) -> Result<(), CliError> {
match cli.command {
Command::Create {
manifest,
model,
output,
} => {
let manifest: ModelPackManifest = serde_json::from_slice(&fs::read(manifest)?)?;
let model: serde_json::Value = serde_json::from_slice(&fs::read(model)?)?;
let signing_key = signing_key_from_environment()?;
let bytes = build_signed_model_pack(&manifest, &model, &signing_key)?;
write_output(&output, &bytes)?;
Ok(())
}
Command::Verify {
pack,
key_id,
public_key_hex,
} => {
let public_key = decode_32(&public_key_hex).ok_or(CliError::PublicKey)?;
let public_key =
VerifyingKey::from_bytes(&public_key).map_err(|_| CliError::PublicKey)?;
let trust = TrustStore([(key_id, public_key)].into_iter().collect());
let (_, inspection) = load_model_pack(fs::File::open(pack)?, &trust)?;
println!("{}", serde_json::to_string_pretty(&inspection)?);
Ok(())
}
Command::CreateHandler {
manifest,
module,
output,
} => {
let manifest: HandlerPackManifest = serde_json::from_slice(&fs::read(manifest)?)?;
let module = fs::read(&module)?;
let actual_digest = hex::encode(Sha256::digest(&module));
if actual_digest != manifest.module_sha256 {
eprintln!(
"rill-pack: manifest moduleSha256 does not match the actual module digest"
);
std::process::exit(1);
}
let signing_key = signing_key_from_environment()?;
let bytes = build_signed_handler_pack(&manifest, &module, &signing_key)?;
write_output(&output, &bytes)?;
Ok(())
}
Command::InspectHandler {
handler,
key_id,
public_key_hex,
} => {
let public_key = decode_32(&public_key_hex).ok_or(CliError::PublicKey)?;
let public_key =
VerifyingKey::from_bytes(&public_key).map_err(|_| CliError::PublicKey)?;
let trust = TrustStore([(key_id, public_key)].into_iter().collect());
let (_, inspection) = load_handler_pack(fs::File::open(handler)?, &trust)?;
println!("{}", serde_json::to_string_pretty(&inspection)?);
Ok(())
}
Command::SignIndex { payload, output } => {
let payload: ReleaseIndexPayload = serde_json::from_slice(&fs::read(payload)?)?;
let index = sign_release_index(payload, &signing_key_from_environment()?)?;
write_output(&output, &serde_json::to_vec_pretty(&index)?)?;
Ok(())
}
Command::VerifyIndex {
index,
key_id,
public_key_hex,
} => {
let public_key = decode_32(&public_key_hex).ok_or(CliError::PublicKey)?;
let public_key =
VerifyingKey::from_bytes(&public_key).map_err(|_| CliError::PublicKey)?;
let trust = TrustStore([(key_id, public_key)].into_iter().collect());
let index: SignedReleaseIndex = serde_json::from_slice(&fs::read(index)?)?;
verify_release_index(&index, &trust)?;
println!("{}", serde_json::to_string_pretty(&index.payload)?);
Ok(())
}
}
}
fn write_output(output: &PathBuf, bytes: &[u8]) -> Result<(), CliError> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)?;
}
fs::write(output, bytes)?;
Ok(())
}
fn signing_key_from_environment() -> Result<SigningKey, CliError> {
let encoded = std::env::var("RILL_SIGNING_KEY_HEX").map_err(|_| CliError::SigningKey)?;
let bytes = decode_32(&encoded).ok_or(CliError::SigningKey)?;
Ok(SigningKey::from_bytes(&bytes))
}
fn decode_32(encoded: &str) -> Option<[u8; 32]> {
hex::decode(encoded).ok()?.try_into().ok()
}