From e38eb09f5df4a3252d90d9623f626859ee83ee48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabricio=20Dematt=C3=AA?= <46208058+demfabris@users.noreply.github.com> Date: Fri, 11 Mar 2022 08:44:05 -0300 Subject: [PATCH] misc/keygen: Implement cli tool to handle key material (#2453) - Load keys from file - Generate new keys (with optional prefix) - Replaces peer-id-generator Co-authored-by: Fabricio Dematte Co-authored-by: Max Inden --- Cargo.toml | 2 +- misc/{peer-id-generator => keygen}/Cargo.toml | 15 ++- misc/keygen/src/config.rs | 41 ++++++ misc/keygen/src/main.rs | 126 ++++++++++++++++++ misc/peer-id-generator/src/main.rs | 87 ------------ 5 files changed, 176 insertions(+), 95 deletions(-) rename misc/{peer-id-generator => keygen}/Cargo.toml (60%) create mode 100644 misc/keygen/src/config.rs create mode 100644 misc/keygen/src/main.rs delete mode 100644 misc/peer-id-generator/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 3818f6fa..65729eb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ members = [ "core", "misc/metrics", "misc/multistream-select", - "misc/peer-id-generator", + "misc/keygen", "muxers/mplex", "muxers/yamux", "protocols/dcutr", diff --git a/misc/peer-id-generator/Cargo.toml b/misc/keygen/Cargo.toml similarity index 60% rename from misc/peer-id-generator/Cargo.toml rename to misc/keygen/Cargo.toml index fc696027..b30ea23b 100644 --- a/misc/peer-id-generator/Cargo.toml +++ b/misc/keygen/Cargo.toml @@ -1,16 +1,17 @@ [package] -name = "peer-id-generator" -edition = "2021" -rust-version = "1.56.1" +name = "keygen" version = "0.1.0" -description = "Generate peer ids that are prefixed with a specific string" -authors = ["Parity Technologies "] +edition = "2021" +authors = ["demfabris "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] -publish = false [dependencies] +structopt = "0.3.26" +zeroize = "1" +serde = { version = "1.0.136", features = ["derive"] } +serde_json = "1.0.79" libp2p-core = { path = "../../core", default-features = false, version = "0.32.0"} -num_cpus = "1.8" +base64 = "0.13.0" diff --git a/misc/keygen/src/config.rs b/misc/keygen/src/config.rs new file mode 100644 index 00000000..996cfd26 --- /dev/null +++ b/misc/keygen/src/config.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::path::Path; + +use libp2p_core::identity::Keypair; +use libp2p_core::PeerId; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Config { + pub identity: Identity, +} + +impl Config { + pub fn from_file(path: &Path) -> Result> { + Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) + } + + pub fn from_key_material(peer_id: PeerId, keypair: &Keypair) -> Result> { + let priv_key = base64::encode(keypair.to_protobuf_encoding()?); + let peer_id = peer_id.to_base58(); + Ok(Self { + identity: Identity { peer_id, priv_key }, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Identity { + #[serde(rename = "PeerID")] + pub peer_id: String, + pub priv_key: String, +} + +impl zeroize::Zeroize for Config { + fn zeroize(&mut self) { + self.identity.peer_id.zeroize(); + self.identity.priv_key.zeroize(); + } +} diff --git a/misc/keygen/src/main.rs b/misc/keygen/src/main.rs new file mode 100644 index 00000000..a3544137 --- /dev/null +++ b/misc/keygen/src/main.rs @@ -0,0 +1,126 @@ +use std::error::Error; +use std::path::PathBuf; +use std::str::{self, FromStr}; +use std::sync::mpsc; +use std::thread; + +mod config; + +use libp2p_core::identity::{self, ed25519}; +use libp2p_core::PeerId; +use structopt::StructOpt; +use zeroize::Zeroizing; + +#[derive(Debug, StructOpt)] +#[structopt(name = "libp2p key material generator")] +struct Args { + /// JSON formatted output + #[structopt(long, global = true)] + json: bool, + + #[structopt(subcommand)] + cmd: Command, +} + +#[derive(Debug, StructOpt)] +enum Command { + /// Read from config file + From { + /// Provide a IPFS config file + #[structopt(parse(from_os_str))] + config: PathBuf, + }, + /// Generate random + Rand { + /// The keypair prefix + #[structopt(long)] + prefix: Option, + }, +} + +// Due to the fact that a peer id uses a SHA-256 multihash, it always starts with the +// bytes 0x1220, meaning that only some characters are valid. +const ALLOWED_FIRST_BYTE: &[u8] = b"NPQRSTUVWXYZ"; + +// The base58 alphabet is not necessarily obvious. +const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +fn main() -> Result<(), Box> { + let args = Args::from_args(); + + let (local_peer_id, local_keypair) = match args.cmd { + // Generate keypair from some sort of key material. Currently supporting `IPFS` config file + Command::From { config } => { + let config = Zeroizing::new(config::Config::from_file(config.as_ref())?); + + let keypair = identity::Keypair::from_protobuf_encoding(&Zeroizing::new( + base64::decode(config.identity.priv_key.as_bytes())?, + ))?; + + let peer_id = keypair.public().into(); + assert_eq!( + PeerId::from_str(&config.identity.peer_id)?, + peer_id, + "Expect peer id derived from private key and peer id retrieved from config to match." + ); + + (peer_id, keypair) + } + + // Generate a random keypair, optionally with a prefix + Command::Rand { prefix } => { + if let Some(prefix) = prefix { + if prefix.as_bytes().iter().any(|c| !ALPHABET.contains(c)) { + eprintln!("Prefix {} is not valid base58", prefix); + std::process::exit(1); + } + + // Checking conformity to ALLOWED_FIRST_BYTE. + if !prefix.is_empty() && !ALLOWED_FIRST_BYTE.contains(&prefix.as_bytes()[0]) { + eprintln!("Prefix {} is not reachable", prefix); + eprintln!( + "Only the following bytes are possible as first byte: {}", + str::from_utf8(ALLOWED_FIRST_BYTE).unwrap() + ); + std::process::exit(1); + } + + let (tx, rx) = mpsc::channel::<(PeerId, identity::Keypair)>(); + + // Find peer IDs in a multithreaded fashion. + for _ in 0..thread::available_parallelism()?.get() { + let prefix = prefix.clone(); + let tx = tx.clone(); + + thread::spawn(move || loop { + let keypair = ed25519::Keypair::generate(); + let peer_id = identity::PublicKey::Ed25519(keypair.public()).to_peer_id(); + let base58 = peer_id.to_base58(); + if base58[8..].starts_with(&prefix) { + tx.send((peer_id, identity::Keypair::Ed25519(keypair))) + .expect("to send"); + } + }); + } + + rx.recv().expect("to recv") + } else { + let keypair = identity::Keypair::Ed25519(ed25519::Keypair::generate()); + (keypair.public().into(), keypair) + } + } + }; + + if args.json { + let config = config::Config::from_key_material(local_peer_id, &local_keypair)?; + println!("{}", serde_json::to_string(&config)?); + } else { + println!( + "PeerId: {:?} Keypair: {:?}", + local_peer_id, + local_keypair.to_protobuf_encoding() + ); + } + + Ok(()) +} diff --git a/misc/peer-id-generator/src/main.rs b/misc/peer-id-generator/src/main.rs deleted file mode 100644 index 45239317..00000000 --- a/misc/peer-id-generator/src/main.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2018 Parity Technologies (UK) Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use libp2p_core::identity; -use std::{env, str, thread, time::Duration}; - -fn main() { - // Due to the fact that a peer id uses a SHA-256 multihash, it always starts with the - // bytes 0x1220, meaning that only some characters are valid. - const ALLOWED_FIRST_BYTE: &'static [u8] = b"NPQRSTUVWXYZ"; - - let prefix = match env::args().nth(1) { - Some(prefix) => prefix, - None => { - println!( - "Usage: {} \n\n\ - Generates a peer id that starts with the chosen prefix using a secp256k1 public \ - key.\n\n\ - Prefix must be a sequence of characters in the base58 \ - alphabet, and must start with one of the following: {}", - env::current_exe() - .unwrap() - .file_name() - .unwrap() - .to_str() - .unwrap(), - str::from_utf8(ALLOWED_FIRST_BYTE).unwrap() - ); - return; - } - }; - - // The base58 alphabet is not necessarily obvious. - const ALPHABET: &'static [u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - if prefix.as_bytes().iter().any(|c| !ALPHABET.contains(c)) { - println!("Prefix {} is not valid base58", prefix); - return; - } - - // Checking conformity to ALLOWED_FIRST_BYTE. - if !prefix.is_empty() { - if !ALLOWED_FIRST_BYTE.contains(&prefix.as_bytes()[0]) { - println!("Prefix {} is not reachable", prefix); - println!( - "Only the following bytes are possible as first byte: {}", - str::from_utf8(ALLOWED_FIRST_BYTE).unwrap() - ); - return; - } - } - - // Find peer IDs in a multithreaded fashion. - for _ in 0..num_cpus::get() { - let prefix = prefix.clone(); - thread::spawn(move || loop { - let keypair = identity::ed25519::Keypair::generate(); - let secret = keypair.secret(); - let peer_id = identity::PublicKey::Ed25519(keypair.public()).to_peer_id(); - let base58 = peer_id.to_base58(); - if base58[2..].starts_with(&prefix) { - println!("Found {:?}", peer_id); - println!("=> Private key = {:?}", secret.as_ref()); - } - }); - } - - loop { - thread::sleep(Duration::from_secs(3600)); - } -}