use aion_context::compliance::{generate_compliance_report, ComplianceFramework, ReportFormat};
use aion_context::export::{export_file, ExportFormat};
use aion_context::keystore::KeyStore;
use aion_context::operations::{
commit_version, commit_version_force_unregistered, init_file, show_current_rules,
show_file_info, show_signatures, show_version_history, verify_file, CommitOptions, InitOptions,
};
use aion_context::types::AuthorId;
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Init(InitArgs),
Commit(CommitArgs),
Verify(VerifyArgs),
Show(ShowArgs),
Key(KeyArgs),
Report(ReportArgs),
Export(ExportArgs),
Registry(RegistryArgs),
Release(ReleaseArgs),
Archive(ArchiveArgs),
}
#[derive(Args, Debug)]
struct InitArgs {
#[arg(value_name = "FILE")]
path: PathBuf,
#[arg(short, long, value_name = "RULES_FILE")]
rules: Option<PathBuf>,
#[arg(short, long, value_name = "AUTHOR_ID")]
author: u64,
#[arg(short, long, value_name = "KEY_ID")]
key: String,
#[arg(short, long, value_name = "MESSAGE", default_value = "Genesis version")]
message: String,
#[arg(long)]
force: bool,
#[arg(long)]
no_encryption: bool,
}
#[derive(Args, Debug)]
struct CommitArgs {
#[arg(value_name = "FILE")]
path: PathBuf,
#[arg(short, long, value_name = "RULES_FILE")]
rules: Option<PathBuf>,
#[arg(short, long, value_name = "AUTHOR_ID")]
author: u64,
#[arg(short, long, value_name = "KEY_ID")]
key: String,
#[arg(short, long, value_name = "MESSAGE")]
message: String,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
#[arg(long)]
force_unregistered: bool,
}
#[derive(Args, Debug)]
struct VerifyArgs {
#[arg(value_name = "FILE")]
path: PathBuf,
#[arg(short, long, value_name = "FORMAT", default_value = "text")]
format: OutputFormat,
#[arg(short, long)]
verbose: bool,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
}
#[derive(Args, Debug)]
struct ShowArgs {
#[arg(value_name = "FILE")]
path: PathBuf,
#[command(subcommand)]
subcommand: ShowSubcommand,
#[arg(short, long, value_name = "FORMAT", default_value = "text")]
format: OutputFormat,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
}
#[derive(Subcommand, Debug)]
enum ShowSubcommand {
Rules,
History,
Signatures,
Info,
}
#[derive(Args, Debug)]
struct KeyArgs {
#[command(subcommand)]
subcommand: KeySubcommand,
}
#[derive(Subcommand, Debug)]
enum KeySubcommand {
Generate {
#[arg(short, long, value_name = "KEY_ID")]
id: String,
#[arg(short, long, value_name = "DESCRIPTION")]
description: Option<String>,
},
List,
Export {
#[arg(value_name = "KEY_ID")]
id: String,
#[arg(short, long, value_name = "FILE")]
output: PathBuf,
},
Import {
#[arg(value_name = "FILE")]
path: PathBuf,
#[arg(short, long, value_name = "KEY_ID")]
id: String,
},
Delete {
#[arg(value_name = "KEY_ID")]
id: String,
#[arg(short, long)]
force: bool,
},
}
#[derive(Args, Debug)]
struct ReportArgs {
#[arg(value_name = "FILE")]
path: PathBuf,
#[arg(short, long, value_enum, default_value = "generic")]
framework: FrameworkType,
#[arg(short = 'F', long, value_enum, default_value = "markdown")]
format: ReportFormatType,
#[arg(short, long, value_name = "OUTPUT")]
output: Option<PathBuf>,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum FrameworkType {
Sox,
Hipaa,
Gdpr,
Generic,
}
impl From<FrameworkType> for ComplianceFramework {
fn from(ft: FrameworkType) -> Self {
match ft {
FrameworkType::Sox => Self::Sox,
FrameworkType::Hipaa => Self::Hipaa,
FrameworkType::Gdpr => Self::Gdpr,
FrameworkType::Generic => Self::Generic,
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum ReportFormatType {
Text,
Markdown,
Json,
}
impl From<ReportFormatType> for ReportFormat {
fn from(rft: ReportFormatType) -> Self {
match rft {
ReportFormatType::Text => Self::Text,
ReportFormatType::Markdown => Self::Markdown,
ReportFormatType::Json => Self::Json,
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum OutputFormat {
Text,
Json,
Yaml,
}
#[derive(Args, Debug)]
struct ExportArgs {
#[arg(value_name = "FILE")]
path: PathBuf,
#[arg(short, long, value_enum, default_value = "json")]
format: ExportFormatType,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
#[arg(short, long, value_name = "OUTPUT")]
output: Option<PathBuf>,
}
#[derive(Args, Debug)]
struct RegistryArgs {
#[command(subcommand)]
subcommand: RegistrySubcommand,
}
#[derive(Subcommand, Debug)]
enum RegistrySubcommand {
Pin {
#[arg(long, value_name = "AUTHOR_ID")]
author: u64,
#[arg(long, value_name = "KEY_ID")]
key: String,
#[arg(long, value_name = "MASTER_KEY_ID")]
master: Option<String>,
#[arg(short, long, value_name = "OUTPUT")]
output: PathBuf,
},
Rotate {
#[arg(long, value_name = "AUTHOR_ID")]
author: u64,
#[arg(long, value_name = "N")]
from_epoch: u32,
#[arg(long, value_name = "N_PLUS_1")]
to_epoch: u32,
#[arg(long, value_name = "KEY_ID")]
new_key: String,
#[arg(long, value_name = "MASTER_KEY_ID")]
master_key: String,
#[arg(long, value_name = "V")]
effective_from_version: u64,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
#[arg(long)]
no_warn: bool,
},
Revoke {
#[arg(long, value_name = "AUTHOR_ID")]
author: u64,
#[arg(long, value_name = "N")]
epoch: u32,
#[arg(long, value_enum)]
reason: CliRevocationReason,
#[arg(long, value_name = "MASTER_KEY_ID")]
master_key: String,
#[arg(long, value_name = "V")]
effective_from_version: u64,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
},
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum CliRevocationReason {
Compromised,
Superseded,
Retired,
Unspecified,
}
impl From<CliRevocationReason> for aion_context::key_registry::RevocationReason {
fn from(r: CliRevocationReason) -> Self {
match r {
CliRevocationReason::Compromised => Self::Compromised,
CliRevocationReason::Superseded => Self::Superseded,
CliRevocationReason::Retired => Self::Retired,
CliRevocationReason::Unspecified => Self::Unspecified,
}
}
}
#[derive(Args, Debug)]
struct ReleaseArgs {
#[command(subcommand)]
subcommand: ReleaseSubcommand,
}
#[derive(Subcommand, Debug)]
#[allow(clippy::large_enum_variant)] enum ReleaseSubcommand {
Seal {
#[arg(long, value_name = "PATH")]
primary: PathBuf,
#[arg(long, value_name = "NAME")]
primary_name: String,
#[arg(long, value_name = "NAME")]
model_name: String,
#[arg(long, value_name = "VERSION")]
model_version: String,
#[arg(long, value_name = "FORMAT", default_value = "safetensors")]
model_format: String,
#[arg(long, value_name = "NAME:VERSION")]
framework: Vec<String>,
#[arg(long, value_name = "SPDX:SCOPE")]
license: Vec<String>,
#[arg(long, value_name = "NAME:RESULT")]
safety_attestation: Vec<String>,
#[arg(long, value_name = "REGIME:CLASS")]
export_control: Vec<String>,
#[arg(long, value_name = "URI")]
builder_id: String,
#[arg(long, value_name = "N", default_value_t = 1u64)]
aion_version: u64,
#[arg(long, value_name = "AUTHOR_ID")]
author: u64,
#[arg(long, value_name = "KEY_ID")]
key: String,
#[arg(long, value_name = "DIR")]
out_dir: PathBuf,
},
Verify {
#[arg(long, value_name = "DIR")]
bundle: PathBuf,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
#[arg(long, value_name = "N", default_value_t = 1u64)]
at_version: u64,
},
Inspect {
#[arg(long, value_name = "DIR")]
bundle: PathBuf,
#[arg(short, long, value_enum, default_value = "text")]
format: OutputFormat,
},
}
#[derive(Args, Debug)]
struct ArchiveArgs {
#[command(subcommand)]
subcommand: ArchiveSubcommand,
}
#[derive(Subcommand, Debug)]
enum ArchiveSubcommand {
Verify {
#[arg(value_name = "DIR")]
dir: PathBuf,
#[arg(long, value_name = "REGISTRY_FILE")]
registry: PathBuf,
#[arg(short, long, value_enum, default_value = "text")]
format: OutputFormat,
},
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum ExportFormatType {
Json,
Yaml,
Csv,
}
impl From<ExportFormatType> for ExportFormat {
fn from(eft: ExportFormatType) -> Self {
match eft {
ExportFormatType::Json => Self::Json,
ExportFormatType::Yaml => Self::Yaml,
ExportFormatType::Csv => Self::Csv,
}
}
}
fn init_tracing() {
let env_filter = tracing_subscriber::EnvFilter::try_from_env("AION_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
let format = std::env::var("AION_LOG_FORMAT").unwrap_or_default();
let builder = tracing_subscriber::fmt().with_env_filter(env_filter);
if format.eq_ignore_ascii_case("json") {
let _ = builder.json().with_writer(std::io::stderr).try_init();
} else {
let _ = builder.with_writer(std::io::stderr).try_init();
}
}
fn main() -> Result<ExitCode> {
init_tracing();
let cli = Cli::parse();
match cli.command {
Commands::Init(args) => cmd_init(&args),
Commands::Commit(args) => cmd_commit(&args),
Commands::Verify(args) => cmd_verify(&args),
Commands::Show(args) => cmd_show(&args),
Commands::Key(args) => cmd_key(&args),
Commands::Report(args) => cmd_report(&args),
Commands::Export(args) => cmd_export(&args),
Commands::Registry(args) => cmd_registry(&args),
Commands::Release(args) => cmd_release(&args),
Commands::Archive(args) => cmd_archive(&args),
}
}
fn cmd_init(args: &InitArgs) -> Result<ExitCode> {
print_init_banner(args);
if args.path.exists() && !args.force {
anyhow::bail!(
"File already exists: {}\nUse --force to overwrite",
args.path.display()
);
}
let rules = load_rules_content(args.rules.as_ref())?;
println!(" Rules size: {} bytes", rules.len());
let signing_key = load_signing_key_for_init(args)?;
let options = InitOptions {
author_id: AuthorId::new(args.author),
signing_key: &signing_key,
message: &args.message,
timestamp: None,
};
if args.force && args.path.exists() {
std::fs::remove_file(&args.path)
.with_context(|| format!("Failed to remove existing file: {}", args.path.display()))?;
}
let result = init_file(&args.path, &rules, &options)
.with_context(|| format!("Failed to create AION file: {}", args.path.display()))?;
println!("\n✅ File created successfully!");
println!(" File ID: 0x{:016x}", result.file_id.as_u64());
println!(" Version: {}", result.version.as_u64());
println!(" Path: {}", args.path.display());
Ok(ExitCode::SUCCESS)
}
fn print_init_banner(args: &InitArgs) {
println!("🚀 Initializing AION file: {}", args.path.display());
println!(" Author: {}", args.author);
println!(" Message: {}", args.message);
println!(
" Encryption: {}",
if args.no_encryption {
"disabled"
} else {
"enabled"
}
);
}
fn load_signing_key_for_init(args: &InitArgs) -> Result<aion_context::crypto::SigningKey> {
let keystore = KeyStore::new();
let key_author_id = parse_key_id(&args.key)?;
keystore.load_signing_key(key_author_id).with_context(|| {
format!(
"Failed to load key '{}' from keystore.\n\
Hint: Generate a key first with: aion key generate --id {}",
args.key, args.key
)
})
}
fn cmd_commit(args: &CommitArgs) -> Result<ExitCode> {
println!("📝 Committing new version to: {}", args.path.display());
println!(" Author: {}", args.author);
println!(" Message: {}", args.message);
if !args.path.exists() {
anyhow::bail!("File not found: {}", args.path.display());
}
let rules = load_rules_content(args.rules.as_ref())?;
println!(" New rules size: {} bytes", rules.len());
let keystore = KeyStore::new();
let key_author_id = parse_key_id(&args.key)?;
let signing_key = keystore.load_signing_key(key_author_id).with_context(|| {
format!(
"Failed to load key '{}' from keystore.\n\
Hint: Generate a key first with: aion key generate --id {}",
args.key, args.key
)
})?;
let options = CommitOptions {
author_id: AuthorId::new(args.author),
signing_key: &signing_key,
message: &args.message,
timestamp: None, };
let registry = load_registry_from_path(&args.registry)?;
let result = if args.force_unregistered {
eprintln!(
"⚠️ --force-unregistered: skipping registry authz pre-check. \
The resulting file will NOT pass `aion verify --registry` until \
the registry is updated to pin this signer (issue #25)."
);
commit_version_force_unregistered(&args.path, &rules, &options, ®istry)
.with_context(|| format!("Failed to commit new version to: {}", args.path.display()))?
} else {
commit_version(&args.path, &rules, &options, ®istry)
.with_context(|| format!("Failed to commit new version to: {}", args.path.display()))?
};
println!("\n✅ Version committed successfully!");
println!(" New version: {}", result.version.as_u64());
println!(" Rules hash: {}", hex::encode(result.rules_hash));
println!(" Path: {}", args.path.display());
Ok(ExitCode::SUCCESS)
}
fn cmd_verify(args: &VerifyArgs) -> Result<ExitCode> {
println!("🔍 Verifying AION file: {}", args.path.display());
if !args.path.exists() {
anyhow::bail!("File not found: {}", args.path.display());
}
let registry = load_registry_from_path(&args.registry)?;
println!(
" Registry: {} (registry-aware verify)",
args.registry.to_str().unwrap_or("<invalid path>")
);
let report = verify_file(&args.path, ®istry)
.with_context(|| format!("Failed to verify file: {}", args.path.display()))?;
match args.format {
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&report)?),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&report)?),
OutputFormat::Text => print_verify_text_report(args, &report),
}
Ok(report.exit_code())
}
fn load_registry_from_path(
path: &std::path::Path,
) -> Result<aion_context::key_registry::KeyRegistry> {
let bytes = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read registry file: {}", path.display()))?;
aion_context::key_registry::KeyRegistry::from_trusted_json(&bytes)
.with_context(|| format!("Failed to parse registry file: {}", path.display()))
}
fn print_verify_text_report(
args: &VerifyArgs,
report: &aion_context::operations::VerificationReport,
) {
println!("\nVerification Results:");
println!("====================");
println!(
"Overall: {}",
if report.is_valid {
"✅ VALID"
} else {
"❌ INVALID"
}
);
println!();
println!(
"Structure: {}",
if report.structure_valid { "✅" } else { "❌" }
);
println!(
"Integrity: {}",
if report.integrity_hash_valid {
"✅"
} else {
"❌"
}
);
println!(
"Hash Chain: {}",
if report.hash_chain_valid {
"✅"
} else {
"❌"
}
);
println!(
"Signatures: {}",
if report.signatures_valid {
"✅"
} else {
"❌"
}
);
if !report.errors.is_empty() {
println!("\nErrors:");
for error in &report.errors {
println!(" • {error}");
}
}
if args.verbose {
println!("\nFile Path: {}", args.path.display());
}
}
fn cmd_show(args: &ShowArgs) -> Result<ExitCode> {
if !args.path.exists() {
anyhow::bail!("File not found: {}", args.path.display());
}
match &args.subcommand {
ShowSubcommand::Rules => show_rules_subcommand(args)?,
ShowSubcommand::History => show_history_subcommand(args)?,
ShowSubcommand::Signatures => show_signatures_subcommand(args)?,
ShowSubcommand::Info => show_info_subcommand(args)?,
}
Ok(ExitCode::SUCCESS)
}
fn show_rules_subcommand(args: &ShowArgs) -> Result<()> {
let rules = show_current_rules(&args.path)?;
match args.format {
OutputFormat::Json | OutputFormat::Yaml => println!("{}", hex::encode(&rules)),
OutputFormat::Text => {
if let Ok(text) = std::str::from_utf8(&rules) {
println!("{text}");
} else {
eprintln!("⚠️ Rules contain binary data, displaying as hex:");
println!("{}", hex::encode(&rules));
}
}
}
Ok(())
}
fn show_history_subcommand(args: &ShowArgs) -> Result<()> {
let versions = show_version_history(&args.path)?;
match args.format {
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&versions)?),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&versions)?),
OutputFormat::Text => {
println!("Version History ({} versions)", versions.len());
println!("================================\n");
for v in &versions {
println!("Version {}:", v.version_number);
println!(" Author: {}", v.author_id);
println!(" Timestamp: {}", v.timestamp);
println!(" Message: {}", v.message);
println!(" Rules Hash: {}", hex::encode(v.rules_hash));
if let Some(parent) = v.parent_hash {
println!(" Parent Hash: {}", hex::encode(parent));
}
println!();
}
}
}
Ok(())
}
fn show_signatures_subcommand(args: &ShowArgs) -> Result<()> {
let registry = load_registry_from_path(&args.registry)?;
let signatures = show_signatures(&args.path, ®istry)?;
match args.format {
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&signatures)?),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&signatures)?),
OutputFormat::Text => {
println!("Signatures ({} total)", signatures.len());
println!("==================\n");
for sig in &signatures {
let status = if sig.verified {
"✅ VALID"
} else {
"❌ INVALID"
};
println!("Version {}: {status}", sig.version_number);
println!(" Author: {}", sig.author_id);
println!(" Public Key: {}", hex::encode(sig.public_key));
if let Some(error) = &sig.error {
println!(" Error: {error}");
}
println!();
}
}
}
Ok(())
}
fn show_info_subcommand(args: &ShowArgs) -> Result<()> {
let registry = load_registry_from_path(&args.registry)?;
let info = show_file_info(&args.path, ®istry)?;
match args.format {
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&info)?),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&info)?),
OutputFormat::Text => {
println!("File Information");
println!("================");
println!("File ID: 0x{:016x}", info.file_id);
println!("Current Version: {}", info.current_version);
println!("Total Versions: {}", info.version_count);
println!("\nLatest Version:");
if let Some(latest) = info.versions.last() {
println!(" Number: {}", latest.version_number);
println!(" Author: {}", latest.author_id);
println!(" Timestamp: {}", latest.timestamp);
println!(" Message: {}", latest.message);
}
println!("\nSignature Status:");
let valid_count = info.signatures.iter().filter(|s| s.verified).count();
println!(" Valid: {valid_count}/{}", info.signatures.len());
}
}
Ok(())
}
fn cmd_key(args: &KeyArgs) -> Result<ExitCode> {
let keystore = KeyStore::new();
match &args.subcommand {
KeySubcommand::Generate { id, description } => {
cmd_key_generate(&keystore, id, description.as_deref())?;
}
KeySubcommand::List => cmd_key_list(&keystore)?,
KeySubcommand::Export { id, output } => cmd_key_export(&keystore, id, output)?,
KeySubcommand::Import { path, id } => cmd_key_import(&keystore, path, id)?,
KeySubcommand::Delete { id, force } => cmd_key_delete(&keystore, id, *force)?,
}
Ok(ExitCode::SUCCESS)
}
fn cmd_key_generate(keystore: &KeyStore, key_id: &str, description: Option<&str>) -> Result<()> {
println!("🔑 Generating new signing key...");
let author_id = parse_key_id(key_id)?;
if keystore.has_signing_key(author_id) {
anyhow::bail!(
"Key already exists for author ID {key_id}\nUse 'aion key delete {key_id}' first or choose a different ID"
);
}
let (_, verifying_key) = keystore
.generate_keypair(author_id)
.context("Failed to generate keypair")?;
println!("\n✅ Key generated successfully!");
println!(" Key ID: {key_id}");
println!(" Author ID: {}", author_id.as_u64());
println!(" Public Key: {}", hex::encode(verifying_key.to_bytes()));
if let Some(desc) = description {
println!(" Description: {desc}");
}
println!("\n💡 Key stored securely in OS keyring");
println!(" Use 'aion key export {key_id}' to create a backup");
Ok(())
}
fn cmd_key_list(keystore: &KeyStore) -> Result<()> {
println!("🔑 Stored Keys");
println!("=============\n");
let keys = keystore.list_keys()?;
if keys.is_empty() {
let mut found_any = false;
for id in 1..10000 {
let author_id = AuthorId::new(id);
if keystore.has_signing_key(author_id) {
found_any = true;
let signing_key = keystore.load_signing_key(author_id)?;
let verifying_key = signing_key.verifying_key();
println!("Key ID: {id}");
println!(" Author ID: {id}");
println!(" Public Key: {}", hex::encode(verifying_key.to_bytes()));
println!();
}
}
if !found_any {
println!("No keys found in keystore.");
println!("\n💡 Generate a new key with: aion key generate <KEY_ID>");
}
} else {
let count = keys.len();
for author_id in keys {
let signing_key = keystore.load_signing_key(author_id)?;
let verifying_key = signing_key.verifying_key();
let id = author_id.as_u64();
println!("Key ID: {id}");
println!(" Author ID: {id}");
println!(" Public Key: {}", hex::encode(verifying_key.to_bytes()));
println!();
}
println!("Total: {count} key(s)");
}
Ok(())
}
fn cmd_key_export(keystore: &KeyStore, key_id: &str, output: &PathBuf) -> Result<()> {
println!("🔐 Exporting key {key_id}...");
let author_id = parse_key_id(key_id)?;
if !keystore.has_signing_key(author_id) {
anyhow::bail!("Key not found: {key_id}\nUse 'aion key list' to see available keys");
}
let password = rpassword::prompt_password("Enter password for encryption: ")
.context("Failed to read password")?;
if password.is_empty() {
anyhow::bail!("Password cannot be empty");
}
let confirm = rpassword::prompt_password("Confirm password: ")
.context("Failed to read password confirmation")?;
if password != confirm {
anyhow::bail!("Passwords do not match");
}
let encrypted_bytes = keystore
.export_encrypted(author_id, &password)
.context("Failed to export key")?;
std::fs::write(output, encrypted_bytes)
.with_context(|| format!("Failed to write to: {}", output.display()))?;
println!("\n✅ Key exported successfully!");
println!(" Output: {}", output.display());
println!(" Size: {} bytes", std::fs::metadata(output)?.len());
println!("\n⚠️ Keep this file secure - it contains your private key!");
Ok(())
}
fn cmd_key_import(keystore: &KeyStore, path: &PathBuf, key_id: &str) -> Result<()> {
println!("🔓 Importing key from {}...", path.display());
let author_id = parse_key_id(key_id)?;
if keystore.has_signing_key(author_id) {
anyhow::bail!(
"Key already exists for author ID {key_id}\nUse 'aion key delete {key_id}' first"
);
}
let encrypted_bytes =
std::fs::read(path).with_context(|| format!("Failed to read from: {}", path.display()))?;
let password =
rpassword::prompt_password("Enter password: ").context("Failed to read password")?;
keystore
.import_encrypted(author_id, &password, &encrypted_bytes)
.context("Failed to import key (wrong password or corrupted file)")?;
let signing_key = keystore.load_signing_key(author_id)?;
let verifying_key = signing_key.verifying_key();
println!("\n✅ Key imported successfully!");
println!(" Key ID: {key_id}");
println!(" Author ID: {}", author_id.as_u64());
println!(" Public Key: {}", hex::encode(verifying_key.to_bytes()));
println!("\n💡 Key stored securely in OS keyring");
Ok(())
}
fn cmd_key_delete(keystore: &KeyStore, key_id: &str, force: bool) -> Result<()> {
let author_id = parse_key_id(key_id)?;
if !keystore.has_signing_key(author_id) {
anyhow::bail!("Key not found: {key_id}\nUse 'aion key list' to see available keys");
}
if !force {
print!("⚠️ Are you sure you want to delete key {key_id}? This cannot be undone. (y/N): ");
std::io::stdout().flush()?;
let mut response = String::new();
std::io::stdin().read_line(&mut response)?;
if !response.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
keystore
.delete_signing_key(author_id)
.context("Failed to delete key")?;
println!("✅ Key {key_id} deleted from keystore");
Ok(())
}
fn parse_key_id(key_id: &str) -> Result<AuthorId> {
let id = key_id
.parse::<u64>()
.with_context(|| format!("Invalid key ID '{key_id}': must be a number"))?;
Ok(AuthorId::new(id))
}
fn load_rules_content(path: Option<&PathBuf>) -> Result<Vec<u8>> {
if let Some(file_path) = path {
std::fs::read(file_path).with_context(|| {
format!(
"Failed to read rules from: {file_path}",
file_path = file_path.display()
)
})
} else {
let mut buffer = Vec::new();
io::stdin()
.read_to_end(&mut buffer)
.context("Failed to read rules from stdin")?;
Ok(buffer)
}
}
fn cmd_report(args: &ReportArgs) -> Result<ExitCode> {
let framework: ComplianceFramework = args.framework.into();
let format: ReportFormat = args.format.into();
eprintln!(
"📊 Generating {} report for: {}",
framework,
args.path.display()
);
eprintln!(" Format: {:?}", args.format);
let registry = load_registry_from_path(&args.registry)?;
let report = generate_compliance_report(&args.path, framework, format, ®istry)
.context("Failed to generate compliance report")?;
if let Some(output_path) = &args.output {
std::fs::write(output_path, &report)
.with_context(|| format!("Failed to write report to: {}", output_path.display()))?;
eprintln!("✅ Report saved to: {}", output_path.display());
} else {
println!("{report}");
}
Ok(ExitCode::SUCCESS)
}
fn cmd_export(args: &ExportArgs) -> Result<ExitCode> {
let format: ExportFormat = args.format.into();
eprintln!(
"📤 Exporting {} from: {}",
format_name(args.format),
args.path.display()
);
let registry = load_registry_from_path(&args.registry)?;
let output = export_file(&args.path, format, ®istry).context("Failed to export file")?;
if let Some(output_path) = &args.output {
std::fs::write(output_path, &output)
.with_context(|| format!("Failed to write export to: {}", output_path.display()))?;
eprintln!("✅ Exported to: {}", output_path.display());
} else {
println!("{output}");
}
Ok(ExitCode::SUCCESS)
}
const fn format_name(format: ExportFormatType) -> &'static str {
match format {
ExportFormatType::Json => "JSON",
ExportFormatType::Yaml => "YAML",
ExportFormatType::Csv => "CSV",
}
}
fn cmd_registry(args: &RegistryArgs) -> Result<ExitCode> {
match &args.subcommand {
RegistrySubcommand::Pin {
author,
key,
master,
output,
} => cmd_registry_pin(*author, key, master.as_deref(), output)?,
RegistrySubcommand::Rotate {
author,
from_epoch,
to_epoch,
new_key,
master_key,
effective_from_version,
registry,
no_warn,
} => cmd_registry_rotate(
*author,
*from_epoch,
*to_epoch,
new_key,
master_key,
*effective_from_version,
registry,
*no_warn,
)?,
RegistrySubcommand::Revoke {
author,
epoch,
reason,
master_key,
effective_from_version,
registry,
} => cmd_registry_revoke(
*author,
*epoch,
(*reason).into(),
master_key,
*effective_from_version,
registry,
)?,
}
Ok(ExitCode::SUCCESS)
}
fn cmd_registry_pin(
author: u64,
key_id: &str,
master_key_id: Option<&str>,
output: &std::path::Path,
) -> Result<()> {
let keystore = KeyStore::new();
let op_signer = load_key_for_registry(&keystore, key_id)?;
let master_signer = match master_key_id {
Some(id) => load_key_for_registry(&keystore, id)?,
None => op_signer.clone(),
};
let author_id = AuthorId::new(author);
let mut registry = load_or_new_registry(output)?;
registry
.register_author(
author_id,
master_signer.verifying_key(),
op_signer.verifying_key(),
0,
)
.with_context(|| format!("Failed to pin author {author}"))?;
write_registry_atomic(®istry, output)?;
println!("✅ Pinned author {author} with key '{key_id}'");
println!(" Registry: {}", output.display());
Ok(())
}
#[allow(clippy::too_many_arguments)] fn cmd_registry_rotate(
author: u64,
from_epoch: u32,
to_epoch: u32,
new_key_id: &str,
master_key_id: &str,
effective_from_version: u64,
registry_path: &std::path::Path,
no_warn: bool,
) -> Result<()> {
let keystore = KeyStore::new();
let new_op = load_key_for_registry(&keystore, new_key_id)?;
let master = load_key_for_registry(&keystore, master_key_id)?;
let mut registry = load_existing_registry(registry_path)?;
if !no_warn {
warn_retroactive_rotation(®istry, author, from_epoch, effective_from_version);
}
let record = aion_context::key_registry::sign_rotation_record(
AuthorId::new(author),
from_epoch,
to_epoch,
new_op.verifying_key().to_bytes(),
effective_from_version,
&master,
);
registry.apply_rotation(&record).with_context(|| {
format!(
"Failed to apply rotation for author {author} (from epoch {from_epoch} \
to epoch {to_epoch} effective version {effective_from_version})"
)
})?;
write_registry_atomic(®istry, registry_path)?;
println!(
"✅ Rotated author {author}: epoch {from_epoch} → {to_epoch}, effective from version {effective_from_version}"
);
println!(" Registry: {}", registry_path.display());
Ok(())
}
fn warn_retroactive_rotation(
registry: &aion_context::key_registry::KeyRegistry,
author: u64,
from_epoch: u32,
effective_from_version: u64,
) {
let author_id = AuthorId::new(author);
let active = registry
.epochs_for(author_id)
.iter()
.find(|epoch| matches!(epoch.status, aion_context::key_registry::KeyStatus::Active));
let Some(epoch) = active else {
return;
};
if epoch.epoch != from_epoch {
return;
}
if epoch.created_at_version != effective_from_version {
return;
}
eprintln!(
"⚠️ --effective-from-version {effective_from_version} matches epoch {from_epoch}'s \
created_at_version. Epoch {from_epoch}'s window collapses to [{effective_from_version}, \
{effective_from_version}); every existing signature by author {author} at version \
{effective_from_version} will fail verify under the new registry."
);
eprintln!(
" Suggested fix: pass `--effective-from-version {next}` (or higher) to leave \
existing v{effective_from_version} signatures valid, OR migrate to a growing-chain \
architecture (issue #50).",
next = effective_from_version.saturating_add(1),
);
eprintln!(" Use `--no-warn` to suppress this message.");
}
fn cmd_registry_revoke(
author: u64,
epoch: u32,
reason: aion_context::key_registry::RevocationReason,
master_key_id: &str,
effective_from_version: u64,
registry_path: &std::path::Path,
) -> Result<()> {
let keystore = KeyStore::new();
let master = load_key_for_registry(&keystore, master_key_id)?;
let mut registry = load_existing_registry(registry_path)?;
let record = aion_context::key_registry::sign_revocation_record(
AuthorId::new(author),
epoch,
reason,
effective_from_version,
&master,
);
registry.apply_revocation(&record).with_context(|| {
format!(
"Failed to apply revocation for author {author} epoch {epoch} \
effective version {effective_from_version}"
)
})?;
write_registry_atomic(®istry, registry_path)?;
println!(
"✅ Revoked author {author} epoch {epoch} ({reason:?}), effective from version {effective_from_version}"
);
println!(" Registry: {}", registry_path.display());
Ok(())
}
fn load_existing_registry(
path: &std::path::Path,
) -> Result<aion_context::key_registry::KeyRegistry> {
if !path.exists() {
anyhow::bail!(
"Registry file not found: {}\n\
Hint: use `aion registry pin` first to create one.",
path.display()
);
}
let bytes = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read registry file: {}", path.display()))?;
aion_context::key_registry::KeyRegistry::from_trusted_json(&bytes)
.with_context(|| format!("Failed to parse registry file: {}", path.display()))
}
fn load_or_new_registry(path: &std::path::Path) -> Result<aion_context::key_registry::KeyRegistry> {
if path.exists() {
load_existing_registry(path)
} else {
Ok(aion_context::key_registry::KeyRegistry::new())
}
}
fn write_registry_atomic(
registry: &aion_context::key_registry::KeyRegistry,
path: &std::path::Path,
) -> Result<()> {
let json = registry
.to_trusted_json()
.context("Failed to serialize registry")?;
let mut tmp = path.to_path_buf();
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("registry.json");
tmp.set_file_name(format!(".{file_name}.tmp"));
std::fs::write(&tmp, &json)
.with_context(|| format!("Failed to write staging file: {}", tmp.display()))?;
std::fs::rename(&tmp, path)
.with_context(|| format!("Failed to rename into place: {}", path.display()))?;
Ok(())
}
fn load_key_for_registry(
keystore: &KeyStore,
key_id: &str,
) -> Result<aion_context::crypto::SigningKey> {
let parsed_id = parse_key_id(key_id)?;
keystore
.load_signing_key(parsed_id)
.with_context(|| format!("Failed to load key '{key_id}' from keystore"))
}
#[derive(serde::Serialize, serde::Deserialize)]
struct ReleaseBundle {
signer: u64,
model_ref: aion_context::aibom::ModelRef,
manifest_canonical_hex: String,
manifest_signature: BundleSig,
manifest_dsse: aion_context::dsse::DsseEnvelope,
aibom: aion_context::aibom::AiBom,
aibom_dsse: aion_context::dsse::DsseEnvelope,
slsa_statement: aion_context::slsa::InTotoStatement,
slsa_dsse: aion_context::dsse::DsseEnvelope,
oci_primary: aion_context::oci::OciArtifactManifest,
oci_aibom_referrer: aion_context::oci::OciArtifactManifest,
oci_slsa_referrer: aion_context::oci::OciArtifactManifest,
log_entries: Vec<BundleLogSeq>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct BundleSig {
author_id: u64,
public_key_hex: String,
signature_hex: String,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct BundleLogSeq {
kind: u16,
seq: u64,
}
fn cmd_release(args: &ReleaseArgs) -> Result<ExitCode> {
match &args.subcommand {
ReleaseSubcommand::Seal {
primary,
primary_name,
model_name,
model_version,
model_format,
framework,
license,
safety_attestation,
export_control,
builder_id,
aion_version,
author,
key,
out_dir,
} => cmd_release_seal(SealInputs {
primary,
primary_name,
model_name,
model_version,
model_format,
framework,
license,
safety_attestation,
export_control,
builder_id,
aion_version: *aion_version,
author: *author,
key,
out_dir,
})?,
ReleaseSubcommand::Verify {
bundle,
registry,
at_version,
} => return cmd_release_verify(bundle, registry, *at_version),
ReleaseSubcommand::Inspect { bundle, format } => cmd_release_inspect(bundle, *format)?,
}
Ok(ExitCode::SUCCESS)
}
struct SealInputs<'a> {
primary: &'a PathBuf,
primary_name: &'a str,
model_name: &'a str,
model_version: &'a str,
model_format: &'a str,
framework: &'a [String],
license: &'a [String],
safety_attestation: &'a [String],
export_control: &'a [String],
builder_id: &'a str,
aion_version: u64,
author: u64,
key: &'a str,
out_dir: &'a PathBuf,
}
#[allow(clippy::needless_pass_by_value)] fn cmd_release_seal(inp: SealInputs<'_>) -> Result<()> {
use aion_context::aibom::{ExportControl, FrameworkRef, License, SafetyAttestation};
use aion_context::release::ReleaseBuilder;
use aion_context::transparency_log::TransparencyLog;
use aion_context::types::AuthorId;
let primary_bytes = std::fs::read(inp.primary)
.with_context(|| format!("Failed to read primary artifact: {}", inp.primary.display()))?;
println!("📦 Sealing {} v{}", inp.model_name, inp.model_version);
println!(
" Primary: {} ({} bytes)",
inp.primary_name,
primary_bytes.len()
);
let keystore = KeyStore::new();
let signing_key = load_key_for_registry(&keystore, inp.key)?;
let mut builder = ReleaseBuilder::new(inp.model_name, inp.model_version, inp.model_format);
builder.primary_artifact(inp.primary_name.to_string(), primary_bytes);
for spec in inp.framework {
let (name, version) = parse_kv_pair(spec, "framework")?;
builder.add_framework(FrameworkRef {
name,
version,
cpe: None,
});
}
for spec in inp.license {
let (spdx_id, scope_str) = parse_kv_pair(spec, "license")?;
let scope = parse_license_scope(&scope_str)?;
builder.add_license(License {
spdx_id,
scope,
text_uri: None,
});
}
for spec in inp.safety_attestation {
let (name, result) = parse_kv_pair(spec, "safety-attestation")?;
builder.add_safety_attestation(SafetyAttestation {
name,
result,
report_hash_algorithm: None,
report_hash: None,
report_uri: None,
});
}
for spec in inp.export_control {
let (regime, classification) = parse_kv_pair(spec, "export-control")?;
builder.add_export_control(ExportControl {
regime,
classification,
notes: None,
});
}
builder.builder_id(inp.builder_id.to_string());
builder.current_aion_version(inp.aion_version);
let mut log = TransparencyLog::new();
let signed = builder
.seal(AuthorId::new(inp.author), &signing_key, &mut log)
.context("ReleaseBuilder::seal failed")?;
let bundle = signed_release_to_bundle(&signed);
let bundle_json = serde_json::to_string_pretty(&bundle).context("serialize bundle")?;
std::fs::create_dir_all(inp.out_dir)
.with_context(|| format!("create out-dir: {}", inp.out_dir.display()))?;
let bundle_path = inp.out_dir.join("release.json");
std::fs::write(&bundle_path, &bundle_json)
.with_context(|| format!("write bundle: {}", bundle_path.display()))?;
let primary_out = inp.out_dir.join("primary.bin");
std::fs::copy(inp.primary, &primary_out).with_context(|| {
format!(
"copy primary to bundle: {} -> {}",
inp.primary.display(),
primary_out.display()
)
})?;
println!(
"\n✅ Sealed {} v{}",
signed.model_ref.name, signed.model_ref.version
);
println!(" Bundle: {}", bundle_path.display());
println!(" Primary: {}", primary_out.display());
println!(" Log entries: {}", signed.log_entries.len());
Ok(())
}
fn parse_kv_pair(spec: &str, label: &str) -> Result<(String, String)> {
let (left, right) = spec
.split_once(':')
.ok_or_else(|| anyhow::anyhow!("--{label} expects `<key>:<value>`, got: {spec}"))?;
Ok((left.to_string(), right.to_string()))
}
fn parse_license_scope(s: &str) -> Result<aion_context::aibom::LicenseScope> {
use aion_context::aibom::LicenseScope;
match s.to_ascii_lowercase().as_str() {
"weights" => Ok(LicenseScope::Weights),
"source" | "source_code" | "sourcecode" => Ok(LicenseScope::SourceCode),
"data" | "training_data" => Ok(LicenseScope::TrainingData),
"docs" | "documentation" => Ok(LicenseScope::Documentation),
"combined" => Ok(LicenseScope::Combined),
other => Err(anyhow::anyhow!(
"unknown license scope: {other} (expected weights|source|data|docs|combined)"
)),
}
}
fn signed_release_to_bundle(s: &aion_context::release::SignedRelease) -> ReleaseBundle {
ReleaseBundle {
signer: s.signer.as_u64(),
model_ref: s.model_ref.clone(),
manifest_canonical_hex: hex::encode(s.manifest.canonical_bytes()),
manifest_signature: BundleSig {
author_id: s.manifest_signature.author_id,
public_key_hex: hex::encode(s.manifest_signature.public_key),
signature_hex: hex::encode(s.manifest_signature.signature),
},
manifest_dsse: s.manifest_dsse.clone(),
aibom: s.aibom.clone(),
aibom_dsse: s.aibom_dsse.clone(),
slsa_statement: s.slsa_statement.clone(),
slsa_dsse: s.slsa_dsse.clone(),
oci_primary: s.oci_primary.clone(),
oci_aibom_referrer: s.oci_aibom_referrer.clone(),
oci_slsa_referrer: s.oci_slsa_referrer.clone(),
log_entries: s
.log_entries
.iter()
.map(|l| BundleLogSeq {
kind: l.kind as u16,
seq: l.seq,
})
.collect(),
}
}
fn bundle_to_signed_release(b: ReleaseBundle) -> Result<aion_context::release::SignedRelease> {
use aion_context::manifest::ArtifactManifest;
use aion_context::serializer::SignatureEntry;
use aion_context::transparency_log::LogEntryKind;
use aion_context::types::AuthorId;
let manifest_bytes =
hex::decode(&b.manifest_canonical_hex).context("decode manifest canonical hex")?;
let manifest = ArtifactManifest::from_canonical_bytes(&manifest_bytes)
.context("parse manifest from canonical bytes")?;
let pk_bytes = hex::decode(&b.manifest_signature.public_key_hex)
.context("decode manifest_signature.public_key_hex")?;
let sig_bytes = hex::decode(&b.manifest_signature.signature_hex)
.context("decode manifest_signature.signature_hex")?;
let pk_arr: [u8; 32] = pk_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("manifest_signature.public_key must be 32 bytes"))?;
let sig_arr: [u8; 64] = sig_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("manifest_signature.signature must be 64 bytes"))?;
let manifest_signature = SignatureEntry::new(
AuthorId::new(b.manifest_signature.author_id),
pk_arr,
sig_arr,
);
let mut log_entries: Vec<(LogEntryKind, u64)> = Vec::with_capacity(b.log_entries.len());
for l in b.log_entries {
let kind = LogEntryKind::from_u16(l.kind)
.with_context(|| format!("unknown LogEntryKind: {}", l.kind))?;
log_entries.push((kind, l.seq));
}
Ok(aion_context::release::SignedRelease::from_components(
aion_context::release::SignedReleaseComponents {
signer: AuthorId::new(b.signer),
model_ref: b.model_ref,
manifest,
manifest_signature,
manifest_dsse: b.manifest_dsse,
aibom: b.aibom,
aibom_dsse: b.aibom_dsse,
slsa_statement: b.slsa_statement,
slsa_dsse: b.slsa_dsse,
oci_primary: b.oci_primary,
oci_aibom_referrer: b.oci_aibom_referrer,
oci_slsa_referrer: b.oci_slsa_referrer,
log_entries,
},
))
}
fn cmd_release_verify(
bundle_dir: &std::path::Path,
registry_path: &std::path::Path,
at_version: u64,
) -> Result<ExitCode> {
let bundle_path = bundle_dir.join("release.json");
let bundle_json = std::fs::read_to_string(&bundle_path)
.with_context(|| format!("read bundle: {}", bundle_path.display()))?;
let bundle: ReleaseBundle = serde_json::from_str(&bundle_json).context("parse release.json")?;
let signed = bundle_to_signed_release(bundle)?;
let registry = load_registry_from_path(registry_path)?;
println!(
"🔍 Verifying release {} v{}",
signed.model_ref.name, signed.model_ref.version
);
match signed.verify(®istry, at_version) {
Ok(()) => {
println!("✅ VALID at version {at_version}");
Ok(ExitCode::SUCCESS)
}
Err(e) => {
eprintln!("❌ INVALID: {e}");
Ok(ExitCode::FAILURE)
}
}
}
fn cmd_release_inspect(bundle_dir: &std::path::Path, format: OutputFormat) -> Result<()> {
let bundle_path = bundle_dir.join("release.json");
let bundle_json = std::fs::read_to_string(&bundle_path)
.with_context(|| format!("read bundle: {}", bundle_path.display()))?;
let bundle: ReleaseBundle = serde_json::from_str(&bundle_json).context("parse release.json")?;
match format {
OutputFormat::Json => println!("{bundle_json}"),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&bundle)?),
OutputFormat::Text => {
println!("Release bundle: {}", bundle_path.display());
println!(" signer: {}", bundle.signer);
println!(
" model: {} v{} ({})",
bundle.model_ref.name, bundle.model_ref.version, bundle.model_ref.format
);
println!(" model size: {} bytes", bundle.model_ref.size);
println!(
" model hash: {} ({})",
hex::encode(bundle.model_ref.hash),
bundle.model_ref.hash_algorithm
);
println!(" frameworks: {}", bundle.aibom.frameworks.len());
println!(" licenses: {}", bundle.aibom.licenses.len());
println!(
" safety atts: {}",
bundle.aibom.safety_attestations.len()
);
println!(" export ctrl: {}", bundle.aibom.export_controls.len());
println!(" log entries: {}", bundle.log_entries.len());
}
}
Ok(())
}
#[derive(serde::Serialize)]
struct FileVerdict {
path: String,
valid: bool,
author: Option<u64>,
public_key_prefix: Option<String>,
error: Option<String>,
}
#[derive(serde::Serialize)]
struct AuthorBreakdown {
author: u64,
keys: Vec<KeyUsage>,
rotated: bool,
}
#[derive(serde::Serialize)]
struct KeyUsage {
pubkey_prefix: String,
files: Vec<String>,
}
#[derive(serde::Serialize)]
struct ArchiveReport {
archive_dir: String,
file_count: usize,
valid_count: usize,
invalid_count: usize,
files: Vec<FileVerdict>,
authors: Vec<AuthorBreakdown>,
}
fn cmd_archive(args: &ArchiveArgs) -> Result<ExitCode> {
match &args.subcommand {
ArchiveSubcommand::Verify {
dir,
registry,
format,
} => cmd_archive_verify(dir, registry, *format),
}
}
fn cmd_archive_verify(
dir: &std::path::Path,
registry_path: &std::path::Path,
format: OutputFormat,
) -> Result<ExitCode> {
if !dir.is_dir() {
anyhow::bail!("Not a directory: {}", dir.display());
}
let registry = load_registry_from_path(registry_path)?;
let aion_files = collect_aion_files(dir)?;
if aion_files.is_empty() {
anyhow::bail!("No `.aion` files found in {}", dir.display());
}
let report = build_archive_report(dir, &aion_files, ®istry);
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&report)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&report)?);
}
OutputFormat::Text => render_archive_report_text(&report),
}
if report.invalid_count == 0 {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::FAILURE)
}
}
fn collect_aion_files(dir: &std::path::Path) -> Result<Vec<PathBuf>> {
let mut paths: Vec<PathBuf> = Vec::new();
for entry in std::fs::read_dir(dir)
.with_context(|| format!("Failed to read directory: {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|s| s.to_str()) == Some("aion") {
paths.push(path);
}
}
paths.sort();
Ok(paths)
}
fn build_archive_report(
dir: &std::path::Path,
paths: &[PathBuf],
registry: &aion_context::key_registry::KeyRegistry,
) -> ArchiveReport {
use std::collections::BTreeMap;
let mut files = Vec::with_capacity(paths.len());
let mut valid_count = 0usize;
let mut invalid_count = 0usize;
let mut grouping: BTreeMap<u64, BTreeMap<String, Vec<String>>> = BTreeMap::new();
for path in paths {
let display = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("?")
.to_string();
let report = match aion_context::operations::verify_file(path, registry) {
Ok(r) => r,
Err(e) => {
invalid_count = invalid_count.saturating_add(1);
files.push(FileVerdict {
path: display.clone(),
valid: false,
author: None,
public_key_prefix: None,
error: Some(format!("verify_file failed: {e}")),
});
continue;
}
};
if report.is_valid {
valid_count = valid_count.saturating_add(1);
} else {
invalid_count = invalid_count.saturating_add(1);
}
let signatures =
aion_context::operations::show_signatures(path, registry).unwrap_or_default();
let (author, pk_prefix) = signatures.last().map_or((None, None), |head| {
let mut hex_buf = String::with_capacity(16);
for b in head.public_key.iter().take(8) {
use std::fmt::Write;
let _ = write!(&mut hex_buf, "{b:02x}");
}
(Some(head.author_id), Some(hex_buf))
});
if let (Some(a), Some(pk)) = (author, &pk_prefix) {
grouping
.entry(a)
.or_default()
.entry(pk.clone())
.or_default()
.push(display.clone());
}
files.push(FileVerdict {
path: display,
valid: report.is_valid,
author,
public_key_prefix: pk_prefix,
error: report.errors.first().cloned(),
});
}
let authors: Vec<AuthorBreakdown> = grouping
.into_iter()
.map(|(author, key_map)| {
let keys: Vec<KeyUsage> = key_map
.into_iter()
.map(|(pubkey_prefix, files)| KeyUsage {
pubkey_prefix,
files,
})
.collect();
let rotated = keys.len() > 1;
AuthorBreakdown {
author,
keys,
rotated,
}
})
.collect();
ArchiveReport {
archive_dir: dir.display().to_string(),
file_count: paths.len(),
valid_count,
invalid_count,
files,
authors,
}
}
fn render_archive_report_text(r: &ArchiveReport) {
println!();
println!("═══════════════════════════════════════════════════════════════════════");
println!(" Archive verification — {}", r.archive_dir);
println!("═══════════════════════════════════════════════════════════════════════");
println!();
println!("Pass 1 — Per-file verification");
println!("──────────────────────────────");
let header = ("FILE", "VERDICT", "DETAILS");
println!(" {:<28} {:<10} {}", header.0, header.1, header.2);
for f in &r.files {
let verdict = if f.valid { "✅ VALID" } else { "❌ INVALID" };
let details = f.error.as_deref().unwrap_or("");
println!(" {:<28} {:<10} {}", f.path, verdict, details);
}
println!();
println!(
" Summary: {}/{} VALID, {} INVALID",
r.valid_count, r.file_count, r.invalid_count
);
println!();
println!("Pass 2 — Signer breakdown");
println!("─────────────────────────");
for author in &r.authors {
if author.rotated {
println!(
" ⚙ Author {} — ROTATED ({} distinct keys observed)",
author.author,
author.keys.len()
);
} else {
println!(" ✓ Author {} — stable", author.author);
}
for k in &author.keys {
println!(
" key {}… files: {}",
k.pubkey_prefix,
k.files.join(", ")
);
}
}
println!();
println!("Findings");
println!("────────");
if r.invalid_count == 0 {
println!(
" ✅ All {} files verified. Archive passes audit.",
r.file_count
);
} else {
println!(
" ❗ {} of {} files failed verification.",
r.invalid_count, r.file_count
);
for f in r.files.iter().filter(|f| !f.valid) {
println!(" - {}", f.path);
}
}
}