signer: a new module with a BIP39-based hot signer
This commit is contained in:
parent
e4d4fcd9fc
commit
6e3b951e54
75
Cargo.lock
generated
75
Cargo.lock
generated
@ -64,6 +64,18 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.9.7",
|
||||
"rand_core 0.4.2",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.29.1"
|
||||
@ -71,11 +83,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cb36de3b18ad25f396f9168302e36fb7e1e8923298ab3127da252d288d5af9d"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.11.0",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.11.0"
|
||||
@ -209,12 +227,15 @@ version = "0.2.0"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"base64",
|
||||
"bip39",
|
||||
"dirs",
|
||||
"fern",
|
||||
"getrandom",
|
||||
"jsonrpc",
|
||||
"libc",
|
||||
"log",
|
||||
"miniscript",
|
||||
"rdrand",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -247,6 +268,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
@ -310,6 +337,30 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e233b642160555c1aa1ff7a78443c6139342f411b6fa6602af2ebbfee9e166bb"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@ -342,7 +393,7 @@ dependencies = [
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
"smallvec 1.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -363,7 +414,7 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.11.0",
|
||||
"secp256k1-sys",
|
||||
"serde",
|
||||
]
|
||||
@ -408,6 +459,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
|
||||
dependencies = [
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
@ -460,6 +520,15 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf"
|
||||
dependencies = [
|
||||
"smallvec 0.6.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@ -56,3 +56,15 @@ libc = "0.2"
|
||||
|
||||
# Used for PSBTs
|
||||
base64 = "0.13"
|
||||
|
||||
# Used for generating mnemonics
|
||||
getrandom = "0.2"
|
||||
|
||||
# Additional entropy for generating mnemonics
|
||||
[target.'cfg(target_arch = "x86")'.dependencies]
|
||||
rdrand = "0.8"
|
||||
[target.'cfg(target_arch = "x86_64")'.dependencies]
|
||||
rdrand = "0.8"
|
||||
|
||||
# Used for the hot signer
|
||||
bip39 = "1.0"
|
||||
|
||||
@ -7,6 +7,8 @@ mod database;
|
||||
pub mod descriptors;
|
||||
#[cfg(feature = "jsonrpc_server")]
|
||||
mod jsonrpc;
|
||||
mod random;
|
||||
pub mod signer;
|
||||
#[cfg(test)]
|
||||
mod testutils;
|
||||
|
||||
|
||||
112
src/random.rs
Normal file
112
src/random.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use miniscript::bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
error, fmt,
|
||||
hash::{BuildHasher, Hasher},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RandomnessError {
|
||||
Hardware(String),
|
||||
Os(String),
|
||||
ContextualInfo(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for RandomnessError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Hardware(s) => write!(f, "Error when getting randomness from hardware: {}", s),
|
||||
Self::Os(s) => write!(f, "Error when getting randomness from the OS: {}", s),
|
||||
Self::ContextualInfo(s) => write!(f, "Error when getting contextual info: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for RandomnessError {}
|
||||
|
||||
// Get some entrop from RDRAND when available.
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
fn cpu_randomness() -> Result<Option<[u8; 32]>, RandomnessError> {
|
||||
if let Ok(mut rand_gen) = rdrand::RdRand::new() {
|
||||
let mut buf = [0; 32];
|
||||
rand_gen
|
||||
.try_fill_bytes(&mut buf)
|
||||
.map_err(|e| RandomnessError::Hardware(e.to_string()))?;
|
||||
assert_ne!(buf, [0; 32]);
|
||||
Ok(Some(buf))
|
||||
} else {
|
||||
// Not available.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
fn hardware_randomness() -> Result<Option<[u8; 32]>, RandomnessError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// OS-generated randomness. See https://docs.rs/getrandom/latest/getrandom/#supported-targets
|
||||
// (basically this calls `getrandom()` or polls `/dev/urandom` on Linux, `BCryptGenRandom` on
|
||||
// Windows, and `getentropy()` / `/dev/random` on Mac.
|
||||
fn system_randomness() -> Result<[u8; 32], RandomnessError> {
|
||||
let mut buf = [0; 32];
|
||||
getrandom::getrandom(&mut buf).map_err(|e| RandomnessError::Os(e.to_string()))?;
|
||||
assert_ne!(buf, [0; 32]);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// Some more contextual data to try to get at least a slight bit of additional entropy.
|
||||
fn additional_data() -> Result<[u8; 32], RandomnessError> {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
|
||||
let timestamp: u16 = (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| RandomnessError::ContextualInfo(e.to_string()))?
|
||||
.as_secs()
|
||||
% u16::MAX as u64) as u16;
|
||||
engine.input(×tamp.to_be_bytes());
|
||||
let hasher_number = hash_map::RandomState::new().build_hasher().finish();
|
||||
engine.input(&hasher_number.to_be_bytes());
|
||||
let pid = std::process::id();
|
||||
engine.input(&pid.to_be_bytes());
|
||||
// TODO: get some more contextual information
|
||||
|
||||
Ok(*sha256::Hash::from_engine(engine).as_inner())
|
||||
}
|
||||
|
||||
/// Get 32 random bytes. This is mainly based on OS-provided randomness (`getrandom` or
|
||||
/// `/dev/urandom` on Linux, `getentropy` / `/dev/random` on MacOS, and `BCryptGenRandom` on
|
||||
/// Windows. In addition some randomness may be taken directly from the CPU if it is
|
||||
/// available, and some contextual information are added to the mix as well.
|
||||
pub fn random_bytes() -> Result<[u8; 32], RandomnessError> {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
|
||||
if let Some(bytes) = cpu_randomness()? {
|
||||
engine.input(&bytes);
|
||||
}
|
||||
engine.input(&system_randomness()?);
|
||||
engine.input(&additional_data()?);
|
||||
// TODO: add more sources of randomness
|
||||
|
||||
Ok(*sha256::Hash::from_engine(engine).as_inner())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// This does not test the quality of the randomness but at least sanity checks it's
|
||||
// not obviously broken.
|
||||
#[test]
|
||||
fn randomness_sanity_check() {
|
||||
let mut set = HashSet::with_capacity(100);
|
||||
|
||||
for _ in 0..100 {
|
||||
let rand = random_bytes().unwrap();
|
||||
assert!(!set.contains(&rand));
|
||||
set.insert(rand);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/signer.rs
Normal file
84
src/signer.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! Signer module
|
||||
//!
|
||||
//! Some helpers to facilitate the usage of a signer in client of the Liana daemon. For now
|
||||
//! only contains a hot signer.
|
||||
|
||||
use crate::random;
|
||||
|
||||
use std::{convert::TryInto, error, fmt, str};
|
||||
|
||||
/// An error related to using a signer.
|
||||
#[derive(Debug)]
|
||||
pub enum SignerError {
|
||||
Randomness(random::RandomnessError),
|
||||
Mnemonic(bip39::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for SignerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Randomness(s) => write!(f, "Error related to getting randomness: {}", s),
|
||||
Self::Mnemonic(s) => write!(f, "Error when working with mnemonics: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for SignerError {}
|
||||
|
||||
/// A signer that keeps the key on the laptop. Based on BIP39.
|
||||
pub struct HotSigner {
|
||||
mnemonic: bip39::Mnemonic,
|
||||
}
|
||||
|
||||
impl HotSigner {
|
||||
/// Create a new hot signer from random bytes. Uses a 12-words mnemonics without a passphrase.
|
||||
pub fn generate() -> Result<Self, SignerError> {
|
||||
// We want a 12-words mnemonic so we only use 16 of the 32 bytes.
|
||||
let random_32bytes = random::random_bytes().map_err(SignerError::Randomness)?;
|
||||
let mnemonic =
|
||||
bip39::Mnemonic::from_entropy(&random_32bytes[..16]).map_err(SignerError::Mnemonic)?;
|
||||
Ok(Self { mnemonic })
|
||||
}
|
||||
|
||||
/// The BIP39 mnemonics from which the master key of this signer is derived.
|
||||
pub fn words(&self) -> [&'static str; 12] {
|
||||
let words: Vec<&'static str> = self.mnemonic.word_iter().collect();
|
||||
words.try_into().expect("Always 12 words")
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for HotSigner {
|
||||
type Err = SignerError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mnemonic = bip39::Mnemonic::from_str(s).map_err(SignerError::Mnemonic)?;
|
||||
Ok(Self { mnemonic })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn hot_signer_gen() {
|
||||
// Entropy isn't completely broken.
|
||||
assert_ne!(
|
||||
HotSigner::generate().unwrap().words(),
|
||||
HotSigner::generate().unwrap().words()
|
||||
);
|
||||
|
||||
// Roundtrips.
|
||||
let signer = HotSigner::generate().unwrap();
|
||||
let mnemonics_str = signer.words().iter().fold(String::new(), |mut s, w| {
|
||||
s += w;
|
||||
s += " ";
|
||||
s
|
||||
});
|
||||
assert_eq!(
|
||||
HotSigner::from_str(&mnemonics_str).unwrap().words(),
|
||||
signer.words()
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user