mod acl_cli;
mod did_key;
#[cfg(feature = "setup")]
mod did_webvh;
mod import_did;
mod keys_cli;
#[cfg(feature = "setup")]
mod setup;
#[cfg(feature = "webvh")]
mod webvh_cli;
use vta_service::*;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use clap::{Parser, Subcommand};
use config::AppConfig;
use ed25519_dalek::SigningKey;
use ed25519_dalek_bip32::{DerivationPath, ExtendedSigningKey};
use keys::seed_store::create_seed_store;
use keys::seeds::load_seed_bytes;
use multibase::Base;
use std::path::PathBuf;
use std::sync::Arc;
#[cfg(not(any(feature = "rest", feature = "didcomm")))]
compile_error!("At least one of 'rest' or 'didcomm' must be enabled.");
#[derive(Parser)]
#[command(name = "vta", about = "Verifiable Trust Agent", version)]
struct Cli {
#[arg(short, long, global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Setup,
BootstrapAdmin {
#[arg(long)]
did: String,
#[arg(long)]
label: Option<String>,
},
Unseal,
ExportAdmin,
Status,
CreateDidKey {
#[arg(long)]
context: String,
#[arg(long)]
admin: bool,
#[arg(long)]
label: Option<String>,
},
CreateDidWebvh {
#[arg(long)]
context: String,
#[arg(long)]
label: Option<String>,
},
ImportDid {
#[arg(long)]
did: String,
#[arg(long)]
role: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long)]
context: Vec<String>,
},
Acl {
#[command(subcommand)]
command: AclCommands,
},
Keys {
#[command(subcommand)]
command: KeyCliCommands,
},
#[cfg(feature = "webvh")]
Webvh {
#[command(subcommand)]
command: WebvhCommands,
},
}
#[derive(Subcommand)]
enum KeyCliCommands {
List {
#[arg(long)]
context: Option<String>,
#[arg(long)]
status: Option<String>,
},
Secrets {
key_ids: Vec<String>,
#[arg(long)]
context: Option<String>,
},
Seeds,
RotateSeed {
#[arg(long)]
mnemonic: Option<String>,
},
}
#[cfg(feature = "webvh")]
#[derive(Subcommand)]
enum WebvhCommands {
AddServer {
#[arg(long)]
id: String,
#[arg(long)]
did: String,
#[arg(long)]
label: Option<String>,
},
ListServers,
UpdateServer {
id: String,
#[arg(long)]
label: Option<String>,
},
RemoveServer {
id: String,
},
CreateDid {
#[arg(long)]
context: String,
#[arg(long)]
server: String,
#[arg(long)]
path: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long, default_value_t = true)]
portable: bool,
#[arg(long)]
mediator_service: bool,
#[arg(long)]
services: Option<String>,
#[arg(long)]
pre_rotation: Option<u32>,
},
ListDids {
#[arg(long)]
context: Option<String>,
#[arg(long)]
server: Option<String>,
},
DeleteDid {
did: String,
},
}
#[derive(Subcommand)]
enum AclCommands {
List {
#[arg(long)]
context: Option<String>,
#[arg(long)]
role: Option<String>,
},
Get {
did: String,
},
Update {
did: String,
#[arg(long)]
role: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long, value_delimiter = ',')]
contexts: Option<Vec<String>>,
},
Delete {
did: String,
#[arg(short, long)]
yes: bool,
},
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
print_banner();
match cli.command {
Some(Commands::Setup) => {
#[cfg(feature = "setup")]
{
if let Err(e) = setup::run_setup_wizard(cli.config).await {
eprintln!("Setup failed: {e}");
std::process::exit(1);
}
}
#[cfg(not(feature = "setup"))]
{
eprintln!("Setup wizard not available (compiled without 'setup' feature)");
std::process::exit(1);
}
}
Some(Commands::BootstrapAdmin { did, label }) => {
if let Err(e) = run_bootstrap_admin(cli.config, did, label).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::Unseal) => {
let config = match AppConfig::load(cli.config) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let store = store::Store::open(&config.store).expect("failed to open store");
if let Err(e) = seal::run_unseal_challenge(&store).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::ExportAdmin) => {
check_seal(&cli.config).await;
if let Err(e) = export_admin(cli.config).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::Status) => {
if let Ok(config) = AppConfig::load(cli.config.clone()) {
init_tracing(&config);
}
if let Err(e) = status::run_status(cli.config).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::CreateDidKey {
context,
admin,
label,
}) => {
check_seal(&cli.config).await;
let args = did_key::CreateDidKeyArgs {
config_path: cli.config,
context,
admin,
label,
};
if let Err(e) = did_key::run_create_did_key(args).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::CreateDidWebvh { context, label }) => {
check_seal(&cli.config).await;
#[cfg(feature = "setup")]
{
let args = did_webvh::CreateDidWebvhArgs {
config_path: cli.config,
context,
label,
};
if let Err(e) = did_webvh::run_create_did_webvh(args).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
#[cfg(not(feature = "setup"))]
{
let _ = (context, label);
eprintln!("create-did-webvh is not available (compiled without 'setup' feature)");
std::process::exit(1);
}
}
Some(Commands::ImportDid {
did,
role,
label,
context,
}) => {
check_seal(&cli.config).await;
let args = import_did::ImportDidArgs {
config_path: cli.config,
did,
role,
label,
context,
};
if let Err(e) = import_did::run_import_did(args).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::Keys { command }) => {
match &command {
KeyCliCommands::List { .. } | KeyCliCommands::Seeds => {}
KeyCliCommands::Secrets { .. } | KeyCliCommands::RotateSeed { .. } => {
check_seal(&cli.config).await;
}
}
let result = match command {
KeyCliCommands::List { context, status } => {
keys_cli::run_keys_list(cli.config, context, status).await
}
KeyCliCommands::Secrets { key_ids, context } => {
keys_cli::run_keys_secrets(cli.config, key_ids, context).await
}
KeyCliCommands::Seeds => keys_cli::run_keys_seeds_list(cli.config).await,
KeyCliCommands::RotateSeed { mnemonic } => {
keys_cli::run_rotate_seed(cli.config, mnemonic).await
}
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::Acl { command }) => {
match &command {
AclCommands::List { .. } | AclCommands::Get { .. } => {}
AclCommands::Update { .. } | AclCommands::Delete { .. } => {
check_seal(&cli.config).await;
}
}
let result = match command {
AclCommands::List { context, role } => {
acl_cli::run_acl_list(cli.config, context, role).await
}
AclCommands::Get { did } => acl_cli::run_acl_get(cli.config, did).await,
AclCommands::Update {
did,
role,
label,
contexts,
} => acl_cli::run_acl_update(cli.config, did, role, label, contexts).await,
AclCommands::Delete { did, yes } => {
acl_cli::run_acl_delete(cli.config, did, yes).await
}
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
#[cfg(feature = "webvh")]
Some(Commands::Webvh { command }) => {
match &command {
WebvhCommands::ListServers | WebvhCommands::ListDids { .. } => {}
_ => check_seal(&cli.config).await,
}
let result = match command {
WebvhCommands::AddServer { id, did, label } => {
webvh_cli::run_add_server(cli.config, id, did, label).await
}
WebvhCommands::ListServers => webvh_cli::run_list_servers(cli.config).await,
WebvhCommands::UpdateServer { id, label } => {
webvh_cli::run_update_server(cli.config, id, label).await
}
WebvhCommands::RemoveServer { id } => {
webvh_cli::run_remove_server(cli.config, id).await
}
WebvhCommands::CreateDid {
context,
server,
path,
label,
portable,
mediator_service,
services,
pre_rotation,
} => {
webvh_cli::run_create_did(
cli.config,
context,
server,
path,
label,
portable,
mediator_service,
services,
pre_rotation,
)
.await
}
WebvhCommands::ListDids { context, server } => {
webvh_cli::run_list_dids(cli.config, context, server).await
}
WebvhCommands::DeleteDid { did } => {
webvh_cli::run_delete_did(cli.config, did).await
}
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
None => {
let config = match AppConfig::load(cli.config) {
Ok(config) => config,
Err(e) => {
eprintln!("Error: {e}");
eprintln!();
eprintln!("To set up a new VTA instance, run:");
eprintln!(" vta setup");
eprintln!();
eprintln!("Or specify a config file:");
eprintln!(" vta --config <path>");
std::process::exit(1);
}
};
init_tracing(&config);
let store = store::Store::open(&config.store).expect("failed to open store");
let seed_store: Arc<dyn keys::seed_store::SeedStore> =
Arc::from(create_seed_store(&config).expect("failed to create seed store"));
if let Err(e) = server::run(
config,
store,
seed_store,
None, None, )
.await
{
tracing::error!("server error: {e}");
std::process::exit(1);
}
}
}
}
fn print_banner() {
let cyan = "\x1b[36m";
let magenta = "\x1b[35m";
let yellow = "\x1b[33m";
let dim = "\x1b[2m";
let reset = "\x1b[0m";
eprintln!(
r#"
{cyan} ██╗ ██╗{magenta}████████╗{yellow} █████╗{reset}
{cyan} ██║ ██║{magenta}╚══██╔══╝{yellow}██╔══██╗{reset}
{cyan} ██║ ██║{magenta} ██║ {yellow}███████║{reset}
{cyan} ╚██╗ ██╔╝{magenta} ██║ {yellow}██╔══██║{reset}
{cyan} ╚████╔╝ {magenta} ██║ {yellow}██║ ██║{reset}
{cyan} ╚═══╝ {magenta} ╚═╝ {yellow}╚═╝ ╚═╝{reset}
{dim} Verifiable Trust Agent v{version}{reset}
"#,
version = env!("CARGO_PKG_VERSION"),
);
}
async fn check_seal(config_path: &Option<PathBuf>) {
let config = match AppConfig::load(config_path.clone()) {
Ok(c) => c,
Err(_) => return, };
let store = match store::Store::open(&config.store) {
Ok(s) => s,
Err(_) => return, };
if let Err(e) = seal::require_unsealed(&store).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
async fn run_bootstrap_admin(
config_path: Option<PathBuf>,
did: String,
label: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfig::load(config_path)?;
let store = store::Store::open(&config.store)?;
let acl_ks = store.keyspace("acl")?;
if let Some(existing) = seal::get_seal(&acl_ks).await? {
eprintln!(
"VTA is already sealed (by {} on {}).",
existing.sealed_by,
existing.sealed_at.format("%Y-%m-%d %H:%M:%S UTC")
);
eprintln!("Cannot bootstrap again. Manage admins via the REST API or DIDComm.");
std::process::exit(1);
}
let entries = acl::list_acl_entries(&acl_ks).await?;
let existing_super_admins: Vec<_> = entries
.iter()
.filter(|e| e.role == acl::Role::Admin && e.allowed_contexts.is_empty())
.collect();
if !existing_super_admins.is_empty() {
eprintln!("WARNING: {} existing super admin(s) found:", existing_super_admins.len());
for admin in &existing_super_admins {
eprintln!(" - {} ({})", admin.did, admin.label.as_deref().unwrap_or("no label"));
}
eprintln!();
eprintln!("Proceeding will add another super admin and seal the VTA.");
eprintln!("Press Ctrl+C to cancel, or Enter to continue...");
let mut buf = String::new();
std::io::stdin().read_line(&mut buf)?;
}
let entry = acl::AclEntry {
did: did.clone(),
role: acl::Role::Admin,
label,
allowed_contexts: vec![], created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
created_by: "cli:bootstrap-admin".into(),
};
acl::store_acl_entry(&acl_ks, &entry).await?;
let seal_record = seal::seal(&acl_ks, &did).await?;
store.persist().await?;
eprintln!();
eprintln!("=== VTA Bootstrapped and Sealed ===");
eprintln!();
eprintln!(" Admin DID: {}", did);
eprintln!(" Sealed at: {}", seal_record.sealed_at.format("%Y-%m-%d %H:%M:%S UTC"));
eprintln!();
eprintln!(" The VTA is now sealed. Offline CLI commands that modify state are disabled.");
eprintln!(" All management must go through the authenticated REST API or DIDComm.");
eprintln!();
eprintln!(" To start the VTA server:");
eprintln!(" vta --config config.toml");
eprintln!();
Ok(())
}
async fn export_admin(config_path: Option<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfig::load(config_path)?;
let store = store::Store::open(&config.store)?;
let acl_ks = store.keyspace("acl")?;
let keys_ks = store.keyspace("keys")?;
let seed_store = create_seed_store(&config)?;
let vta_did = config.vta_did.as_deref().unwrap_or("(not set)");
let entries = acl::list_acl_entries(&acl_ks).await?;
let admins: Vec<_> = entries
.iter()
.filter(|e| e.role == acl::Role::Admin)
.collect();
if admins.is_empty() {
eprintln!("No admin entries found in ACL.");
return Ok(());
}
eprintln!("VTA DID: {vta_did}");
if let Some(msg) = &config.messaging {
eprintln!("Mediator DID: {}", msg.mediator_did);
}
eprintln!();
for admin in &admins {
eprintln!("Admin DID: {}", admin.did);
if let Some(label) = &admin.label {
eprintln!(" Label: {label}");
}
if admin.did.starts_with("did:key:") {
match reconstruct_credential(&*seed_store, &admin.did, vta_did, &keys_ks).await {
Ok(credential) => {
eprintln!();
eprintln!(" Credential:");
eprintln!(" {credential}");
}
Err(e) => {
eprintln!(" Could not reconstruct credential: {e}");
}
}
}
eprintln!();
}
Ok(())
}
async fn reconstruct_credential(
seed_store: &dyn keys::seed_store::SeedStore,
admin_did: &str,
vta_did: &str,
keys_ks: &store::KeyspaceHandle,
) -> Result<String, Box<dyn std::error::Error>> {
let multibase_pubkey = admin_did.strip_prefix("did:key:").unwrap();
let key_id = format!("{admin_did}#{multibase_pubkey}");
let record: keys::KeyRecord = keys_ks
.get(keys::store_key(&key_id))
.await?
.ok_or("admin key record not found in store")?;
let seed = load_seed_bytes(keys_ks, seed_store, record.seed_id).await?;
let root = ExtendedSigningKey::from_seed(&seed)
.map_err(|e| format!("failed to create BIP-32 root key: {e}"))?;
let derivation_path: DerivationPath = record
.derivation_path
.parse()
.map_err(|e| format!("invalid derivation path: {e}"))?;
let derived = root
.derive(&derivation_path)
.map_err(|e| format!("key derivation failed: {e}"))?;
let signing_key = SigningKey::from_bytes(derived.signing_key.as_bytes());
let private_key_multibase = multibase::encode(Base::Base58Btc, signing_key.as_bytes());
let bundle = serde_json::json!({
"did": admin_did,
"privateKeyMultibase": private_key_multibase,
"vtaDid": vta_did,
});
let bundle_json = serde_json::to_string(&bundle)?;
Ok(BASE64.encode(bundle_json.as_bytes()))
}