use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use auths_crypto::TypedSignerKey;
use auths_keri::{
IssuedCert, KeyState, QuicLoopbackOutcome, TlsCertError, TlsKeyAuthorizer, TrustedKel,
extract_aid_from_san, issue_authorized_kel_rooted_cert,
issue_authorized_kel_rooted_cert_with_key, issue_kel_rooted_cert,
issue_kel_rooted_cert_with_key, parse_kel_json, quic_loopback_compose,
verify_authorized_against_key_state,
};
use auths_utils::path::expand_tilde;
use clap::{Parser, Subcommand};
use crate::config::CliConfig;
struct SignerKeyAuthorizer {
signer: TypedSignerKey,
key_index: usize,
}
impl TlsKeyAuthorizer for SignerKeyAuthorizer {
fn current_key_index(&self) -> usize {
self.key_index
}
fn sign_tls_key(&self, spki_der: &[u8]) -> Result<Vec<u8>, TlsCertError> {
self.signer
.sign(spki_der)
.map_err(|e| TlsCertError::Generate(format!("authorize TLS key: {e}")))
}
}
fn load_signer(key_path: &Path) -> Result<TypedSignerKey> {
let path = expand_tilde(key_path)?;
let pem = std::fs::read_to_string(&path)
.map_err(|e| anyhow!("read signing key {}: {e}", path.display()))?;
let (_, der) = pkcs8::SecretDocument::from_pem(&pem)
.map_err(|e| anyhow!("parse signing key PEM {}: {e}", path.display()))?;
TypedSignerKey::from_pkcs8(der.as_bytes())
.map_err(|e| anyhow!("load signing key {}: {e}", path.display()))
}
#[derive(Parser, Debug, Clone)]
#[command(
about = "Issue/verify a KEL-rooted X.509 cert, read its did:keri subjectAltName, and carry it over TLS or QUIC/HTTP3 — an auths identity that stock TLS stacks (rustls/openssl/go) handshake with",
after_help = "Examples:
auths tls-cert issue --from-kel kel.json --sign-key aid.key.pem --san localhost # AID-authorized leaf
auths tls-cert issue --from-kel kel.json --sign-key aid.key.pem --out leaf # writes leaf.cert.pem + leaf.key.pem
auths tls-cert identity --cert leaf.cert.pem # read the did:keri AID out of the SAN
auths tls-cert verify --cert leaf.cert.pem --from-kel kel.json # adversarial: rejects forged/revoked/stripped
auths tls-cert quic --from-kel kel.json # carry the leaf + channel binding over QUIC/HTTP3"
)]
pub struct TlsCertCommand {
#[command(subcommand)]
pub action: TlsCertAction,
}
#[derive(Subcommand, Debug, Clone)]
pub enum TlsCertAction {
Issue(IssueArgs),
Identity(IdentityArgs),
Verify(VerifyArgs),
Quic(QuicArgs),
}
#[derive(Parser, Debug, Clone)]
pub struct IssueArgs {
#[clap(long, value_name = "KEL.json")]
pub from_kel: PathBuf,
#[clap(long = "san", value_name = "HOST")]
pub sans: Vec<String>,
#[clap(long, value_name = "KEY.pem")]
pub tls_key: Option<PathBuf>,
#[clap(long, value_name = "AID-KEY.pem")]
pub sign_key: Option<PathBuf>,
#[clap(long, default_value_t = 0, value_name = "N")]
pub sign_key_index: usize,
#[clap(long, value_name = "PREFIX")]
pub out: Option<PathBuf>,
}
#[derive(Parser, Debug, Clone)]
pub struct IdentityArgs {
#[clap(long, value_name = "CERT.pem")]
pub cert: PathBuf,
}
#[derive(Parser, Debug, Clone)]
pub struct VerifyArgs {
#[clap(long, value_name = "CERT.pem")]
pub cert: PathBuf,
#[clap(long, value_name = "KEL.json")]
pub from_kel: PathBuf,
}
#[derive(Parser, Debug, Clone)]
pub struct QuicArgs {
#[clap(long, value_name = "KEL.json")]
pub from_kel: PathBuf,
}
impl TlsCertCommand {
pub fn execute(&self, _ctx: &CliConfig) -> Result<()> {
match &self.action {
TlsCertAction::Issue(args) => args.run(),
TlsCertAction::Identity(args) => args.run(),
TlsCertAction::Verify(args) => args.run(),
TlsCertAction::Quic(args) => args.run(),
}
}
}
fn replay_kel(kel_path: &Path) -> Result<auths_keri::KeyState> {
let path = expand_tilde(kel_path)?;
let json =
std::fs::read_to_string(&path).map_err(|e| anyhow!("read KEL {}: {e}", path.display()))?;
let events = parse_kel_json(&json).map_err(|e| anyhow!("parse KEL: {e}"))?;
TrustedKel::from_trusted_source(&events)
.replay()
.map_err(|e| anyhow!("replay KEL: {e}"))
}
impl IssueArgs {
fn run(&self) -> Result<()> {
let state = replay_kel(&self.from_kel)?;
let issued = self.issue(&state)?;
match &self.out {
Some(prefix) => {
let cert_path = with_suffix(prefix, "cert.pem");
let key_path = with_suffix(prefix, "key.pem");
std::fs::write(&cert_path, issued.cert_pem.as_bytes())
.map_err(|e| anyhow!("write {}: {e}", cert_path.display()))?;
write_private_key(&key_path, &issued.key_pem)?;
println!(
"issued KEL-rooted leaf for {}\n cert: {}\n key: {}",
issued.binding.did_keri(),
cert_path.display(),
key_path.display()
);
}
None => {
print!("{}", issued.cert_pem);
}
}
Ok(())
}
fn issue(&self, state: &KeyState) -> Result<auths_keri::IssuedCert> {
let tls_key_pem = match &self.tls_key {
Some(key_path) => {
let path = expand_tilde(key_path)?;
Some(
std::fs::read_to_string(&path)
.map_err(|e| anyhow!("read TLS key {}: {e}", path.display()))?,
)
}
None => None,
};
match (&self.sign_key, tls_key_pem) {
(Some(sign_key_path), Some(tls_pem)) => {
let signer = load_signer(sign_key_path)?;
let authorizer = SignerKeyAuthorizer {
signer,
key_index: self.sign_key_index,
};
issue_authorized_kel_rooted_cert_with_key(state, &authorizer, &tls_pem, &self.sans)
.map_err(|e| anyhow!("issue authorized KEL-rooted cert: {e}"))
}
(Some(sign_key_path), None) => {
let signer = load_signer(sign_key_path)?;
let authorizer = SignerKeyAuthorizer {
signer,
key_index: self.sign_key_index,
};
issue_authorized_kel_rooted_cert(state, &authorizer, &self.sans)
.map_err(|e| anyhow!("issue authorized KEL-rooted cert: {e}"))
}
(None, Some(tls_pem)) => issue_kel_rooted_cert_with_key(state, &tls_pem, &self.sans)
.map_err(|e| anyhow!("issue KEL-rooted cert: {e}")),
(None, None) => issue_kel_rooted_cert(state, &self.sans)
.map_err(|e| anyhow!("issue KEL-rooted cert: {e}")),
}
}
}
impl IdentityArgs {
fn run(&self) -> Result<()> {
let cert_path = expand_tilde(&self.cert)?;
let cert_pem = std::fs::read_to_string(&cert_path)
.map_err(|e| anyhow!("read cert {}: {e}", cert_path.display()))?;
let aid = extract_aid_from_san(&cert_pem)
.map_err(|e| anyhow!("read did:keri identity from cert SAN: {e}"))?;
println!(
"did:keri:{aid}\n AID: {aid}\n (the SAN names the identity; replay its KEL to root trust in the log)"
);
Ok(())
}
}
impl VerifyArgs {
fn run(&self) -> Result<()> {
let state = replay_kel(&self.from_kel)?;
let cert_path = expand_tilde(&self.cert)?;
let cert_pem = std::fs::read_to_string(&cert_path)
.map_err(|e| anyhow!("read cert {}: {e}", cert_path.display()))?;
let binding = verify_authorized_against_key_state(&cert_pem, &state)
.map_err(|e| anyhow!("certificate rejected: {e}"))?;
let authorized_by = binding
.tls_key_authorization
.as_ref()
.map(|a| a.key_index)
.unwrap_or_default();
println!(
"verified: certificate is rooted in the KEL and the AID authorized its TLS key\n did:keri: {}\n current keys: {}\n KEL tip: {}\n TLS key authorized by current key #{}",
binding.did_keri(),
binding.current_keys.join(", "),
binding.kel_tip,
authorized_by,
);
Ok(())
}
}
impl QuicArgs {
fn run(&self) -> Result<()> {
let state = replay_kel(&self.from_kel)?;
let leaf = issue_kel_rooted_cert(&state, &["localhost".to_string(), "::1".to_string()])
.map_err(|e| anyhow!("issue KEL-rooted leaf for QUIC: {e}"))?;
let outcome = run_quic_loopback(&leaf, &state)?;
println!(
"QUIC/HTTP3 composition verified — the KEL-rooted leaf and channel binding carry over QUIC\n did:keri: {}\n ALPN: h3 (HTTP/3)\n served leaf re-rooted in the replayed KEL: yes\n both endpoints derive the same channel binding (anti-relay): {}\n channel binding: {} ({} bytes, per-connection)",
outcome.did_keri,
if outcome.binding_agrees { "yes" } else { "no" },
outcome.channel_binding_hex,
outcome.channel_binding_len,
);
Ok(())
}
}
fn run_quic_loopback(leaf: &IssuedCert, state: &KeyState) -> Result<QuicLoopbackOutcome> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| anyhow!("build QUIC runtime: {e}"))?;
rt.block_on(quic_loopback_compose(&leaf.cert_pem, &leaf.key_pem, state))
.map_err(|e| anyhow!("QUIC composition failed: {e}"))
}
fn with_suffix(prefix: &Path, suffix: &str) -> PathBuf {
let name = prefix
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
prefix.with_file_name(format!("{name}.{suffix}"))
}
fn write_private_key(path: &Path, pem: &str) -> Result<()> {
std::fs::write(path, pem.as_bytes()).map_err(|e| anyhow!("write {}: {e}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(path, perms)
.map_err(|e| anyhow!("chmod {}: {e}", path.display()))?;
}
Ok(())
}