use std::path::PathBuf;
use std::time::Duration;
use clap::{Parser, Subcommand};
use okami::delegation::{Capability, DelegationChain, DelegationToken};
use okami::identity::{load_signing_key, save_signing_key, AgentIdentity, SpiffeId};
#[derive(Debug, Parser)]
#[command(
name = "okami",
about = "Post-quantum cryptographic identity for AI agents",
version
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
Init {
#[arg(long)]
trust_domain: String,
},
Keygen {
#[arg(long)]
trust_domain: String,
#[arg(long)]
workload: String,
#[arg(long, default_value = ".")]
output: PathBuf,
},
Inspect {
#[arg(long)]
credential: PathBuf,
},
Delegate {
#[arg(long)]
from: PathBuf,
#[arg(long)]
to: String,
#[arg(long)]
scopes: String,
#[arg(long, default_value = "3600")]
expiry: u64,
#[arg(long)]
chain: Option<PathBuf>,
#[arg(long)]
output: Option<PathBuf>,
},
VerifyChain {
#[arg(long)]
chain: PathBuf,
},
Tree {
#[arg(long)]
chain: PathBuf,
},
}
fn signing_key_path(dir: &std::path::Path) -> PathBuf {
dir.join("signing.key")
}
fn credential_path(dir: &std::path::Path) -> PathBuf {
dir.join("credential.bin")
}
fn config_path(dir: &std::path::Path) -> PathBuf {
dir.join("config.toml")
}
fn cmd_init(trust_domain: &str) -> anyhow::Result<()> {
let okami_dir = std::path::Path::new(".okami");
if okami_dir.exists() {
return Err(okami::Error::AlreadyInitialized.into());
}
std::fs::create_dir(okami_dir)?;
let identity = AgentIdentity::new(trust_domain, "root")?;
let key_bytes = identity.signing_key_bytes();
let cred_bytes = identity.credential().to_bytes()?;
save_signing_key(&signing_key_path(okami_dir), &key_bytes)?;
std::fs::write(credential_path(okami_dir), &cred_bytes)?;
let config = format!(
"# Okami workspace configuration\ntrust_domain = {:?}\nspiffe_id = {:?}\n",
trust_domain,
identity.spiffe_id().as_str()
);
std::fs::write(config_path(okami_dir), config)?;
println!("Initialized .okami/ workspace");
println!(" Trust domain : {trust_domain}");
println!(" SPIFFE ID : {}", identity.spiffe_id());
println!(" Signing key : .okami/signing.key (mode 0600)");
println!(" Credential : .okami/credential.bin");
Ok(())
}
fn cmd_keygen(trust_domain: &str, workload: &str, output: &std::path::Path) -> anyhow::Result<()> {
std::fs::create_dir_all(output)?;
let identity = AgentIdentity::new(trust_domain, workload)?;
let key_bytes = identity.signing_key_bytes();
let cred_bytes = identity.credential().to_bytes()?;
save_signing_key(&signing_key_path(output), &key_bytes)?;
std::fs::write(credential_path(output), &cred_bytes)?;
println!("Generated keypair");
println!(" SPIFFE ID : {}", identity.spiffe_id());
println!(" Signing key: {}", signing_key_path(output).display());
println!(" Credential : {}", credential_path(output).display());
Ok(())
}
fn cmd_inspect(credential_path: &std::path::Path) -> anyhow::Result<()> {
let bytes = std::fs::read(credential_path)?;
let cred = okami::identity::PqcCredential::from_bytes(&bytes)?;
println!("PQC Credential");
println!(" SPIFFE ID : {}", cred.spiffe_id);
println!(
" Algorithm : v{} (hybrid Ed25519+ML-DSA-65)",
cred.algo
);
println!(" Created at : {}", cred.created_at);
println!(" Expires at : {}", cred.expires_at);
println!(
" Expired : {}",
if cred.is_expired() { "YES" } else { "no" }
);
println!(
" Verifying key : {} bytes",
cred.verifying_key_bytes.len()
);
Ok(())
}
fn cmd_delegate(
from_dir: &std::path::Path,
to_spiffe: &str,
scopes_str: &str,
expiry_secs: u64,
existing_chain: Option<&std::path::Path>,
output: Option<&std::path::Path>,
) -> anyhow::Result<()> {
let sk_path = signing_key_path(from_dir);
let cred_path = credential_path(from_dir);
let key_bytes = load_signing_key(&sk_path)?;
let cred_bytes = std::fs::read(&cred_path)?;
let cred = okami::identity::PqcCredential::from_bytes(&cred_bytes)?;
let issuer = AgentIdentity::from_stored(cred, &key_bytes)?;
let subject_id = SpiffeId::parse(to_spiffe)?;
let scopes: Vec<Capability> = scopes_str
.split(',')
.filter(|s| !s.trim().is_empty())
.map(|s| Capability::new(s.trim()))
.collect::<Result<_, _>>()?;
let existing: Option<DelegationChain> = existing_chain
.map(|p| {
let bytes = std::fs::read(p)?;
DelegationChain::from_bytes(&bytes).map_err(anyhow::Error::from)
})
.transpose()?;
let issuer_scopes: Vec<Capability> = match &existing {
Some(chain) => chain.leaf().map(|t| t.scopes.clone()).unwrap_or_default(),
None => scopes.clone(),
};
let parent: Option<&DelegationToken> = existing.as_ref().and_then(|c| c.leaf());
let token = DelegationToken::issue(
&issuer,
subject_id,
scopes,
&issuer_scopes,
Duration::from_secs(expiry_secs),
parent,
)?;
let mut chain_tokens: Vec<DelegationToken> = existing.map(|c| c.tokens).unwrap_or_default();
chain_tokens.push(token);
let chain = DelegationChain::new(chain_tokens);
let chain_bytes = chain.to_bytes()?;
match output {
Some(path) => {
std::fs::write(path, &chain_bytes)?;
println!("Chain written to {}", path.display());
}
None => {
println!("{}", hex::encode(&chain_bytes));
}
}
Ok(())
}
fn cmd_verify_chain(chain_path: &std::path::Path) -> anyhow::Result<()> {
let bytes = std::fs::read(chain_path)?;
let chain = DelegationChain::from_bytes(&bytes)?;
match chain.verify(None) {
Ok(()) => {
println!("Chain VALID");
println!(" Links : {}", chain.tokens.len());
let scopes: Vec<&str> = chain
.effective_scopes()
.iter()
.map(|s| s.as_str())
.collect();
println!(" Scopes : {}", scopes.join(", "));
}
Err(e) => {
eprintln!("Chain INVALID: {e}");
std::process::exit(1);
}
}
Ok(())
}
fn cmd_tree(chain_path: &std::path::Path) -> anyhow::Result<()> {
let bytes = std::fs::read(chain_path)?;
let chain = DelegationChain::from_bytes(&bytes)?;
print!("{}", chain.ascii_tree());
Ok(())
}
const CLI_STACK_SIZE: usize = 32 * 1024 * 1024;
fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
match &cli.command {
Commands::Init { trust_domain } => cmd_init(trust_domain),
Commands::Keygen {
trust_domain,
workload,
output,
} => cmd_keygen(trust_domain, workload, output),
Commands::Inspect { credential } => cmd_inspect(credential),
Commands::Delegate {
from,
to,
scopes,
expiry,
chain,
output,
} => cmd_delegate(
from,
to,
scopes,
*expiry,
chain.as_deref(),
output.as_deref(),
),
Commands::VerifyChain { chain } => cmd_verify_chain(chain),
Commands::Tree { chain } => cmd_tree(chain),
}
}
fn main() {
let handle = std::thread::Builder::new()
.stack_size(CLI_STACK_SIZE)
.spawn(run)
.expect("failed to spawn CLI worker thread");
let result = match handle.join() {
Ok(r) => r,
Err(_) => {
eprintln!("error: CLI worker thread panicked");
std::process::exit(1);
}
};
if let Err(e) = result {
eprintln!("error: {e}");
std::process::exit(1);
}
}