diff --git a/src/baid64.rs b/src/baid64.rs index ac95967..61b87e1 100644 --- a/src/baid64.rs +++ b/src/baid64.rs @@ -232,8 +232,8 @@ impl Display for Baid64Display { if self.chunking { let bytes = s.as_bytes(); - f.write_str(&String::from_utf8_lossy(&bytes[..6]))?; - for chunk in bytes[6..].chunks(8) { + f.write_str(&String::from_utf8_lossy(&bytes[..8]))?; + for chunk in bytes[8..].chunks(7) { write!(f, "-{}", &String::from_utf8_lossy(chunk))?; } } else { diff --git a/src/bip340.rs b/src/bip340.rs index 0a4f042..8082961 100644 --- a/src/bip340.rs +++ b/src/bip340.rs @@ -47,7 +47,7 @@ impl Hash for Bip340Secret { } impl DisplayBaid64 for Bip340Secret { - const HRI: &'static str = "ssi:bip340-priv"; + const HRI: &'static str = "bip340-priv"; const CHUNKING: bool = false; const PREFIX: bool = true; const EMBED_CHECKSUM: bool = true; diff --git a/src/ed25519.rs b/src/ed25519.rs index 5d6a313..4995382 100644 --- a/src/ed25519.rs +++ b/src/ed25519.rs @@ -47,7 +47,7 @@ impl Hash for Ed25519Secret { } impl DisplayBaid64<64> for Ed25519Secret { - const HRI: &'static str = "ssi:ed25519-priv"; + const HRI: &'static str = "ed25519-priv"; const CHUNKING: bool = false; const PREFIX: bool = true; const EMBED_CHECKSUM: bool = true; diff --git a/src/identity.rs b/src/identity.rs index 27ecf4f..1e950bd 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -44,7 +44,7 @@ pub enum UidParseError { } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] -#[display("{name} <{schema}:{id}>")] +#[display("{name} <{schema}:{id}>", alt = "{name} {schema}:{id}")] pub struct Uid { pub name: String, pub schema: String, diff --git a/src/lib.rs b/src/lib.rs index 9c952cc..282d332 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,8 @@ pub use bip340::Bip340Secret; pub use ed25519::Ed25519Secret; pub use identity::{Ssi, SsiParseError, Uid}; pub use public::{ - Algo, Chain, InvalidPubkey, InvalidSig, SsiPub, SsiSig, UnknownAlgo, UnknownChain, + Algo, Chain, Fingerprint, InvalidPubkey, InvalidSig, SsiCert, SsiPub, SsiQuery, SsiSig, + UnknownAlgo, UnknownChain, }; pub use runtime::{Error, SsiRuntime, SSI_DIR}; -pub use secret::SsiSecret; +pub use secret::{SecretParseError, SsiPair, SsiSecret}; diff --git a/src/main.rs b/src/main.rs index 273681d..133e6a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,11 +22,14 @@ #[macro_use] extern crate clap; +use std::fs; +use std::io::{stdin, Read}; +use std::path::PathBuf; use std::str::FromStr; -use chrono::DateTime; +use chrono::{DateTime, Utc}; use clap::Parser; -use ssi::{Algo, Chain, Ssi, SsiRuntime, SsiSecret, Uid}; +use ssi::{Algo, Chain, InvalidSig, Ssi, SsiQuery, SsiRuntime, SsiSecret, Uid}; #[derive(Parser, Clone, Debug)] pub struct Args { @@ -37,15 +40,17 @@ pub struct Args { #[derive(Subcommand, Clone, Debug)] pub enum Command { - /// Generate a new identity - a pair of public and private keys. + /// Generate a new identity - a pair of public and private keys New { + /// Signature algorithm to use #[clap(short, long, default_value = "ed25519")] algo: Algo, + /// Which blockchain should be used for key revocation #[clap(short, long, default_value = "bitcoin")] chain: Chain, - /// Vanity prefix + /// Vanity prefix: "mine" an identity starting with certain string #[clap(long)] prefix: Option, @@ -53,21 +58,80 @@ pub enum Command { #[clap(short, long, requires = "prefix", default_value = "8")] threads: u8, + /// User identity information in form of "Name Surname ... + /// " #[clap(long, required = true)] uid: Vec, + /// Create identity with no specific expiration date #[clap(long, required_unless_present = "expiry")] no_expiry: bool, + /// Set expiration date for the identity (in YYYY-MM-DD format) #[clap(conflicts_with = "no_expiry", required_unless_present = "no_expiry")] expiry: Option, }, + + List { + /// List only signing identities + #[clap(short, long)] + signing: bool, + }, + + /// Sign a file or a message + Sign { + /// Text message to sign + #[clap(short, long, conflicts_with = "file")] + text: Option, + + /// File to create a detached signature for + #[clap(short, long)] + file: Option, + + /// Identity to use for the signature + ssi: SsiQuery, + }, + /* + Verify { + /// Signature certificate to 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, @@ -89,8 +153,6 @@ fn main() { .collect::>() .expect("invalid UID"); - let mut runtime = SsiRuntime::load().expect("unable to load data"); - let passwd = rpassword::prompt_password("Password for private key encryption: ") .expect("unable to read password"); @@ -112,5 +174,30 @@ fn main() { runtime.store().expect("unable to save data"); } + + Command::Sign { 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, + (None, Some(f)) => fs::read_to_string(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 + } + _ => unreachable!(), + }; + let signer = runtime + .find_signer(ssi, &passwd) + .expect("unknown signing identity"); + eprintln!("Using key {signer})"); + let cert = signer.sign(msg); + println!("{cert}"); + } } } diff --git a/src/public.rs b/src/public.rs index a5a7e3c..9ee3ef0 100644 --- a/src/public.rs +++ b/src/public.rs @@ -23,7 +23,7 @@ use std::fmt::{self, Display, Formatter}; use std::hash::Hash; use std::str::FromStr; -use amplify::{Bytes, Display}; +use amplify::{Bytes, Bytes32, Display}; use crate::baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str}; @@ -105,7 +105,7 @@ impl FromStr for Chain { impl From for u8 { fn from(chain: Chain) -> Self { match chain { - Chain::Bitcoin => 0xBC, + Chain::Bitcoin => 0xB7, Chain::Liquid => 0x10, Chain::Other(v) => v, } @@ -115,7 +115,7 @@ impl From for u8 { impl From for Chain { fn from(value: u8) -> Self { match value { - 0xBC => Chain::Bitcoin, + 0xB7 => Chain::Bitcoin, 0x10 => Chain::Liquid, n => Chain::Other(n), } @@ -163,6 +163,10 @@ impl SsiPub { } } + pub fn fingerprint(self) -> Fingerprint { + Fingerprint([self.key[0], self.key[1], self.key[2], self.key[3], self.key[4], self.key[5]]) + } + pub fn to_byte_array(&self) -> [u8; 32] { let mut buf = [0u8; 32]; buf[0..30].copy_from_slice(self.key.as_slice()); @@ -173,7 +177,13 @@ impl SsiPub { } impl Display for SsiPub { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) } + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if !f.alternate() { + self.fmt_baid64(f) + } else { + write!(f, "{}", self.fingerprint()) + } + } } impl FromStr for SsiPub { @@ -225,3 +235,66 @@ pub enum InvalidSig { /// can't verify signature - unsupported signature method {0}. UnsupportedAlgo(u8), } + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, From)] +#[display(inner)] +pub enum SsiQuery { + #[from] + Pub(SsiPub), + #[from] + Fp(Fingerprint), + #[from] + Id(String), +} + +impl FromStr for SsiQuery { + type Err = Baid64ParseError; + + fn from_str(s: &str) -> Result { + if s.len() == 8 { + Fingerprint::from_str(s).map(Self::Fp) + } else if s.starts_with("ssi:") || (s.contains('-') && (s.len() == 48 || s.len() == 52)) { + SsiPub::from_str(s).map(Self::Pub) + } else { + Ok(SsiQuery::Id(s.to_owned())) + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, From)] +pub struct Fingerprint([u8; 6]); + +impl DisplayBaid64<6> for Fingerprint { + const HRI: &'static str = ""; + const CHUNKING: bool = false; + const PREFIX: bool = false; + const EMBED_CHECKSUM: bool = false; + const MNEMONIC: bool = false; + + fn to_baid64_payload(&self) -> [u8; 6] { self.0 } +} + +impl FromBaid64Str<6> for Fingerprint {} + +impl Display for Fingerprint { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) } +} + +impl FromStr for Fingerprint { + type Err = Baid64ParseError; + + fn from_str(s: &str) -> Result { Self::from_baid64_str(s) } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct SsiCert { + pub fp: Fingerprint, + pub msg: Bytes32, + pub sig: SsiSig, +} + +impl Display for SsiCert { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ssi:{fp}?msg={msg}&sig={sig}", fp = self.fp, msg = self.msg, sig = self.sig) + } +} diff --git a/src/runtime.rs b/src/runtime.rs index 5d937ba..c379266 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -27,7 +27,7 @@ use std::io::{self, BufRead, Write}; use std::path::PathBuf; use crate::baid64::Baid64ParseError; -use crate::{Ssi, SsiParseError, SsiSecret}; +use crate::{Fingerprint, SecretParseError, Ssi, SsiPair, SsiParseError, SsiQuery, SsiSecret}; #[derive(Debug, Display, Error, From)] #[display(inner)] @@ -38,6 +38,9 @@ pub enum Error { #[from] Baid64(Baid64ParseError), + #[from] + Secret(SecretParseError), + #[from] Ssi(SsiParseError), } @@ -107,5 +110,37 @@ impl SsiRuntime { Ok(()) } - pub fn identities(&self) -> impl Iterator { self.identities.iter() } + pub fn find_identity(&self, query: impl Into) -> Option<&Ssi> { + let query = query.into(); + self.identities.iter().find(|ssi| match query { + SsiQuery::Pub(pk) => ssi.pk == pk, + SsiQuery::Fp(fp) => ssi.pk.fingerprint() == fp, + SsiQuery::Id(ref id) => ssi.uids.iter().any(|uid| { + &uid.id == id || + &uid.to_string() == id || + &uid.name == id || + &format!("{}:{}", uid.schema, uid.id) == id + }), + }) + } + + pub fn find_signer(&self, query: impl Into, passwd: &str) -> Option { + let ssi = self.find_identity(query.into()).cloned()?; + let sk = self.secrets.iter().find_map(|s| { + let mut s = (*s).clone(); + if !passwd.is_empty() { + s.decrypt(passwd); + } + if s.to_public() == ssi.pk { + Some(s) + } else { + None + } + })?; + Some(SsiPair::new(ssi, sk)) + } + + pub fn is_signing(&self, fp: Fingerprint) -> bool { + self.secrets.iter().any(|s| s.fingerprint() == fp) + } } diff --git a/src/secret.rs b/src/secret.rs index 8e45032..6e59c95 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -19,34 +19,58 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::{self, Display, Formatter}; use std::hash::Hash; use std::str::FromStr; use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; use aes::{Aes256, Block}; +use amplify::Bytes32; +use chrono::{DateTime, Utc}; use sha2::{Digest, Sha256}; use crate::baid64::Baid64ParseError; -use crate::{Algo, Bip340Secret, Chain, Ed25519Secret, SsiPub, SsiSig}; +use crate::{Algo, Bip340Secret, Chain, Ed25519Secret, Fingerprint, Ssi, SsiCert, SsiPub, SsiSig}; -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Display, From)] -#[display(inner)] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum SsiSecret { + Bip340(Fingerprint, Bip340Secret), + Ed25519(Fingerprint, Ed25519Secret), +} + +#[derive(Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum SecretParseError { + /// incomplete private key data. + Incomplete, + /// invalid fingerprint data in private key. {0}. + InvalidFingerprint(Baid64ParseError), #[from] - Bip340(Bip340Secret), - #[from] - Ed25519(Ed25519Secret), + /// invalid secret key data. {0} + InvalidSecret(Baid64ParseError), } impl FromStr for SsiSecret { - type Err = Baid64ParseError; - - fn from_str(s: &str) -> Result { - if s.starts_with("ssi:bip340-priv") { - Bip340Secret::from_str(s).map(Self::Bip340) + type Err = SecretParseError; + + fn from_str(mut s: &str) -> Result { + s = s.trim_start_matches("ssi:"); + let (fp, sk) = s.split_once('/').ok_or(SecretParseError::Incomplete)?; + let fp = Fingerprint::from_str(fp).map_err(SecretParseError::InvalidFingerprint)?; + if sk.starts_with("bip340-priv") { + Ok(Self::Bip340(fp, Bip340Secret::from_str(sk)?)) } else { - Ed25519Secret::from_str(s).map(Self::Ed25519) + Ok(Self::Ed25519(fp, Ed25519Secret::from_str(sk)?)) + } + } +} + +impl Display for SsiSecret { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SsiSecret::Bip340(fp, sk) => write!(f, "{fp}/{sk}"), + SsiSecret::Ed25519(fp, sk) => write!(f, "{fp}/{sk}"), } } } @@ -60,9 +84,17 @@ impl SsiSecret { } } - pub fn new_ed25519(chain: Chain) -> Self { Self::Ed25519(Ed25519Secret::new(chain)) } + pub fn new_ed25519(chain: Chain) -> Self { + let sk = Ed25519Secret::new(chain); + let fp = sk.to_public().fingerprint(); + Self::Ed25519(fp, sk) + } - pub fn new_bip340(chain: Chain) -> Self { Self::Bip340(Bip340Secret::new(chain)) } + pub fn new_bip340(chain: Chain) -> Self { + let sk = Bip340Secret::new(chain); + let fp = sk.to_public().fingerprint(); + Self::Bip340(fp, sk) + } pub fn vanity(prefix: &str, algo: Algo, chain: Chain, threads: u8) -> Self { let (tx, rx) = crossbeam_channel::bounded(1); @@ -83,17 +115,30 @@ impl SsiSecret { rx.recv().expect("threading failed") } + pub fn algorithm(&self) -> Algo { + match self { + SsiSecret::Bip340(_, _) => Algo::Bip340, + SsiSecret::Ed25519(_, _) => Algo::Ed25519, + } + } + + pub fn fingerprint(&self) -> Fingerprint { + match self { + SsiSecret::Bip340(fp, _) | SsiSecret::Ed25519(fp, _) => *fp, + } + } + pub fn to_public(&self) -> SsiPub { match self { - SsiSecret::Bip340(sk) => sk.to_public(), - SsiSecret::Ed25519(sk) => sk.to_public(), + SsiSecret::Bip340(_, sk) => sk.to_public(), + SsiSecret::Ed25519(_, sk) => sk.to_public(), } } pub fn sign(&self, msg: [u8; 32]) -> SsiSig { match self { - SsiSecret::Bip340(sk) => sk.sign(msg), - SsiSecret::Ed25519(sk) => sk.sign(msg), + SsiSecret::Bip340(_, sk) => sk.sign(msg), + SsiSecret::Ed25519(_, sk) => sk.sign(msg), } } @@ -125,17 +170,17 @@ impl SsiSecret { pub fn to_vec(&self) -> Vec { match self { - SsiSecret::Bip340(sk) => sk.0.secret_bytes().to_vec(), - SsiSecret::Ed25519(sk) => sk.0.to_vec(), + SsiSecret::Bip340(_, sk) => sk.0.secret_bytes().to_vec(), + SsiSecret::Ed25519(_, sk) => sk.0.to_vec(), } } fn replace(&mut self, secret: &[u8]) { match self { - SsiSecret::Bip340(sk) => { + SsiSecret::Bip340(_, sk) => { sk.0 = secp256k1::SecretKey::from_slice(&secret).expect("same size") } - SsiSecret::Ed25519(sk) => { + SsiSecret::Ed25519(_, sk) => { sk.0 = ec25519::SecretKey::from_slice(&secret).expect("same size") } } @@ -145,3 +190,32 @@ impl SsiSecret { impl From for SsiPub { fn from(sk: SsiSecret) -> Self { sk.to_public() } } + +#[derive(Clone, Eq, PartialEq, Display)] +#[display("{pk}")] +pub struct SsiPair { + pub pk: SsiPub, + pub sk: SsiSecret, + pub expiry: Option>, +} + +impl SsiPair { + pub fn new(ssi: Ssi, sk: SsiSecret) -> Self { + SsiPair { + pk: ssi.pk, + sk, + expiry: ssi.expiry, + } + } + + pub fn sign(&self, msg: impl AsRef<[u8]>) -> SsiCert { + let msg = Sha256::digest(msg); + let digest = Sha256::digest(msg); + let sig = self.sk.sign(digest.into()); + SsiCert { + fp: self.pk.fingerprint(), + msg: Bytes32::from_byte_array(digest), + sig, + } + } +}