-
Notifications
You must be signed in to change notification settings - Fork 219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: address points in issue #4138 and companions #4336
Changes from 4 commits
2f10996
a103aba
33da112
0b17607
da661f8
aee19eb
b6e29d5
95a9948
ed61369
20b9323
58c5e5a
cc743e3
5600252
c2bb431
2fa7515
27f1ab6
ea94992
c3a1d0a
52d8e8d
0801a20
065620a
c06b98c
c13ace2
b89f8af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,10 +28,16 @@ use chacha20::{ | |
Key, | ||
Nonce, | ||
}; | ||
use digest::{Digest, FixedOutput}; | ||
use chacha20poly1305::{ | ||
self, | ||
aead::{Aead, NewAead}, | ||
ChaCha20Poly1305, | ||
}; | ||
use rand::{rngs::OsRng, RngCore}; | ||
use tari_comms::types::{Challenge, CommsPublicKey}; | ||
use tari_crypto::{ | ||
hash::blake2::Blake256, | ||
hashing::{DomainSeparatedHasher, GenericHashDomain}, | ||
keys::{DiffieHellmanSharedSecret, PublicKey}, | ||
tari_utilities::{epoch_time::EpochTime, ByteArray}, | ||
}; | ||
|
@@ -43,17 +49,53 @@ use crate::{ | |
version::DhtProtocolVersion, | ||
}; | ||
|
||
const DOMAIN_SEPARATION_CHALLENGE_LABEL: &str = "com.tari.comms.dht.crypt.challenge"; | ||
const DOMAIN_SEPARATION_KEY_MESSAGE_LABEL: &str = "com.tari.comms.dht.crypt.challenge"; | ||
const DOMAIN_SEPARATION_KEY_SIGNATURE_LABEL: &str = "com.tari.comms.dht.crypt.key_signature"; | ||
|
||
#[derive(Debug, Clone, Zeroize)] | ||
#[zeroize(drop)] | ||
pub struct CipherKey(chacha20::Key); | ||
pub struct AuthenticatedCipherKey(chacha20poly1305::Key); | ||
|
||
// TODO: | ||
// 1. rename mac_challenge function | ||
// 2. check if output of generate_ecdh_secret has correct output size | ||
|
||
/// Generates a Diffie-Hellman secret `kx.G` as a `chacha20::Key` given secret scalar `k` and public key `P = x.G`. | ||
pub fn generate_ecdh_secret<PK>(secret_key: &PK::K, public_key: &PK) -> CipherKey | ||
pub fn generate_ecdh_secret<PK>(secret_key: &PK::K, public_key: &PK) -> [u8; 32] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This encoded output size is specific to the elliptic curve group used, and does not hold generically. |
||
where PK: PublicKey + DiffieHellmanSharedSecret<PK = PK> { | ||
// TODO: PK will still leave the secret in released memory. Implementing Zerioze on RistrettoPublicKey is not | ||
// currently possible because (Compressed)RistrettoPoint does not implement it. | ||
let k = PK::shared_secret(secret_key, public_key); | ||
CipherKey(*Key::from_slice(k.as_bytes())) | ||
let mut output = [0u8; 32]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This encoded size is specific to the elliptic curve group used, and is not generic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be useful to define a generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jorgeantonio21 Would like to have the well-defined DH return type, perhaps for a follow-up PR? |
||
|
||
output.copy_from_slice(k.as_bytes()); | ||
output | ||
} | ||
|
||
pub fn generate_key_message(data: &[u8]) -> CipherKey { | ||
// domain separated hash of data (e.g. ecdh shared secret) using hashing API | ||
let domain_separated_hash = | ||
DomainSeparatedHasher::<Challenge, GenericHashDomain>::new(DOMAIN_SEPARATION_KEY_MESSAGE_LABEL) | ||
.chain(data) | ||
.finalize() | ||
.into_vec(); | ||
|
||
// Domain separation uses Challenge = Blake256, thus its output has 32-byte length | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible to check and enforce this at compile time, in case later changes subtly break this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of a specific ECDH shared secret type would mean this function enforces this input type. |
||
CipherKey(*Key::from_slice(domain_separated_hash.as_bytes())) | ||
} | ||
|
||
pub fn generate_key_signature_for_authenticated_encryption(data: &[u8]) -> AuthenticatedCipherKey { | ||
// domain separated of data (e.g. ecdh shared secret) using hashing API | ||
let domain_separated_hash = | ||
DomainSeparatedHasher::<Blake256, GenericHashDomain>::new(DOMAIN_SEPARATION_KEY_SIGNATURE_LABEL) | ||
.chain(data) | ||
.finalize() | ||
.into_vec(); | ||
|
||
// Domain separation uses Challenge = Blake256, thus its output has 32-byte length | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to check and enforce this at compile time, in case later changes subtly break this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of a specific ECDH shared secret type would mean this function enforces this input type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I believe so. Noting that this will not really break sublty and will panic in
let domain_separated_hash =
DomainSeparatedHasher::<Blake256, GenericHashDomain>::new(DOMAIN_SEPARATION_KEY_SIGNATURE_LABEL)
.chain(data)
.finalize();
// Will not compile if Into<[u8;32]> is not implemented.
let output_array : [u8;32] = domain_separated_hash.into_inner().into(); This requires adding the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sdbondi How generic can that be made? It may be the case that other domain-separated hash function applications require output of a different length. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AaronFeickert That is handled by the Adding trait bounds are not necessary here as we use concrete types, but the trait bounds would be This became possible with this change (tari-project/tari-crypto#119) since we can now use let domain_separated_hash = DomainSeparatedHasher::<Blake256, GenericHashDomain>::new(DOMAIN_SEPARATION_KEY_SIGNATURE_LABEL) .chain(data) .finalize();
// Will not compile if Into<[u8;32]> is not implemented.
let output_array : [u8;32] = domain_separated_hash.into(); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So as the tari_crypto domain hasher code currently stands, you'd need to disambiguate between the functions on the hasher type and the let hasher = comms_dht_hash_domain_key_signature().chain(data);
let domain_separated_hash =<DomainSeparatedHasher<CommsChallenge, DHTCommsHashDomain> as Digest>::finalize(hasher);
let hash: [u8; 32] = domain_separated_hash.into(); Since we know that we're using Blake256 and breakage will be caught in tests - I think we can leave this as is |
||
AuthenticatedCipherKey(*chacha20poly1305::Key::from_slice(domain_separated_hash.as_bytes())) | ||
} | ||
|
||
/// Decrypts cipher text using ChaCha20 stream cipher given the cipher key and cipher text with integral nonce. | ||
|
@@ -73,6 +115,22 @@ pub fn decrypt(cipher_key: &CipherKey, cipher_text: &[u8]) -> Result<Vec<u8>, Dh | |
Ok(cipher_text) | ||
} | ||
|
||
pub fn decrypt_with_chacha20_poly1305( | ||
cipher_key: &AuthenticatedCipherKey, | ||
cipher_signature: &[u8], | ||
) -> Result<Vec<u8>, DhtOutboundError> { | ||
let nonce = [0u8; size_of::<chacha20poly1305::Nonce>()]; | ||
|
||
let nonce_ga = chacha20poly1305::Nonce::from_slice(&nonce); | ||
|
||
let cipher = ChaCha20Poly1305::new(&cipher_key.0); | ||
let decrypted_signature = cipher | ||
.decrypt(nonce_ga, cipher_signature) | ||
.map_err(|_| DhtOutboundError::CipherError(String::from("Authenticated decryption failed")))?; | ||
|
||
Ok(decrypted_signature) | ||
} | ||
|
||
/// Encrypt the plain text using the ChaCha20 stream cipher | ||
pub fn encrypt(cipher_key: &CipherKey, plain_text: &[u8]) -> Vec<u8> { | ||
let mut nonce = [0u8; size_of::<Nonce>()]; | ||
|
@@ -88,6 +146,28 @@ pub fn encrypt(cipher_key: &CipherKey, plain_text: &[u8]) -> Vec<u8> { | |
buf | ||
} | ||
|
||
/// Produces authenticated encryption of the signature using the ChaCha20-Poly1305 stream cipher, | ||
/// refer to https://docs.rs/chacha20poly1305/latest/chacha20poly1305/# for more details. | ||
/// Attention: as pointed in https://github.com/tari-project/tari/issues/4138, it is possible | ||
/// to use a fixed length Nonce, with homogeneous zero data, as this does not incur any security | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a fixed-length nonce, but a fixed nonce. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely ! Thank you ! |
||
/// vulnerabilities. However, such function is not intented to be used outside of dht scope | ||
pub fn encrypt_with_chacha20_poly1305( | ||
cipher_key: &AuthenticatedCipherKey, | ||
signature: &[u8], | ||
) -> Result<Vec<u8>, DhtOutboundError> { | ||
let nonce = [0u8; size_of::<chacha20poly1305::Nonce>()]; | ||
|
||
let nonce_ga = chacha20poly1305::Nonce::from_slice(&nonce); | ||
let cipher = ChaCha20Poly1305::new(&cipher_key.0); | ||
|
||
// length of encrypted equals signature.len() + 16 (the latter being the tag size for ChaCha20-poly1305) | ||
let encrypted = cipher | ||
.encrypt(nonce_ga, signature) | ||
.map_err(|_| DhtOutboundError::CipherError(String::from("Authenticated encryption failed")))?; | ||
|
||
Ok(encrypted) | ||
} | ||
|
||
/// Generates a 32-byte hashed challenge that commits to the message header and body | ||
pub fn create_message_challenge(header: &DhtMessageHeader, body: &[u8]) -> [u8; 32] { | ||
create_message_challenge_parts( | ||
|
@@ -111,14 +191,10 @@ pub fn create_message_challenge_parts( | |
ephemeral_public_key: Option<&CommsPublicKey>, | ||
body: &[u8], | ||
) -> [u8; 32] { | ||
let mut mac_challenge = Challenge::new(); | ||
mac_challenge.update(&protocol_version.as_bytes()); | ||
mac_challenge.update(destination.to_inner_bytes()); | ||
mac_challenge.update(&(message_type as i32).to_le_bytes()); | ||
mac_challenge.update(&flags.bits().to_le_bytes()); | ||
// get byte representation of `expires` input | ||
let expires = expires.map(|t| t.as_u64().to_le_bytes()).unwrap_or_default(); | ||
mac_challenge.update(&expires); | ||
|
||
// get byte representation of `ephemeral_public_key` | ||
let e_pk = ephemeral_public_key | ||
.map(|e_pk| { | ||
let mut buf = [0u8; 32]; | ||
|
@@ -127,10 +203,25 @@ pub fn create_message_challenge_parts( | |
buf | ||
}) | ||
.unwrap_or_default(); | ||
mac_challenge.update(&e_pk); | ||
|
||
mac_challenge.update(&body); | ||
mac_challenge.finalize_fixed().into() | ||
// we digest the given data into a domain independent hash function to produce a signature | ||
// use of the hashing API for domain separation and deal with variable length input | ||
let domain_separated_hash = | ||
DomainSeparatedHasher::<Challenge, GenericHashDomain>::new(DOMAIN_SEPARATION_CHALLENGE_LABEL) | ||
.chain(&protocol_version.as_bytes()) | ||
.chain(destination.to_inner_bytes()) | ||
.chain(&(message_type as i32).to_le_bytes()) | ||
.chain(&flags.bits().to_le_bytes()) | ||
.chain(&expires) | ||
.chain(&e_pk) | ||
.chain(&body) | ||
.finalize() | ||
.into_vec(); | ||
|
||
let mut output = [0u8; 32]; | ||
// the use of Challenge bind to Blake256, should always produce a 32-byte length output data | ||
output.copy_from_slice(domain_separated_hash.as_slice()); | ||
output | ||
} | ||
|
||
#[cfg(test)] | ||
|
@@ -160,4 +251,96 @@ mod test { | |
let secret_msg = "Last enemy position 0830h AJ 9863".as_bytes().to_vec(); | ||
assert_eq!(plain_text, secret_msg); | ||
} | ||
|
||
#[test] | ||
fn sanity_check() { | ||
let domain_separated_hash = | ||
DomainSeparatedHasher::<Blake256, GenericHashDomain>::new(DOMAIN_SEPARATION_KEY_SIGNATURE_LABEL) | ||
.chain(&[10, 12, 13, 82, 93, 101, 87, 28, 27, 17, 11, 35, 43]) | ||
.finalize() | ||
.into_vec(); | ||
|
||
// Domain separation uses Challenge = Blake256, thus its output has 32-byte length | ||
let key = AuthenticatedCipherKey(*chacha20poly1305::Key::from_slice(domain_separated_hash.as_bytes())); | ||
|
||
let signature = b"Top secret message, handle with care".as_slice(); | ||
let n = signature.len(); | ||
let nonce = [0u8; size_of::<chacha20poly1305::Nonce>()]; | ||
|
||
let nonce_ga = chacha20poly1305::Nonce::from_slice(&nonce); | ||
let cipher = ChaCha20Poly1305::new(&key.0); | ||
|
||
let encrypted = cipher | ||
.encrypt(nonce_ga, signature) | ||
.map_err(|_| DhtOutboundError::CipherError(String::from("Authenticated encryption failed"))) | ||
.unwrap(); | ||
|
||
assert_eq!(encrypted.len(), n + 16); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could remove the magic number by getting the tag size programmatically. |
||
} | ||
|
||
#[test] | ||
fn decryption_fails_in_case_tag_is_manipulated() { | ||
let (sk, pk) = CommsPublicKey::random_keypair(&mut OsRng); | ||
let key_data = generate_ecdh_secret(&sk, &pk); | ||
let key = generate_key_signature_for_authenticated_encryption(&key_data); | ||
|
||
let signature = b"Top secret message, handle with care".as_slice(); | ||
|
||
let mut encrypted = encrypt_with_chacha20_poly1305(&key, signature).unwrap(); | ||
|
||
// sanity check to validate that encrypted.len() = signature.len() + 16 | ||
assert_eq!(encrypted.len(), signature.len() + 16); | ||
|
||
// manipulate tag and check that decryption fails | ||
let n = encrypted.len(); | ||
encrypted[n - 1] += 1; | ||
|
||
// decryption should fail | ||
assert!(decrypt_with_chacha20_poly1305(&key, encrypted.as_slice()) | ||
.unwrap_err() | ||
.to_string() | ||
.contains("Authenticated decryption failed")); | ||
} | ||
|
||
#[test] | ||
fn decryption_fails_in_case_body_message_is_manipulated() { | ||
let (sk, pk) = CommsPublicKey::random_keypair(&mut OsRng); | ||
let key_data = generate_ecdh_secret(&sk, &pk); | ||
let key = generate_key_signature_for_authenticated_encryption(&key_data); | ||
|
||
let signature = b"Top secret message, handle with care".as_slice(); | ||
|
||
let mut encrypted = encrypt_with_chacha20_poly1305(&key, signature).unwrap(); | ||
|
||
// manipulate encrypted message body and check that decryption fails | ||
encrypted[0] += 1; | ||
|
||
// decryption should fail | ||
assert!(decrypt_with_chacha20_poly1305(&key, encrypted.as_slice()) | ||
.unwrap_err() | ||
.to_string() | ||
.contains("Authenticated decryption failed")); | ||
} | ||
|
||
#[test] | ||
fn decryption_fails_if_message_sned_to_incorrect_node() { | ||
let (sk, pk) = CommsPublicKey::random_keypair(&mut OsRng); | ||
let (other_sk, other_pk) = CommsPublicKey::random_keypair(&mut OsRng); | ||
|
||
let key_data = generate_ecdh_secret(&sk, &pk); | ||
let other_key_data = generate_ecdh_secret(&other_sk, &other_pk); | ||
|
||
let key = generate_key_signature_for_authenticated_encryption(&key_data); | ||
let other_key = generate_key_signature_for_authenticated_encryption(&other_key_data); | ||
|
||
let signature = b"Top secret message, handle with care".as_slice(); | ||
|
||
let encrypted = encrypt_with_chacha20_poly1305(&key, signature).unwrap(); | ||
|
||
// decryption should fail | ||
assert!(decrypt_with_chacha20_poly1305(&other_key, encrypted.as_slice()) | ||
.unwrap_err() | ||
.to_string() | ||
.contains("Authenticated decryption failed")); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Domain separators must be unique.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely, changed !