switch to x25519 for onion encryption

This commit is contained in:
scilio 2022-10-13 11:15:52 -04:00
parent f70e1dfbf2
commit c59688a045
6 changed files with 136 additions and 111 deletions

26
Cargo.lock generated
View file

@ -45,7 +45,7 @@ dependencies = [
"nom 7.1.0",
"pin-project",
"rand 0.7.3",
"rand 0.8.4",
"rand 0.8.5",
"rust-embed",
"scrypt",
"sha2 0.9.8",
@ -65,7 +65,7 @@ dependencies = [
"cookie-factory",
"hkdf",
"nom 7.1.0",
"rand 0.8.4",
"rand 0.8.5",
"secrecy 0.8.0",
"sha2 0.9.8",
]
@ -2475,7 +2475,9 @@ dependencies = [
"bytes 0.5.6",
"chacha20",
"clap",
"curve25519-dalek 2.1.3",
"dirs",
"ed25519-dalek",
"futures 0.3.17",
"grin_api 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
"grin_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
@ -2488,6 +2490,7 @@ dependencies = [
"grin_wallet_api",
"grin_wallet_impls",
"grin_wallet_libwallet",
"grin_wallet_util",
"hmac 0.12.0",
"hyper 0.14.14",
"itertools",
@ -2496,7 +2499,7 @@ dependencies = [
"jsonrpc-http-server",
"lazy_static",
"pbkdf2 0.8.0",
"rand 0.8.4",
"rand 0.7.3",
"ring",
"rpassword",
"serde",
@ -2506,6 +2509,7 @@ dependencies = [
"thiserror",
"tokio 1.12.0",
"toml",
"x25519-dalek 0.6.0",
]
[[package]]
@ -3033,14 +3037,13 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rand_hc 0.3.1",
]
[[package]]
@ -3124,15 +3127,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core 0.6.3",
]
[[package]]
name = "rand_isaac"
version = "0.1.1"
@ -3854,7 +3848,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if 1.0.0",
"libc",
"rand 0.8.4",
"rand 0.8.5",
"redox_syscall 0.2.10",
"remove_dir_all",
"winapi 0.3.9",

View file

@ -11,7 +11,9 @@ byteorder = "1"
bytes = "0.5.6"
chacha20 = "0.8.1"
clap = { version = "2.33", features = ["yaml"] }
curve25519-dalek = "2.1"
dirs = "2.0"
ed25519-dalek = "1.0.1"
futures = "0.3"
hmac = { version = "0.12.0", features = ["std"]}
hyper = { version = "0.14", features = ["full"] }
@ -21,7 +23,7 @@ jsonrpc-derive = "18.0"
jsonrpc-http-server = "18.0"
lazy_static = "1"
pbkdf2 = "0.8.0"
rand = "0.8.4"
rand = "0.7.3"
ring = "0.16"
rpassword = "4.0"
serde = { version = "1", features= ["derive"]}
@ -31,6 +33,7 @@ sha2 = "0.10.0"
thiserror = "1.0.31"
tokio = { version = "1", features = ["full"] }
toml = "0.5"
x25519-dalek = "0.6.0"
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
grin_util = "5"
grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
@ -42,3 +45,4 @@ grin_store = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-al
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }

View file

@ -190,8 +190,8 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
)
}
#[cfg(unix)]
async fn build_signals_fut() {
if cfg!(unix) {
use tokio::signal::unix::{signal, SignalKind};
// Listen for SIGINT, SIGQUIT, and SIGTERM
@ -207,12 +207,14 @@ async fn build_signals_fut() {
Box::pin(interrupt_signal.recv()),
])
.await;
} else {
}
#[cfg(not(unix))]
async fn build_signals_fut() {
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
}
}
fn prompt_password() -> ZeroingString {
ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap())

View file

