diff --git a/crypto/dkg/src/encryption.rs b/crypto/dkg/src/encryption.rs index 4bcb7802..e4aab0dd 100644 --- a/crypto/dkg/src/encryption.rs +++ b/crypto/dkg/src/encryption.rs @@ -98,11 +98,13 @@ fn ecdh(private: &Zeroizing, public: C::G) -> Zeroizing(dst: &'static [u8], ecdh: &Zeroizing) -> ChaCha20 { +fn cipher(context: &str, ecdh: &Zeroizing) -> ChaCha20 { // Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly // TODO: https://github.com/serai-dex/serai/issues/151 let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2"); - transcript.domain_separate(dst); + transcript.append_message(b"context", context.as_bytes()); + + transcript.domain_separate(b"encryption_key"); let mut ecdh = ecdh.to_bytes(); transcript.append_message(b"shared_key", ecdh.as_ref()); @@ -132,7 +134,7 @@ fn cipher(dst: &'static [u8], ecdh: &Zeroizing) -> ChaCha2 fn encrypt( rng: &mut R, - dst: &'static [u8], + context: &str, from: Participant, to: C::G, mut msg: Zeroizing, @@ -149,7 +151,7 @@ fn encrypt( // Generate a new key for this message, satisfying cipher's requirement of distinct keys per // message, and enabling revealing this message without revealing any others let key = Zeroizing::new(C::random_nonzero_F(rng)); - cipher::(dst, &ecdh::(&key, to)).apply_keystream(msg.as_mut().as_mut()); + cipher::(context, &ecdh::(&key, to)).apply_keystream(msg.as_mut().as_mut()); let pub_key = C::generator() * key.deref(); let nonce = Zeroizing::new(C::random_nonzero_F(rng)); @@ -159,7 +161,7 @@ fn encrypt( pop: SchnorrSignature::sign( &key, nonce, - pop_challenge::(pub_nonce, pub_key, from, msg.deref().as_ref()), + pop_challenge::(context, pub_nonce, pub_key, from, msg.deref().as_ref()), ), msg, } @@ -192,7 +194,12 @@ impl EncryptedMessage { } #[cfg(test)] - pub(crate) fn invalidate_msg(&mut self, rng: &mut R, from: Participant) { + pub(crate) fn invalidate_msg( + &mut self, + rng: &mut R, + context: &str, + from: Participant, + ) { // Invalidate the message by specifying a new key/Schnorr PoP // This will cause all initial checks to pass, yet a decrypt to gibberish let key = Zeroizing::new(C::random_nonzero_F(rng)); @@ -203,7 +210,7 @@ impl EncryptedMessage { self.pop = SchnorrSignature::sign( &key, nonce, - pop_challenge::(pub_nonce, pub_key, from, self.msg.deref().as_ref()), + pop_challenge::(context, pub_nonce, pub_key, from, self.msg.deref().as_ref()), ); } @@ -212,7 +219,7 @@ impl EncryptedMessage { pub(crate) fn invalidate_share_serialization( &mut self, rng: &mut R, - dst: &'static [u8], + context: &str, from: Participant, to: C::G, ) { @@ -228,7 +235,7 @@ impl EncryptedMessage { assert!(!bool::from(C::F::from_repr(repr).is_some())); self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref()); - *self = encrypt(rng, dst, from, to, self.msg.clone()); + *self = encrypt(rng, context, from, to, self.msg.clone()); } // Assumes the encrypted message is a secret share. @@ -236,7 +243,7 @@ impl EncryptedMessage { pub(crate) fn invalidate_share_value( &mut self, rng: &mut R, - dst: &'static [u8], + context: &str, from: Participant, to: C::G, ) { @@ -245,7 +252,7 @@ impl EncryptedMessage { // Assumes the share isn't randomly 1 let repr = C::F::one().to_repr(); self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref()); - *self = encrypt(rng, dst, from, to, self.msg.clone()); + *self = encrypt(rng, context, from, to, self.msg.clone()); } } @@ -292,8 +299,18 @@ impl EncryptionKeyProof { // This doesn't need to take the msg. It just doesn't hurt as an extra layer. // This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no // root of trust other than their existence in the assumed-to-exist external authenticated channel. -fn pop_challenge(nonce: C::G, key: C::G, sender: Participant, msg: &[u8]) -> C::F { +fn pop_challenge( + context: &str, + nonce: C::G, + key: C::G, + sender: Participant, + msg: &[u8], +) -> C::F { let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2"); + transcript.append_message(b"context", context.as_bytes()); + + transcript.domain_separate(b"proof_of_possession"); + transcript.append_message(b"nonce", nonce.to_bytes()); transcript.append_message(b"key", key.to_bytes()); // This is sufficient to prevent the attack this is meant to stop @@ -306,8 +323,10 @@ fn pop_challenge(nonce: C::G, key: C::G, sender: Participant, ms C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr")) } -fn encryption_key_transcript() -> RecommendedTranscript { - RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2") +fn encryption_key_transcript(context: &str) -> RecommendedTranscript { + let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2"); + transcript.append_message(b"context", context.as_bytes()); + transcript } #[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] @@ -321,7 +340,7 @@ pub(crate) enum DecryptionError { // A simple box for managing encryption. #[derive(Clone)] pub(crate) struct Encryption { - dst: &'static [u8], + context: String, i: Participant, enc_key: Zeroizing, enc_pub_key: C::G, @@ -339,14 +358,10 @@ impl Zeroize for Encryption { } impl Encryption { - pub(crate) fn new( - dst: &'static [u8], - i: Participant, - rng: &mut R, - ) -> Self { + pub(crate) fn new(context: String, i: Participant, rng: &mut R) -> Self { let enc_key = Zeroizing::new(C::random_nonzero_F(rng)); Self { - dst, + context, i, enc_pub_key: C::generator() * enc_key.deref(), enc_key, @@ -376,7 +391,7 @@ impl Encryption { participant: Participant, msg: Zeroizing, ) -> EncryptedMessage { - encrypt(rng, self.dst, self.i, self.enc_keys[&participant], msg) + encrypt(rng, &self.context, self.i, self.enc_keys[&participant], msg) } pub(crate) fn decrypt( @@ -394,18 +409,18 @@ impl Encryption { batch, batch_id, msg.key, - pop_challenge::(msg.pop.R, msg.key, from, msg.msg.deref().as_ref()), + pop_challenge::(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()), ); let key = ecdh::(&self.enc_key, msg.key); - cipher::(self.dst, &key).apply_keystream(msg.msg.as_mut().as_mut()); + cipher::(&self.context, &key).apply_keystream(msg.msg.as_mut().as_mut()); ( msg.msg, EncryptionKeyProof { key, dleq: DLEqProof::prove( rng, - &mut encryption_key_transcript(), + &mut encryption_key_transcript(&self.context), &[C::generator(), msg.key], &self.enc_key, ), @@ -423,10 +438,10 @@ impl Encryption { // There's no encryption key proof if the accusation is of an invalid signature proof: Option>, ) -> Result, DecryptionError> { - if !msg - .pop - .verify(msg.key, pop_challenge::(msg.pop.R, msg.key, from, msg.msg.deref().as_ref())) - { + if !msg.pop.verify( + msg.key, + pop_challenge::(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()), + ) { Err(DecryptionError::InvalidSignature)?; } @@ -435,13 +450,13 @@ impl Encryption { proof .dleq .verify( - &mut encryption_key_transcript(), + &mut encryption_key_transcript(&self.context), &[C::generator(), msg.key], &[self.enc_keys[&decryptor], *proof.key], ) .map_err(|_| DecryptionError::InvalidProof)?; - cipher::(self.dst, &proof.key).apply_keystream(msg.msg.as_mut().as_mut()); + cipher::(&self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut()); Ok(msg.msg) } else { Err(DecryptionError::InvalidProof) diff --git a/crypto/dkg/src/frost.rs b/crypto/dkg/src/frost.rs index b8b9b449..054d7bd9 100644 --- a/crypto/dkg/src/frost.rs +++ b/crypto/dkg/src/frost.rs @@ -132,7 +132,7 @@ impl KeyGenMachine { ); // Additionally create an encryption mechanism to protect the secret shares - let encryption = Encryption::new(b"FROST", self.params.i, rng); + let encryption = Encryption::new(self.context.clone(), self.params.i, rng); // Step 4: Broadcast let msg = diff --git a/crypto/dkg/src/tests/frost.rs b/crypto/dkg/src/tests/frost.rs index ad8327c1..39b2f658 100644 --- a/crypto/dkg/src/tests/frost.rs +++ b/crypto/dkg/src/tests/frost.rs @@ -13,6 +13,8 @@ use crate::{ type FrostEncryptedMessage = EncryptedMessage::F>>; type FrostSecretShares = HashMap>; +const CONTEXT: &str = "DKG Test Key Generation"; + // Commit, then return enc key and shares #[allow(clippy::type_complexity)] fn commit_enc_keys_and_shares( @@ -27,7 +29,7 @@ fn commit_enc_keys_and_shares( let mut enc_keys = HashMap::new(); for i in (1 ..= PARTICIPANTS).map(Participant) { let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(); - let machine = KeyGenMachine::::new(params, "DKG Test Key Generation".to_string()); + let machine = KeyGenMachine::::new(params, CONTEXT.to_string()); let (machine, these_commitments) = machine.generate_coefficients(rng); machines.insert(i, machine); @@ -179,7 +181,12 @@ mod literal { // We then malleate 1's blame proof, so 1 ends up malicious // Doesn't simply invalidate the PoP as that won't have a blame statement // By mutating the encrypted data, we do ensure a blame statement is created - secret_shares.get_mut(&TWO).unwrap().get_mut(&ONE).unwrap().invalidate_msg(&mut OsRng, TWO); + secret_shares + .get_mut(&TWO) + .unwrap() + .get_mut(&ONE) + .unwrap() + .invalidate_msg(&mut OsRng, CONTEXT, TWO); let mut blame = None; let machines = machines @@ -209,7 +216,12 @@ mod literal { let (mut machines, _, mut secret_shares) = commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng); - secret_shares.get_mut(&TWO).unwrap().get_mut(&ONE).unwrap().invalidate_msg(&mut OsRng, TWO); + secret_shares + .get_mut(&TWO) + .unwrap() + .get_mut(&ONE) + .unwrap() + .invalidate_msg(&mut OsRng, CONTEXT, TWO); let mut blame = None; let machines = machines @@ -240,7 +252,7 @@ mod literal { secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization( &mut OsRng, - b"FROST", + CONTEXT, ONE, enc_keys[&TWO], ); @@ -273,7 +285,7 @@ mod literal { secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value( &mut OsRng, - b"FROST", + CONTEXT, ONE, enc_keys[&TWO], );