diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3e00d8b..b32c3839 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: strategy: fail-fast: false matrix: - toolchain: [ nightly, beta, stable ] + toolchain: [ nightly, beta, stable, 1.59.0 ] steps: - uses: actions/checkout@v2 - name: Install rust ${{ matrix.toolchain }} @@ -81,24 +81,6 @@ jobs: with: command: check args: --workspace --all-targets --all-features - toolchains-old: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - toolchain: [ 1.51.0 ] - steps: - - uses: actions/checkout@v2 - - name: Install rust ${{ matrix.toolchain }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - override: true - - name: All features - uses: actions-rs/cargo@v1 - with: - command: check - args: --workspace --all-targets --features all_msrv47 dependency: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index dc9afbc4..c5b3507f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,17 @@ dependencies = [ "toml", ] +[[package]] +name = "amplify" +version = "4.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb19b1bb6fe191f155406d8300d14bc9fa66f59bb8a8520d2a8015d6be80bf7d" +dependencies = [ + "amplify_derive", + "amplify_num", + "wasm-bindgen", +] + [[package]] name = "amplify_derive" version = "2.11.3" @@ -153,7 +164,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea66aa1dc492569f2a3ff224449ab29733854b2f591830fee53840c3b3ac4b" dependencies = [ - "amplify", + "amplify 3.13.0", "bitcoin 0.28.1", "secp256k1 0.22.1", "stability", @@ -254,12 +265,27 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorize" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc17e449bc7854c50b943d113a98bc0e01dc6585d2c66eaa09ca645ebd8a7e62" + [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "cxx" version = "1.0.81" @@ -354,7 +380,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2293d9a54744b73b753fad2e3c16295345504dc230696ee8aa5dbfd276ab4" dependencies = [ - "amplify", + "amplify 3.13.0", "proc-macro2", "quote", "syn", @@ -486,7 +512,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfcfa60726746c360fb68ad137ca83baa9670db38e97549454a6da86c8c8c05" dependencies = [ - "amplify", + "amplify 3.13.0", "bitcoin 0.28.1", "bitcoin_scripts", "chrono", @@ -524,26 +550,29 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lnpbp" -version = "0.9.0-alpha.1" +version = "0.9.0-alpha.2" dependencies = [ - "amplify", + "amplify 3.13.0", "base58", "base64-compat", "clap", + "colorize", "lnpbp_bech32", "lnpbp_chain", "lnpbp_elgamal", + "lnpbp_identity", "serde", "serde_json", "serde_with", "serde_yaml 0.9.14", + "strict_encoding 2.0.0-alpha.2", ] [[package]] name = "lnpbp_bech32" version = "0.9.0-alpha.1" dependencies = [ - "amplify", + "amplify 3.13.0", "bech32 0.9.1", "bitcoin_hashes 0.11.0", "deflate", @@ -557,7 +586,7 @@ dependencies = [ name = "lnpbp_chain" version = "0.9.0-alpha.1" dependencies = [ - "amplify", + "amplify 3.13.0", "bitcoin 0.29.2", "bitcoin_hashes 0.11.0", "lightning_encoding", @@ -572,9 +601,22 @@ dependencies = [ name = "lnpbp_elgamal" version = "0.9.0" dependencies = [ - "amplify", + "amplify 3.13.0", + "bitcoin_hashes 0.11.0", + "secp256k1 0.24.1", +] + +[[package]] +name = "lnpbp_identity" +version = "0.9.0-beta.1" +dependencies = [ + "amplify 4.0.0-alpha.1", + "bech32 0.9.1", "bitcoin_hashes 0.11.0", + "crc32fast", + "mnemonic", "secp256k1 0.24.1", + "strict_encoding 2.0.0-alpha.2", ] [[package]] @@ -586,6 +628,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mnemonic" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fae0e4c0b155d3b019a7cbc27abe4a90e15c06814d27889ce9f5f44e2faf77" +dependencies = [ + "byteorder", + "lazy_static", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -857,7 +909,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef7fccd0e9a81be0575f12845e93c2f5becaae168e63b43ab036bd80148aeb58" dependencies = [ - "amplify", + "amplify 3.13.0", "bitcoin 0.28.1", "bitcoin_hashes 0.10.0", "chrono", @@ -870,7 +922,7 @@ version = "2.0.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a0358463899b0a7edfb4e46e96b770ed6250726d247cbea386e9769c7f48d6" dependencies = [ - "amplify", + "amplify 3.13.0", "bitcoin 0.29.2", "bitcoin_hashes 0.11.0", "chrono", @@ -895,7 +947,7 @@ version = "2.0.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc108771f81d5c5d1c4f295b86b16453ce51aaf71718afc824f05ea0df9f3752" dependencies = [ - "amplify", + "amplify 3.13.0", "strict_encoding 2.0.0-alpha.2", ] diff --git a/Cargo.toml b/Cargo.toml index 79468aa6..dd85bb9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lnpbp" -version = "0.9.0-alpha.1" +version = "0.9.0-alpha.2" license = "MIT" authors = ["Dr. Maxim Orlovsky "] description = "LNP/BP Core Library implementing LNPBP specifications & standards" @@ -24,28 +24,31 @@ required-features = ["cli"] [dependencies] amplify = { version = "3.13.0", features = ["stringly_conversions", "std"] } +strict_encoding = "2.0.0-alpha.2" lnpbp_bech32 = { version = "0.9.0-alpha.1", path = "bech32" } lnpbp_chain = { version = "0.9.0-alpha.1", path = "chain" } lnpbp_elgamal = { version = "0.9.0", path = "elgamal", optional = true } +lnpbp_identity = { version = "0.9.0-beta.1", path = "identity", optional = true } serde_crate = { package = "serde", version = "1", features = ["derive"], optional = true } serde_with = { version = "1.8", features = ["hex"], optional = true } # serde_with_macros = { version = "~1.2.0", optional = true } # Fix for the problem in 1.3.0 -clap = { version = "~3.1.18", features = ["derive"], optional = true } -serde_yaml = { version = "0.9", optional = true } -serde_json = { version = "1", optional = true } +clap = { version = "~3.1.18", features = ["derive"], optional = true } # Used by cli only +serde_yaml = { version = "0.9", optional = true } # Used by cli only +serde_json = { version = "1", optional = true } # Used by cli only base64-compat = { version = "1", optional = true } # Used by cli only base58 = { version = "0.2", optional = true } # Used by cli only +colorize = { version = "0.1.0", optional = true } # Used by cli only [features] default = ["zip"] -all = ["serde", "elgamal", "zip", "cli"] -cli = ["clap", "serde", "base64-compat", "base58", "serde_yaml", "serde_json", "amplify/hex"] +all = ["serde", "elgamal", "identity", "zip", "cli"] +cli = ["clap", "serde", "identity", "base64-compat", "base58", "serde_yaml", "serde_json", "amplify/hex", "colorize"] serde = ["serde_crate", "serde_with", "amplify/serde", "lnpbp_bech32/serde", "lnpbp_chain/serde"] +identity = ["lnpbp_identity"] elgamal = ["lnpbp_elgamal"] # Provides ElGamal encryption module from this library zip = ["lnpbp_bech32/zip"] -all_msrv47 = ["serde", "elgamal", "zip"] # Used in testing against MSRV 1.47 [workspace] -members = [".", "bech32", "chain", "elgamal"] -default-members = [".", "bech32", "chain", "elgamal"] +members = [".", "bech32", "chain", "elgamal", "identity"] +default-members = [".", "bech32", "chain", "elgamal", "identity"] diff --git a/README.md b/README.md index a440d502..7c81a0d5 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The development of the libraries is supported by LNP/BP Standards Association. ### Clone and compile library -Minimum supported rust compiler version (MSRV): 1.51.0 (if command-line tool is not used) and 1.54.0 (otherwise). +Minimum supported rust compiler version (MSRV): 1.59.0. ```shell script git clone https://github.com/lnp-bp/rust-lnpbp diff --git a/chain/src/lib.rs b/chain/src/lib.rs index cf3ec7ab..dbe15e70 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -207,9 +207,7 @@ hash_newtype!( ); impl Default for AssetId { - fn default() -> Self { - AssetId::from_inner([0u8; 32]) - } + fn default() -> Self { AssetId::from_inner([0u8; 32]) } } impl strict_encoding::Strategy for AssetId { diff --git a/elgamal/src/lib.rs b/elgamal/src/lib.rs index 3a2004e8..126bc5bb 100644 --- a/elgamal/src/lib.rs +++ b/elgamal/src/lib.rs @@ -79,7 +79,8 @@ pub fn encrypt( let mut hash = sha256::Hash::hash(&encryption_key.serialize()); // Tweaking the encryption key with the blinding factor - let tweak = Scalar::from_be_bytes(blinding_key.secret_bytes()).expect("negligible probability"); + let tweak = Scalar::from_be_bytes(blinding_key.secret_bytes()) + .expect("negligible probability"); encryption_key = encryption_key.add_exp_tweak(context, &tweak)?; // Pad the message to the round number of 30-byte chunks with the generated @@ -152,7 +153,8 @@ pub fn decrypt( } // Tweak the encryption key with the blinding factor - let tweak = Scalar::from_be_bytes(decryption_key.secret_bytes()).expect("negligible probability"); + let tweak = Scalar::from_be_bytes(decryption_key.secret_bytes()) + .expect("negligible probability"); unblinding_key = unblinding_key.add_exp_tweak(context, &tweak)?; let encryption_key = unblinding_key; @@ -237,8 +239,10 @@ mod test { let uk = unblinding_key.clone(); let ek = encryption_key.clone(); - let tweak1 = Scalar::from_be_bytes(blinding_key.secret_bytes()).unwrap(); - let tweak2 = Scalar::from_be_bytes(decryption_key.secret_bytes()).unwrap(); + let tweak1 = + Scalar::from_be_bytes(blinding_key.secret_bytes()).unwrap(); + let tweak2 = + Scalar::from_be_bytes(decryption_key.secret_bytes()).unwrap(); assert_eq!( ek.add_exp_tweak(&SECP256K1, &tweak1).unwrap(), uk.add_exp_tweak(&SECP256K1, &tweak2).unwrap() diff --git a/identity/Cargo.toml b/identity/Cargo.toml new file mode 100644 index 00000000..60fa85ff --- /dev/null +++ b/identity/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "lnpbp_identity" +version = "0.9.0-beta.1" +license = "MIT" +authors = ["Dr. Maxim Orlovsky "] +description = "LNP/BP identity standards implementation" +repository = "https://github.com/LNP-BP/rust-lnpbp" +homepage = "https://github.com/LNP-BP/rust-lnpbp/tree/master/identity" +keywords = ["bitcoin", "lnp-bp", "identity", "ed25519", "secp256k1"] +categories = ["cryptography"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +amplify = "4.0.0-alpha.1" +secp256k1 = { version = "0.24.1", features = ["global-context", "rand-std"] } +strict_encoding = "2.0.0-alpha.2" +bech32 = "0.9.1" +crc32fast = "1.3.2" +mnemonic = "1.0.1" +bitcoin_hashes = "0.11.0" diff --git a/identity/src/lib.rs b/identity/src/lib.rs new file mode 100644 index 00000000..a33b0ef0 --- /dev/null +++ b/identity/src/lib.rs @@ -0,0 +1,629 @@ +// LNP/BP lLibraries implementing LNPBP specifications & standards +// Written in 2020 by +// Dr. Maxim Orlovsky +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to +// the public domain worldwide. This software is distributed without +// any warranty. +// +// You should have received a copy of the MIT License +// along with this software. +// If not, see . + +#[macro_use] +extern crate amplify; + +use std::fmt::{self, Debug, Display, Formatter}; +use std::io; +use std::io::{Read, Write}; +use std::str::FromStr; +use std::string::FromUtf8Error; + +use amplify::hex::ToHex; +use bech32::{FromBase32, ToBase32}; +use bitcoin_hashes::{sha256, sha256d, Hash}; +use secp256k1::{rand, Message, SECP256K1}; +use strict_encoding::{StrictDecode, StrictEncode}; + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error)] +#[display("unknown algorithm {0}")] +pub struct UnrecognizedAlgo(pub String); + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +pub enum CertError { + #[display("incorrect bech32(m) string due to {0}")] + #[from] + Bech32(bech32::Error), + + #[display( + "unrecognized certificate of `{0}` type; only `{1}1...` strings are \ + supported" + )] + InvalidHrp(String, &'static str), + + #[display("certificates require bech32m encoding")] + InvalidVariant, + + #[display("mnemonic guard does not match certificate nym {0}")] + InvalidMnemonic(String), + + #[display("provided certificate contains incomplete data")] + #[from(strict_encoding::Error)] + IncompleteData, + + #[display( + "certificate uses unknown cryptographic algorithm; try to update the \ + tool version" + )] + UnknownAlgo, + + #[display(inner)] + #[from] + Utf8(FromUtf8Error), +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +pub enum VerifyError { + #[display( + "signature algorithm does not match digital identity certificate" + )] + AlgoMismatch, + + #[display("invalid signature")] + #[from(secp256k1::Error)] + InvalidSig, +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] +#[derive(StrictEncode, StrictDecode)] +#[strict_encoding(by_value, repr = u8)] +#[repr(u8)] +pub enum HashAlgo { + #[display("sha256d")] + Sha256d = 2, +} + +impl HashAlgo { + #[allow(clippy::len_without_is_empty)] + pub fn len(self) -> u8 { + match self { + Self::Sha256d => 32, + } + } +} + +impl FromStr for HashAlgo { + type Err = UnrecognizedAlgo; + + fn from_str(s: &str) -> Result { + match s { + s if s == Self::Sha256d.to_string() => Ok(Self::Sha256d), + wrong => Err(UnrecognizedAlgo(wrong.to_owned())), + } + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] +#[derive(StrictEncode, StrictDecode)] +#[strict_encoding(by_value, repr = u8)] +#[repr(u8)] +pub enum EcAlgo { + #[display("bip340")] + Bip340 = 1, + + #[display("ed25519")] + Ed25519 = 2, +} + +impl EcAlgo { + pub fn cert_len(self) -> usize { 1 + self.pub_len() + self.sig_len() } + + pub fn prv_len(self) -> usize { + match self { + Self::Bip340 => 32, + Self::Ed25519 => 32, + } + } + + pub fn pub_len(self) -> usize { + match self { + Self::Bip340 => 32, + Self::Ed25519 => 32, + } + } + + pub fn sig_len(self) -> usize { + match self { + Self::Bip340 => 64, + Self::Ed25519 => 64, + } + } + + pub fn decode(code: u8) -> Option { + Some(match code { + x if x == Self::Bip340 as u8 => Self::Bip340, + x if x == Self::Ed25519 as u8 => Self::Ed25519, + _ => return None, + }) + } + + pub fn encode(self) -> u8 { self as u8 } +} + +impl FromStr for EcAlgo { + type Err = UnrecognizedAlgo; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "bip340" => Ok(Self::Bip340), + "ed25519" => Ok(Self::Ed25519), + wrong => Err(UnrecognizedAlgo(wrong.to_owned())), + } + } +} + +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct IdentitySigner { + pub cert: IdentityCert, + prvkey: Box<[u8]>, +} + +impl StrictEncode for IdentitySigner { + fn strict_encode( + &self, + mut e: E, + ) -> Result { + let len = self.cert.strict_encode(&mut e)?; + e.write_all(&self.prvkey)?; + Ok(len + self.cert.algo.prv_len()) + } +} + +impl StrictDecode for IdentitySigner { + fn strict_decode( + mut d: D, + ) -> Result { + let cert = IdentityCert::strict_decode(&mut d)?; + + let mut prvkey = vec![0u8; cert.algo.prv_len()]; + d.read_exact(&mut prvkey)?; + + secp256k1::SecretKey::from_slice(&prvkey).map_err(secp_to_sten_err)?; + + Ok(Self { + cert, + prvkey: Box::from(prvkey), + }) + } +} + +impl IdentitySigner { + pub fn new_bip340() -> Self { + let pair = secp256k1::KeyPair::new(SECP256K1, &mut rand::thread_rng()); + let cert = IdentityCert::from(pair); + Self { + cert, + prvkey: Box::from(pair.secret_key().secret_bytes()), + } + } + + pub fn sign(&self, msg: impl AsRef<[u8]>) -> SigCert { + match self.cert.algo { + EcAlgo::Bip340 => { + let sk = secp256k1::SecretKey::from_slice(&self.prvkey) + .expect("invalid private key"); + let pair = secp256k1::KeyPair::from_secret_key(SECP256K1, &sk); + let msg = + Message::from_hashed_data::(msg.as_ref()); + let sig = pair.sign_schnorr(msg); + SigCert { + hash: HashAlgo::Sha256d, + curve: EcAlgo::Bip340, + sig: Box::from(&sig[..]), + } + } + EcAlgo::Ed25519 => todo!("Ed25519 signatures"), + } + } + + pub fn sign_stream(&self, mut input: impl Read) -> io::Result { + match self.cert.algo { + EcAlgo::Bip340 => { + let sk = secp256k1::SecretKey::from_slice(&self.prvkey) + .expect("invalid private key"); + let pair = secp256k1::KeyPair::from_secret_key(SECP256K1, &sk); + let mut engine = sha256d::Hash::engine(); + let mut buf = [0u8; 64]; + loop { + let len = input.read(&mut buf)?; + if len == 0 { + break; + } + engine.write_all(&buf[..len])?; + } + let hash = sha256d::Hash::from_engine(engine); + let msg = Message::from_slice(&hash[..]).expect("hash"); + let sig = pair.sign_schnorr(msg); + Ok(SigCert { + hash: HashAlgo::Sha256d, + curve: EcAlgo::Bip340, + sig: Box::from(&sig[..]), + }) + } + EcAlgo::Ed25519 => todo!("Ed25519 signatures"), + } + } +} + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct IdentityCert { + algo: EcAlgo, + pubkey: Box<[u8]>, + sig: Box<[u8]>, +} + +impl IdentityCert { + pub fn nym(&self) -> String { + let mut mnemonic = Vec::with_capacity(64); + let mut crc32data = Vec::with_capacity(self.algo.cert_len() as usize); + crc32data.push(self.algo.encode()); + crc32data.extend(&*self.pubkey); + let crc32 = crc32fast::hash(&crc32data); + mnemonic::encode(crc32.to_be_bytes(), &mut mnemonic) + .expect("mnemonic encoding"); + + String::from_utf8(mnemonic) + .expect("mnemonic library error") + .replace('-', "_") + } + + pub fn fingerprint(&self) -> String { + let mut s = format!("{:#}", self); + let _ = s.split_off(6); + s + } +} + +impl StrictEncode for IdentityCert { + fn strict_encode( + &self, + mut e: E, + ) -> Result { + self.algo.strict_encode(&mut e)?; + e.write_all(&self.pubkey)?; + e.write_all(&self.sig)?; + Ok(self.algo.cert_len() as usize) + } +} + +impl StrictDecode for IdentityCert { + fn strict_decode( + mut d: D, + ) -> Result { + let algo = EcAlgo::strict_decode(&mut d)?; + + let mut pubkey = vec![0u8; algo.pub_len()]; + let mut sig = vec![0u8; algo.sig_len()]; + d.read_exact(&mut pubkey)?; + d.read_exact(&mut sig)?; + + secp256k1::XOnlyPublicKey::from_slice(&pubkey) + .map_err(secp_to_sten_err)?; + secp256k1::schnorr::Signature::from_slice(&sig) + .map_err(secp_to_sten_err)?; + + Ok(Self { + algo, + pubkey: Box::from(pubkey), + sig: Box::from(sig), + }) + } +} + +impl Debug for IdentityCert { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "nym {}", self.nym())?; + writeln!(f, "fgp {}", self.fingerprint())?; + writeln!(f, "crv {}", self.algo)?; + writeln!(f, "idk {}", bin_fmt(&self.pubkey))?; + writeln!(f, "sig {}", bin_fmt(&self.sig))?; + + Ok(()) + } +} + +impl Display for IdentityCert { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let data = self + .strict_serialize() + .expect("strict encoding of certificate"); + let s = + bech32::encode("crt", data.to_base32(), bech32::Variant::Bech32m) + .expect("bech32 encoding of certificate"); + + if f.alternate() { + f.write_str(&s[s.len() - 6..])?; + } else { + f.write_str(&s)?; + } + f.write_str("_")?; + f.write_str(&self.nym()) + } +} + +impl FromStr for IdentityCert { + type Err = CertError; + + fn from_str(s: &str) -> Result { + let (b32, mnem) = s.split_once('_').unwrap_or((s, "")); + let (hrp, encoded, variant) = bech32::decode(b32)?; + + if hrp != "crt" { + return Err(CertError::InvalidHrp(hrp, "crt")); + } + + if variant != bech32::Variant::Bech32m { + return Err(CertError::InvalidVariant); + } + + let data = Vec::::from_base32(&encoded)?; + + let cert = Self::strict_deserialize(data)?; + + let nym = cert.nym(); + if !mnem.is_empty() && cert.nym() != mnem { + return Err(CertError::InvalidMnemonic(nym)); + } + + Ok(cert) + } +} + +impl From for IdentityCert { + fn from(pair: secp256k1::KeyPair) -> Self { + let pubkey = pair.x_only_public_key().0.serialize(); + let msg = secp256k1::Message::from_hashed_data::(&pubkey); + let sig = pair.sign_schnorr(msg); + IdentityCert { + algo: EcAlgo::Bip340, + pubkey: Box::from(&pubkey[..]), + sig: Box::from(&sig[..]), + } + } +} + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct SigCert { + hash: HashAlgo, + curve: EcAlgo, + sig: Box<[u8]>, +} + +impl SigCert { + pub fn verify( + &self, + cert: &IdentityCert, + msg: impl AsRef<[u8]>, + ) -> Result<(), VerifyError> { + if cert.algo != self.curve { + return Err(VerifyError::AlgoMismatch); + } + + match self.curve { + EcAlgo::Bip340 => { + let sig = secp256k1::schnorr::Signature::from_slice(&self.sig) + .expect("broken signature data"); + let msg = match self.hash { + HashAlgo::Sha256d => { + Message::from_hashed_data::(msg.as_ref()) + } + }; + let pubkey = + secp256k1::XOnlyPublicKey::from_slice(&cert.pubkey) + .expect("broken pubkey"); + sig.verify(&msg, &pubkey)?; + } + EcAlgo::Ed25519 => todo!("Ed25519 signature verification"), + } + + Ok(()) + } +} + +impl StrictEncode for SigCert { + fn strict_encode( + &self, + mut e: E, + ) -> Result { + let mut len = self.hash.strict_encode(&mut e)?; + len += self.curve.strict_encode(&mut e)?; + e.write_all(&self.sig)?; + Ok(len + self.curve.sig_len()) + } +} + +impl StrictDecode for SigCert { + fn strict_decode( + mut d: D, + ) -> Result { + let hash = HashAlgo::strict_decode(&mut d)?; + let curve = EcAlgo::strict_decode(&mut d)?; + + let mut sig = vec![0u8; curve.sig_len()]; + d.read_exact(&mut sig)?; + + secp256k1::schnorr::Signature::from_slice(&sig) + .map_err(secp_to_sten_err)?; + + Ok(Self { + hash, + curve, + sig: Box::from(sig), + }) + } +} + +impl Debug for SigCert { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "dig {}", self.hash)?; + writeln!(f, "crv {}", self.curve)?; + writeln!(f, "sig {}", bin_fmt(&self.sig))?; + + Ok(()) + } +} + +impl Display for SigCert { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let data = self + .strict_serialize() + .expect("strict encoding of signature"); + let s = + bech32::encode("sig", data.to_base32(), bech32::Variant::Bech32m) + .expect("bech32 encoding of signature"); + + f.write_str(&s) + } +} + +impl FromStr for SigCert { + type Err = CertError; + + fn from_str(s: &str) -> Result { + let (hrp, encoded, variant) = bech32::decode(s)?; + + if hrp != "sig" { + return Err(CertError::InvalidHrp(hrp, "sig")); + } + + if variant != bech32::Variant::Bech32m { + return Err(CertError::InvalidVariant); + } + + let data = Vec::::from_base32(&encoded)?; + Self::strict_deserialize(data).map_err(CertError::from) + } +} + +fn bin_fmt(v: &[u8]) -> String { + v.to_hex() + .chars() + .enumerate() + .flat_map(|(i, c)| { + match i { + 0 => None, + i if i % (4 * 16) == 0 => Some('\n'), + i if i % 4 == 0 => Some(' '), + _ => None, + } + .into_iter() + .chain(std::iter::once(c)) + }) + .collect::() + .replace('\n', "\n ") +} + +fn secp_to_sten_err(err: secp256k1::Error) -> strict_encoding::Error { + strict_encoding::Error::DataIntegrityError(format!( + "broken elliptic curve data. Details: {}", + err + )) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use secp256k1::SECP256K1; + + use crate::{IdentityCert, IdentitySigner, SigCert}; + + fn cert() -> IdentityCert { + IdentityCert::from_str("crt1q9umuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqte3fc3pu8qq6p0qx48fjttj7ecfcemry5r7yqlnfna6qhf4s46r2aw68wqc9spn4a465x54zy03gleun58fcz3tpxhqcg5nv4ssgyeysxq8t50zt_venice_vega_balloon").unwrap() + } + + fn sig() -> SigCert { + SigCert::from_str("sig1qgqm0d5l8sjas4v2vk3tzqc5m2ltpkv208uf2chyh3fqmhwq3rdnu3ve0gkytrl7wl68075zxxukq9ff6gmd38w7hmtdas089jefkf2rsyasksse").unwrap() + } + + #[test] + fn cert_create() { + let pair = secp256k1::KeyPair::from_seckey_slice( + &SECP256K1, + &secp256k1::ONE_KEY[..], + ) + .unwrap(); + let _ = IdentityCert::from(pair); + } + + #[test] + fn cert_display() { + let cert = cert(); + + assert_eq!(cert.nym(), "venice_vega_balloon"); + assert_eq!(cert.fingerprint(), "8t50zt"); + + assert_eq!(format!("{}", cert), "crt1q9umuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqte3fc3pu8qq6p0qx48fjttj7ecfcemry5r7yqlnfna6qhf4s46r2aw68wqc9spn4a465x54zy03gleun58fcz3tpxhqcg5nv4ssgyeysxq8t50zt_venice_vega_balloon"); + assert_eq!(format!("{:#}", cert), "8t50zt_venice_vega_balloon"); + assert_eq!( + format!("{:?}", cert), + "\ +nym venice_vega_balloon +fgp 8t50zt +crv bip340 +idk 79be 667e f9dc bbac 55a0 6295 ce87 0b07 029b fcdb 2dce 28d9 59f2 815b \ + 16f8 1798 +sig a711 0f0e 0068 2f01 aa74 c96b 97b3 84e3 3b19 283f 101f 9a67 dd02 e9ac \ + 2ba1 abae + d1dc 0c16 019d 7b5d 50d4 a888 f8a3 f9e4 e874 e051 584d 7061 149b 2b08 \ + 2099 240c +" + ); + } + + #[test] + fn sign() { + let me = IdentitySigner::new_bip340(); + let msg = "This is me"; + let sig = me.sign(msg); + sig.verify(&me.cert, msg).unwrap(); + } + + #[test] + #[should_panic(expected = "InvalidSig")] + fn wrong_sig_msg() { + let me = IdentitySigner::new_bip340(); + let msg = "This is me"; + let sig = me.sign(msg); + sig.verify(&me.cert, "This is not me").unwrap(); + } + + #[test] + #[should_panic(expected = "InvalidSig")] + fn wrong_sig_key() { + let me = IdentitySigner::new_bip340(); + let other = IdentitySigner::new_bip340(); + let msg = "This is me"; + let sig = me.sign(msg); + sig.verify(&other.cert, msg).unwrap(); + } + + #[test] + fn sig_display() { + let sig = sig(); + assert_eq!(format!("{}", sig), "sig1qgqm0d5l8sjas4v2vk3tzqc5m2ltpkv208uf2chyh3fqmhwq3rdnu3ve0gkytrl7wl68075zxxukq9ff6gmd38w7hmtdas089jefkf2rsyasksse"); + assert_eq!(format!("{:#}", sig), "sig1qgqm0d5l8sjas4v2vk3tzqc5m2ltpkv208uf2chyh3fqmhwq3rdnu3ve0gkytrl7wl68075zxxukq9ff6gmd38w7hmtdas089jefkf2rsyasksse"); + assert_eq!( + format!("{:?}", sig), + "\ +dig sha256d +crv bip340 +sig b7b6 9f3c 25d8 558a 65a2 b103 14da beb0 d98a 79f8 9562 e4bc 520d ddc0 \ + 88db 3e45 + 997a 2c45 8ffe 77f4 77fa 8231 b960 1529 d236 d89d debe d6de c1e7 2cb2 \ + 9b25 4381 +" + ); + } +} diff --git a/src/bin/lnpbp.rs b/src/bin/lnpbp.rs index a2bddc73..4a2671a5 100644 --- a/src/bin/lnpbp.rs +++ b/src/bin/lnpbp.rs @@ -17,15 +17,25 @@ extern crate clap; extern crate amplify; extern crate serde_crate as serde; -use std::fmt::{Debug, Display}; -use std::io::{self, Read}; +use std::fmt::{Debug, Display, Formatter}; +use std::io::{self, Read, Write}; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; use std::str::FromStr; +use std::string::FromUtf8Error; +use std::{fmt, fs}; -use amplify::hex::{FromHex, ToHex}; -use base58::{FromBase58, ToBase58}; +use amplify::hex::{self, FromHex, ToHex}; +use base58::{FromBase58, FromBase58Error, ToBase58}; use clap::Parser; +use colorize::AnsiColor; use lnpbp::bech32::Blob; +use lnpbp::{bech32, id}; +use lnpbp_identity::{ + EcAlgo, IdentityCert, IdentitySigner, SigCert, VerifyError, +}; use serde::Serialize; +use strict_encoding::{StrictDecode, StrictEncode}; #[derive(Parser, Clone, Debug)] #[clap( @@ -43,18 +53,142 @@ pub struct Opts { #[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Command { - /// Commands for working with consignments - Convert { - /// Original data; if none are given reads from STDIN - data: Option, + /// Commands for working with LNP/BP identities + #[clap(subcommand)] + Identity(IdentityCommand), + /// Commands for converting data between encodings + Convert { /// Formatting of the input data - #[clap(short, long, default_value = "bech32")] - input: Format, + #[clap(short = 'f', long, default_value = "bech32")] + from: Format, /// Formatting for the output - #[clap(short, long, default_value = "yaml")] - output: Format, + #[clap(short = 't', long = "to", default_value = "yaml")] + into: Format, + + /// Original data string + #[clap(short, long, conflicts_with = "file")] + data: Option, + + /// File with the source data. If no `--data` option is given reads + /// the data from STDIN + #[clap()] + input_file: Option, + + /// File to store the results of the conversion. Defaults to STDOUT + #[clap()] + output_file: Option, + }, +} + +#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum IdentityCommand { + /// Generate a new identity, saving it to the file + Create { + /// Curve algorithm to use foe the new identity + #[clap(short, long, default_value = "bip340")] + algo: id::EcAlgo, + + /// File to store the identity in + #[clap()] + file: PathBuf, + }, + + /// Read info about the identity from the file + Info { + /// File containing identity information + #[clap()] + file: PathBuf, + }, + + /// Sign a message, a file or data read from STDIN + Sign { + /// File containing identity information + #[clap()] + identity_file: PathBuf, + + /// Message to sign + #[clap(short, long)] + message: Option, + + /// File to sign + #[clap(conflicts_with = "message")] + message_file: Option, + }, + + /// Verify an identity certificate and optionally a signature against a + /// file, message or data read from STDIN + Verify { + /// An identity certificate to use + #[clap()] + cert: IdentityCert, + + /// A signature to verify + #[clap()] + sig: SigCert, + + /// Message to verify the signature + #[clap(short, long = "msg")] + message: Option, + + /// File to verify the signature + #[clap(conflicts_with = "message")] + message_file: Option, + }, + + /// Encrypt a message + Encrypt { + /// Use ASCII armoring + #[clap(short, long = "ascii")] + armor: bool, + + /// File containing local information + #[clap()] + identity_file: PathBuf, + + /// An identity of the receiver + #[clap()] + cert: IdentityCert, + + /// Message to encrypt + #[clap(short, long = "msg", conflicts_with = "file")] + message: Option, + + /// File to encrypt + #[clap()] + src_file: Option, + + /// Destination file to save the encrypted data to + #[clap()] + dst_file: Option, + }, + + /// Decrypt a previously encrypted message + Decrypt { + /// The input data are ASCII armored + #[clap(short, long = "ascii")] + armor: bool, + + /// File containing local information + #[clap()] + identity_file: PathBuf, + + /// An identity of the receiver + #[clap()] + cert: IdentityCert, + + /// Message to decrypt + #[clap(short, long = "msg", conflicts_with = "file")] + message: Option, + + /// File to decrypt + #[clap()] + src_file: Option, + + /// Destination file to save the decrypted data to + #[clap()] + dst_file: Option, }, } @@ -110,7 +244,7 @@ impl FromStr for Format { "base64" => Format::Base64, "yaml" => Format::Yaml, "json" => Format::Json, - "hex" => Format::Hexadecimal, + "hex" | "base32" => Format::Hexadecimal, "raw" | "bin" | "binary" => Format::Raw, "rust" => Format::Rust, other => return Err(format!("Unknown format: {}", other)), @@ -118,53 +252,83 @@ impl FromStr for Format { } } -fn input_read(data: Option, format: Format) -> Result +#[derive(Display, Error, From)] +#[display(inner)] +pub enum Error { + #[from] + Io(io::Error), + + #[from] + Utf8(FromUtf8Error), + + #[display("incorrect hex string due to {0}")] + #[from] + Hex(hex::Error), + + #[display("incorrect bech32(m) string due to {0}")] + #[from] + Bech32(bech32::Error), + + #[display("incorrect base58 string")] + #[from] + Base58(FromBase58Error), + + #[display("incorrect base64 string due to {0}")] + #[from] + Base64(base64::DecodeError), + + #[display("incorrect JSON encoding. Details: {0}")] + #[from] + Json(serde_json::Error), + + #[display("incorrect YAML encoding. Details: {0}")] + #[from] + Yaml(serde_yaml::Error), + + #[display("incorrect encoding of the binary data. Details: {0}")] + #[from] + StrictEncoding(strict_encoding::Error), + + #[display("can't read data from {0} format")] + UnsupportedFormat(Format), + + #[from] + Signature(VerifyError), +} + +impl Debug for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +fn input_read(data: Vec, format: Format) -> Result where T: From> + FromStr + for<'de> serde::Deserialize<'de>, - ::Err: Display, + Error: From<::Err>, { - let data = data - .map(|d| d.as_bytes().to_vec()) - .ok_or_else(String::default) - .or_else(|_| -> Result, String> { - let mut buf = Vec::new(); - io::stdin() - .read_to_end(&mut buf) - .as_ref() - .map_err(io::Error::to_string)?; - Ok(buf) - })?; - let s = &String::from_utf8_lossy(&data); + match format { + Format::Base64 => return Ok(base64::decode(&data).map(T::from)?), + Format::Raw => return Ok(T::from(data)), + _ => {} + } + + let s = &String::from_utf8(data)?; Ok(match format { - Format::Bech32 => T::from_str(s).map_err(|err| err.to_string())?, - Format::Base58 => { - T::from(s.from_base58().map_err(|err| { - format!("Incorrect Base58 encoding: {:?}", err) - })?) - } - Format::Base64 => T::from( - base64::decode(&data) - .map_err(|err| format!("Incorrect Base64 encoding: {}", err))?, - ), - Format::Yaml => { - serde_yaml::from_str(s).map_err(|err| err.to_string())? - } - Format::Json => { - serde_json::from_str(s).map_err(|err| err.to_string())? - } - Format::Hexadecimal => { - T::from(Vec::::from_hex(s).map_err(|err| err.to_string())?) - } - Format::Raw => T::from(data), - _ => return Err(format!("Can't read data from {} format", format)), + Format::Bech32 => T::from_str(s)?, + Format::Base58 => T::from(s.from_base58()?), + Format::Yaml => serde_yaml::from_str(s)?, + Format::Json => serde_json::from_str(s)?, + Format::Hexadecimal => T::from(Vec::::from_hex(s)?), + _ => return Err(Error::UnsupportedFormat(format)), }) } fn output_write( - mut f: impl io::Write, + mut f: impl Write, data: T, format: Format, -) -> Result<(), String> +) -> Result<(), Error> where T: AsRef<[u8]> + Debug + Display + Serialize, { @@ -173,40 +337,109 @@ where Format::Bech32 => write!(f, "{}", data), Format::Base58 => write!(f, "{}", data.as_ref().to_base58()), Format::Base64 => write!(f, "{}", base64::encode(data.as_ref())), - Format::Yaml => write!( - f, - "{}", - serde_yaml::to_string(data.as_ref()) - .as_ref() - .map_err(serde_yaml::Error::to_string)? - ), - Format::Json => write!( - f, - "{}", - serde_json::to_string(data.as_ref()) - .as_ref() - .map_err(serde_json::Error::to_string)? - ), + Format::Yaml => write!(f, "{}", serde_yaml::to_string(data.as_ref())?), + Format::Json => write!(f, "{}", serde_json::to_string(data.as_ref())?), Format::Hexadecimal => write!(f, "{}", data.as_ref().to_hex()), Format::Rust => write!(f, "{:#04X?}", data.as_ref()), Format::Raw => f.write(data.as_ref()).map(|_| ()), } - .as_ref() - .map_err(io::Error::to_string)?; - Ok(()) + .map_err(Error::from) +} + +fn file_str_or_stdin( + file: Option, + msg: Option, +) -> Result, io::Error> { + Ok(match (file, msg) { + (Some(path), None) => { + let fd = fs::File::open(path)?; + Box::new(fd) + } + (None, Some(msg)) => { + let cursor = io::Cursor::new(msg.into_bytes()); + let reader = io::BufReader::new(cursor); + Box::new(reader) + } + (None, None) => { + let fd = io::stdin(); + Box::new(fd) + } + (Some(_), Some(_)) => unreachable!("clap broken"), + }) } -fn main() -> Result<(), String> { +fn file_or_stdout(file: Option) -> Result, io::Error> { + Ok(match file { + Some(path) => { + let fd = fs::File::create(path)?; + Box::new(fd) + } + None => { + let fd = io::stdout(); + Box::new(fd) + } + }) +} + +fn main() -> Result<(), Error> { let opts = Opts::parse(); match opts.command { + Command::Identity(IdentityCommand::Create { algo, file }) => { + if algo != EcAlgo::Bip340 { + todo!("other than Secp256k1 BIP340 algorithms") + } + let id = IdentitySigner::new_bip340(); + let fd = fs::File::create(file)?; + let mut perms = fd.metadata()?.permissions(); + perms.set_mode(0o600); + fd.set_permissions(perms)?; + id.strict_encode(fd)?; + println!("{}", id.cert); + println!("{:?}", id.cert); + } + Command::Identity(IdentityCommand::Info { file }) => { + let fd = fs::File::open(file)?; + let id = IdentitySigner::strict_decode(fd)?; + println!("{}", id.cert); + println!("{:?}", id.cert); + } + Command::Identity(IdentityCommand::Sign { + identity_file, + message, + message_file, + }) => { + let fd = fs::File::open(identity_file)?; + let id = IdentitySigner::strict_decode(fd)?; + let input = file_str_or_stdin(message_file, message)?; + let sig = id.sign_stream(input)?; + println!("{}", sig); + } + Command::Identity(IdentityCommand::Verify { + cert, + sig, + message, + message_file, + }) => { + let mut input = file_str_or_stdin(message_file, message)?; + let mut data = vec![]; + input.read_to_end(&mut data)?; + sig.verify(&cert, data)?; + println!("{}", "Signature is valid".green()); + } + Command::Identity(_) => todo!("elgamal encryption support"), Command::Convert { data, - input, - output, + from, + into, + input_file, + output_file, } => { - let data: Blob = input_read(data, input)?; - output_write(io::stdout(), data, output)?; + let mut input = file_str_or_stdin(input_file, data)?; + let mut data = vec![]; + input.read_to_end(&mut data)?; + let data: Blob = input_read(data, from)?; + output_write(file_or_stdout(output_file)?, data, into)?; } } diff --git a/src/lib.rs b/src/lib.rs index b5a71693..e123c6c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,3 +42,5 @@ pub extern crate lnpbp_bech32 as bech32; pub extern crate lnpbp_chain as chain; #[cfg(feature = "elgamal")] pub extern crate lnpbp_elgamal as elgamal; +#[cfg(feature = "identity")] +pub extern crate lnpbp_identity as id;