@ -1,34 +1,62 @@
use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
use crate::secp::{self, Commitment, SecretKey};
use crate::types::Payload;
use crate::onion::OnionError::{InvalidKeyLength, SerializationError};
use chacha20::cipher::{NewCipher, StreamCipher};
use chacha20::{ChaCha20, Key, Nonce};
use grin_core::ser::{self, ProtocolVersion, Readable, Reader, Writeable, Writer};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use grin_util::{self, ToHex};
use hmac::digest::InvalidLength;
use hmac::{Hmac, Mac};
use serde::ser::SerializeStruct;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::convert::TryInto;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::result::Result;
use thiserror::Error;
use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret};
type HmacSha256 = Hmac<Sha256>;
type RawBytes = Vec<u8>;
/// A data packet with layers of encryption
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug)]
pub struct Onion {
/// The onion originator's portion of the shared secret
pub ephemeral_pubkey: PublicKey,
pub ephemeral_pubkey: xPublicKey,
/// The pedersen commitment before adjusting the excess and subtracting the fee
pub commit: Commitment,
/// The encrypted payloads which represent the layers of the onion
pub enc_payloads: Vec<RawBytes>,
}
impl PartialEq for Onion {
fn eq(&self, other: &Onion) -> bool {
*self.ephemeral_pubkey.as_bytes() == *other.ephemeral_pubkey.as_bytes()
&& self.commit == other.commit
&& self.enc_payloads == other.enc_payloads
}
}
impl Eq for Onion {}
impl Hash for Onion {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write(self.ephemeral_pubkey.as_bytes());
state.write(self.commit.as_ref());
state.write_usize(self.enc_payloads.len());
for p in &self.enc_payloads {
state.write(p.as_slice());
}
}
}
fn vec_to_32_byte_arr(v: Vec<u8>) -> Result<[u8; 32], OnionError> {
v.try_into().map_err(|_| InvalidKeyLength)
}
impl Onion {
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
let mut vec = vec![];
@ -38,9 +66,7 @@ impl Onion {
/// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload
pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion), OnionError> {
let secp = Secp256k1::new();
let shared_secret = SharedSecret::new(&secp, &self.ephemeral_pubkey, &secret_key);
let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey);
let mut cipher = new_stream_cipher(&shared_secret)?;
let mut decrypted_bytes = self.enc_payloads[0].clone();
@ -62,10 +88,12 @@ impl Onion {
let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?;
let mut ephemeral_pubkey = self.ephemeral_pubkey.clone();
ephemeral_pubkey
.mul_assign(&secp, &blinding_factor)
.map_err(|e| OnionError::CalcPubKeyError(e))?;
let ephemeral_key = StaticSecret::from(
*blinding_factor
.diffie_hellman(&self.ephemeral_pubkey)
.as_bytes(),
);
let ephemeral_pubkey = xPublicKey::from(&ephemeral_key);
let mut commitment = self.commit.clone();
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)
@ -84,23 +112,23 @@ impl Onion {
fn calc_blinding_factor(
shared_secret: &SharedSecret,
ephemeral_pubkey: &PublicKey,
) -> Result<SecretKey, OnionError> {
let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?;
ephemeral_pubkey: &xPublicKey,
) -> Result<StaticSecret, OnionError> {
let mut hasher = Sha256::default();
hasher.update(&serialized_pubkey);
hasher.update(&shared_secret[0..32]);
hasher.update(ephemeral_pubkey.as_bytes());
hasher.update(shared_secret.as_bytes());
let hashed: [u8; 32] = hasher
.finalize()
.as_slice()
.try_into()
.map_err(|_| InvalidKeyLength)?;
let secp = Secp256k1::new();
let blind = SecretKey::from_slice(&secp, &hasher.finalize())
.map_err(|e| OnionError::CalcBlindError(e))?;
Ok(blind)
Ok(StaticSecret::from(hashed))
}
fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20, OnionError> {
let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?;
mu_hmac.update(&shared_secret[0..32]);
mu_hmac.update(shared_secret.as_bytes());
let mukey = mu_hmac.finalize().into_bytes();
let key = Key::from_slice(&mukey[0..32]);
@ -111,7 +139,7 @@ fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20, OnionErro
impl Writeable for Onion {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
self.ephemeral_pubkey.write(writer)?;
writer.write_fixed_bytes(self.ephemeral_pubkey.as_bytes())?;
writer.write_fixed_bytes(&self.commit)?;
writer.write_u64(self.enc_payloads.len() as u64)?;
for p in &self.enc_payloads {
@ -124,7 +152,9 @@ impl Writeable for Onion {
impl Readable for Onion {
fn read<R: Reader>(reader: &mut R) -> Result<Onion, ser::Error> {
let ephemeral_pubkey = PublicKey::read(reader)?;
let pubkey_bytes: [u8; 32] =
vec_to_32_byte_arr(reader.read_fixed_bytes(32)?).map_err(|_| ser::Error::CountError)?;
let ephemeral_pubkey = xPublicKey::from(pubkey_bytes);
let commit = Commitment::read(reader)?;
let mut enc_payloads: Vec<RawBytes> = Vec::new();
let len = reader.read_u64()?;
@ -148,11 +178,7 @@ impl serde::ser::Serialize for Onion {
{
let mut state = serializer.serialize_struct("Onion", 3)?;
let secp = Secp256k1::new();
state.serialize_field(
"pubkey",
&self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex(),
)?;
state.serialize_field("pubkey", &self.ephemeral_pubkey.as_bytes().to_hex())?;
state.serialize_field("commit", &self.commit.to_hex())?;
let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect();
@ -197,11 +223,10 @@ impl<'de> serde::de::Deserialize<'de> for Onion {
let val: String = map.next_value()?;
let vec =
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
let secp = Secp256k1::new();
pubkey = Some(
PublicKey::from_slice(&secp, &vec[..])
.map_err(serde::de::Error::custom)?,
);
pubkey =
Some(xPublicKey::from(vec_to_32_byte_arr(vec).map_err(|_| {
serde::de::Error::custom("Invalid length pubkey")
})?));
}
Field::Commit => {
let val: String = map.next_value()?;
@ -267,40 +292,49 @@ impl From<ser::Error> for OnionError {
#[cfg(test)]
pub mod test_util {
use super::{Onion, OnionError, RawBytes};
use crate::secp::test_util::{rand_commit, rand_proof, rand_pubkey};
use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
use crate::secp::test_util::{rand_commit, rand_proof};
use crate::secp::{random_secret, Commitment, SecretKey};
use crate::types::Payload;
use chacha20::cipher::StreamCipher;
use grin_core::core::FeeFields;
use rand::RngCore;
use rand::{thread_rng, RngCore};
use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::{SharedSecret, StaticSecret};
#[derive(Clone)]
pub struct Hop {
pub pubkey: PublicKey,
pub pubkey: xPublicKey,
pub payload: Payload,
}
/*
Choose random xi for each node ni and create a Payload (Pi) for each containing xi
Build a rangeproof for Cn=Cin+(Σx1...n)*G and include it in payload Pn
Choose random initial ephemeral keypair (r1, R1)
Derive remaining ephemeral keypairs such that ri+1=ri*Sha256(Ri||si) where si=ECDH(Ri, Ki)
For each node ni, use ChaCha20 stream cipher with key=HmacSha256("MWIXNET"||si) and nonce "NONCE1234567" to encrypt payloads Pi...n
*/
/// Create an Onion for the Commitment, encrypting the payload for each hop
pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion, OnionError> {
let secp = Secp256k1::new();
let session_key = secp::random_secret();
let mut ephemeral_key = session_key.clone();
let initial_key = StaticSecret::new(&mut thread_rng());
let mut ephemeral_key = initial_key.clone();
let mut shared_secrets: Vec<SharedSecret> = Vec::new();
let mut enc_payloads: Vec<RawBytes> = Vec::new();
for hop in hops {
let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key);
let shared_secret = ephemeral_key.diffie_hellman(&hop.pubkey);
let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)
.map_err(|e| OnionError::CalcPubKeyError(e))?;
let ephemeral_pubkey = xPublicKey::from(&ephemeral_key);
let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
shared_secrets.push(shared_secret);
enc_payloads.push(hop.payload.serialize()?);
ephemeral_key
.mul_assign(&secp, &blinding_factor)
.map_err(|e| OnionError::CalcPubKeyError(e))?;
ephemeral_key = StaticSecret::from(
*ephemeral_key
.diffie_hellman(&xPublicKey::from(&blinding_factor))
.as_bytes(),
);
}
for i in (0..shared_secrets.len()).rev() {
@ -311,8 +345,7 @@ pub mod test_util {
}
let onion = Onion {
ephemeral_pubkey: PublicKey::from_secret_key(&secp, &session_key)
.map_err(|e| OnionError::CalcPubKeyError(e))?,
ephemeral_pubkey: xPublicKey::from(&initial_key),
commit: commitment.clone(),
enc_payloads,
};
@ -322,13 +355,13 @@ pub mod test_util {
pub fn rand_onion() -> Onion {
let commit = rand_commit();
let mut hops = Vec::new();
let k = (rand::thread_rng().next_u64() % 5) + 1;
let k = (thread_rng().next_u64() % 5) + 1;
for i in 0..k {
let hop = Hop {
pubkey: rand_pubkey(),
pubkey: xPublicKey::from(random_secret().0),
payload: Payload {
excess: secp::random_secret(),
fee: FeeFields::from(rand::thread_rng().next_u32()),
excess: random_secret(),
fee: FeeFields::from(thread_rng().next_u32()),
rangeproof: if i == (k - 1) {
Some(rand_proof())
} else {
@ -346,15 +379,12 @@ pub mod test_util {
pub fn next_ephemeral_pubkey(
onion: &Onion,
server_key: &SecretKey,
) -> Result<PublicKey, OnionError> {
let secp = Secp256k1::new();
let mut ephemeral_pubkey = onion.ephemeral_pubkey.clone();
let shared_secret = SharedSecret::new(&secp, &ephemeral_pubkey, &server_key);
let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
ephemeral_pubkey
.mul_assign(&secp, &blinding_factor)
.map_err(|e| OnionError::CalcPubKeyError(e))?;
Ok(ephemeral_pubkey)
) -> Result<xPublicKey, OnionError> {
let shared_secret =
StaticSecret::from(server_key.0.clone()).diffie_hellman(&onion.ephemeral_pubkey);
let blinding_factor = super::calc_blinding_factor(&shared_secret, &onion.ephemeral_pubkey)?;
let mul = blinding_factor.diffie_hellman(&onion.ephemeral_pubkey);
Ok(xPublicKey::from(&StaticSecret::from(*mul.as_bytes())))
}
}
@ -365,6 +395,7 @@ pub mod tests {
use crate::types::Payload;
use grin_core::core::FeeFields;
use x25519_dalek::{PublicKey as xPublicKey, StaticSecret};
/// Test end-to-end Onion creation and unwrapping logic.
#[test]
@ -405,7 +436,7 @@ pub mod tests {
};
hops.push(Hop {
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
pubkey: xPublicKey::from(&StaticSecret::from(keys[i].0.clone())),
payload: Payload {
excess,
fee: FeeFields::from(fee_per_hop as u32),

View file

@ -218,7 +218,7 @@ pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature, secp256k1zkp::Er
#[cfg(test)]
pub mod test_util {
use crate::secp::{self, Commitment, PublicKey, RangeProof, Secp256k1};
use crate::secp::{self, Commitment, RangeProof, Secp256k1};
use grin_core::core::hash::Hash;
use grin_util::ToHex;
use rand::RngCore;
@ -242,11 +242,6 @@ pub mod test_util {
None,
)
}
pub fn rand_pubkey() -> PublicKey {
let secp = Secp256k1::new();
PublicKey::from_secret_key(&secp, &secp::random_secret()).unwrap()
}
}
#[cfg(test)]

View file

@ -270,9 +270,7 @@ mod tests {
use crate::node::mock::MockGrinNode;
use crate::onion::test_util::{self, Hop};
use crate::onion::Onion;
use crate::secp::{
self, ComSignature, Commitment, PublicKey, RangeProof, Secp256k1, SecretKey,
};
use crate::secp::{self, ComSignature, Commitment, RangeProof, Secp256k1, SecretKey};
use crate::server::{Server, ServerImpl, SwapError};
use crate::store::{SwapData, SwapStatus, SwapStore};
use crate::types::Payload;
@ -283,6 +281,8 @@ mod tests {
use grin_core::global::{self, ChainTypes};
use std::net::TcpListener;
use std::sync::Arc;
use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::StaticSecret;
macro_rules! assert_error_type {
($result:expr, $error_type:pat) => {
@ -351,9 +351,8 @@ mod tests {
fee: u64,
proof: Option<RangeProof>,
) -> Hop {
let secp = Secp256k1::new();
Hop {
pubkey: PublicKey::from_secret_key(&secp, &server_key).unwrap(),
pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
payload: Payload {
excess: hop_excess.clone(),
fee: FeeFields::from(fee as u32),