#[macro_use]
extern crate clap;
use std::fs;
use std::io::{stdin, Read};
use std::path::PathBuf;
use std::str::FromStr;
use chrono::{DateTime, Utc};
use clap::Parser;
use ssi::{Algo, Chain, InvalidSig, Ssi, SsiCert, SsiQuery, SsiRuntime, SsiSecret, Uid};
#[derive(Parser, Clone, Debug)]
pub struct Args {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Clone, Debug)]
pub enum Command {
New {
#[clap(short, long, default_value = "ed25519")]
algo: Algo,
#[clap(short, long, default_value = "bitcoin")]
chain: Chain,
#[clap(long)]
prefix: Option<String>,
#[clap(short, long, requires = "prefix", default_value = "8")]
threads: u8,
#[clap(long, required = true)]
uid: Vec<String>,
#[clap(long, required_unless_present = "expiry")]
no_expiry: bool,
#[clap(conflicts_with = "no_expiry", required_unless_present = "no_expiry")]
expiry: Option<String>,
},
List {
#[clap(short, long)]
signing: bool,
},
Sign {
#[clap(long)]
full: bool,
#[clap(short, long, conflicts_with = "file")]
text: Option<String>,
#[clap(short, long)]
file: Option<PathBuf>,
ssi: SsiQuery,
},
Verify {
signature: SsiCert,
},
}
fn main() {
let args = Args::parse();
let mut runtime = SsiRuntime::load().expect("unable to load data");
match args.command {
Command::List { signing } => {
let now = Utc::now();
for ssi in &runtime.identities {
if signing && !runtime.is_signing(ssi.pk.fingerprint()) {
continue;
}
print!("{}\t", ssi.pk);
match ssi.expiry {
None => print!("no expiry"),
Some(e) => print!("{}", e.format("%Y-%m-%d")),
}
print!("\t");
match ssi.check_integrity() {
Ok(_) if ssi.expiry >= Some(now) => println!("expired"),
Ok(_) => println!("valid"),
Err(InvalidSig::InvalidPubkey) => println!("invalid pubkey"),
Err(InvalidSig::InvalidSig) => println!("invalid"),
Err(InvalidSig::InvalidData) => println!("broken"),
Err(InvalidSig::UnsupportedAlgo(_)) => println!("unsupported"),
}
for uid in &ssi.uids {
println!("\t{uid}");
}
}
println!();
}
Command::New {
algo,
chain,
prefix,
threads,
no_expiry: _,
expiry,
uid,
} => {
let expiry = expiry.map(|expiry| {
DateTime::parse_from_str(&expiry, "%Y-%m-%d")
.expect("invalid expiry date")
.to_utc()
});
let uids = uid
.iter()
.map(String::as_str)
.map(Uid::from_str)
.collect::<Result<_, _>>()
.expect("invalid UID");
let passwd = rpassword::prompt_password("Password for private key encryption: ")
.expect("unable to read password");
eprintln!("Generating new {algo} identity....");
let mut secret = match prefix {
Some(prefix) => SsiSecret::vanity(&prefix, algo, chain, threads),
None => SsiSecret::new(algo, chain),
};
let ssi = Ssi::new(uids, expiry, &secret);
println!("{ssi}");
if !passwd.is_empty() {
secret.encrypt(passwd);
}
runtime.secrets.insert(secret);
runtime.identities.insert(ssi);
runtime.store().expect("unable to save data");
}
Command::Sign {
full,
text,
file,
ssi,
} => {
eprintln!("Signing with {ssi} ...");
let passwd = rpassword::prompt_password("Password for private key encryption: ")
.expect("unable to read password");
let msg = match (text, file) {
(Some(t), None) => t.into_bytes(),
(None, Some(f)) => fs::read(f).expect("unable to read the file"),
(None, None) => {
let mut s = String::new();
stdin()
.read_to_string(&mut s)
.expect("unable to read standard input");
s.into_bytes()
}
_ => unreachable!(),
};
let signer = runtime
.find_signer(ssi, &passwd)
.expect("unknown signing identity");
eprintln!("Using key {signer}");
let cert = signer.sign(msg);
if full {
println!("{cert:#}");
} else {
println!("{cert}");
}
}
Command::Verify { signature } => {
eprint!("Verifying signature for message digest {} ... ", signature.msg);
let pk = runtime
.find_identity(signature.fp)
.map(|ssi| ssi.pk)
.or(signature.pk)
.expect("unknown signing identity");
match pk.verify(signature.msg.to_byte_array(), signature.sig) {
Ok(_) => eprintln!("valid"),
Err(err) => eprintln!("invalid: {err}"),
}
println!();
}
}
}