From 016165ff8e05a7f2b9c66765db569432c6193c60 Mon Sep 17 00:00:00 2001 From: lbl <1791778603@qq.com> Date: Thu, 20 Aug 2026 22:40:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(crypto):=20flags/reserved=20=E5=A4=B4?= =?UTF-8?q?=E9=83=A8=E5=AD=97=E8=8A=82=E7=BA=B3=E5=85=A5=20AAD=20=E8=AE=A4?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chacha20_poly1305 加解密 AAD 为空,flags 字节(COMPRESSED/FEC/GATEWAY) 未经认证,中间人可翻转标志位造成不可检测的丢包/语义篡改。 AAD 取头部 byte2(flags)/byte3(reserved);ttl(byte1) 中继转发时会递减, 不纳入 AAD 以保持中继兼容。 注意:此改动与旧版本(AAD 为空)不兼容,新旧节点间无法互通加密包。 测试: - test_tampered_flags_rejected: 翻转 flags 必须解密失败 - test_ttl_change_still_decrypts: ttl 递减不影响解密 --- vnt-core/src/crypto/chacha20_poly1305.rs | 66 ++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/vnt-core/src/crypto/chacha20_poly1305.rs b/vnt-core/src/crypto/chacha20_poly1305.rs index 594126b..6bbf5e1 100644 --- a/vnt-core/src/crypto/chacha20_poly1305.rs +++ b/vnt-core/src/crypto/chacha20_poly1305.rs @@ -71,6 +71,18 @@ impl PacketCrypto { Ok(nonce12) } + /// AAD 覆盖头部 byte2(flags)/byte3(reserved)。 + /// flags(COMPRESSED/FEC/GATEWAY)只由发送方设置、传输中不会被修改, + /// 必须纳入认证,否则中间人可翻转标志位造成不可检测的丢包/语义篡改; + /// ttl(byte1) 在中继转发时会递减,不能纳入 AAD。 + fn make_aad>(pkt: &NetPacket) -> [u8; 2] { + let buf = pkt.buffer(); + if buf.len() < HEAD_LENGTH { + return [0; 2]; + } + [buf[2], buf[3]] + } + /// 原地加密(in-place) /// payload 后需要预留16字节用于存放 tag pub fn encrypt_in_place + AsMut<[u8]>>( @@ -82,6 +94,7 @@ impl PacketCrypto { let seq = self.seq.fetch_add(1, Ordering::Relaxed); pkt.set_seq(seq); let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?); + let aad = Aad::from(Self::make_aad(pkt)); let payload = pkt.payload_mut(); let payload_len = payload.len() - TAG_LEN; // 实际 payload 长度(不含 tag 预留空间) @@ -89,7 +102,7 @@ impl PacketCrypto { // 只加密实际的 payload 部分 let tag = self .key - .seal_in_place_separate_tag(nonce, Aad::empty(), &mut payload[..payload_len]) + .seal_in_place_separate_tag(nonce, aad, &mut payload[..payload_len]) .map_err(|_| io::Error::other("encrypt failed"))?; // 将 tag 写入 payload 后的预留空间 @@ -104,12 +117,13 @@ impl PacketCrypto { pkt: &mut NetPacket, ) -> io::Result { let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?); + let aad = Aad::from(Self::make_aad(pkt)); let payload_with_tag = pkt.payload_mut(); let plaintext = self .key - .open_in_place(nonce, Aad::empty(), payload_with_tag) + .open_in_place(nonce, aad, payload_with_tag) .map_err(|_| io::Error::other("decrypt failed"))?; Ok(plaintext.len()) } @@ -222,7 +236,8 @@ mod tests { } /// nonce 完全由包自带的头部字节决定,与发送端状态无关: - /// 旧版本(seq 恒为 0)发出的包,新版本必须能正常解密,反之亦然。 + /// 未启用 AAD 的旧版本发出的包(flags/reserved 为 0 时 AAD 语义不同), + /// 这里用同一 AAD 逻辑模拟对端,验证 nonce 只依赖头部、与发送端 seq 状态无关。 #[test] fn test_cross_version_compat() { let key = [7u8; 32]; @@ -235,11 +250,12 @@ mod tests { let original: Vec = pkt.buffer()[HEAD_LENGTH..HEAD_LENGTH + 20].to_vec(); pkt.set_seq(0); let nonce = Nonce::assume_unique_for_key(peer.make_nonce(&pkt).unwrap()); + let aad = Aad::from(PacketCrypto::make_aad(&pkt)); let payload = pkt.payload_mut(); let payload_len = payload.len() - TAG_LEN; let tag = peer .key - .seal_in_place_separate_tag(nonce, Aad::empty(), &mut payload[..payload_len]) + .seal_in_place_separate_tag(nonce, aad, &mut payload[..payload_len]) .unwrap(); payload[payload_len..payload_len + TAG_LEN].copy_from_slice(tag.as_ref()); @@ -258,4 +274,46 @@ mod tests { &original2[..] ); } + + /// AAD 覆盖 flags(byte2):中间人翻转 COMPRESSED/FEC/GATEWAY 标志位 + /// 必须导致解密失败,而不是被静默接受。 + #[test] + fn test_tampered_flags_rejected() { + let crypto = PacketCrypto::new([7u8; 32]); + + let mut pkt = build_test_packet(20); + crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); + + // 翻转 flags 字节(模拟中间人篡改) + pkt.set_fec_flag(true); + + assert!( + crypto.decrypt_in_place(&mut pkt).is_err(), + "tampered flags must fail authentication" + ); + } + + /// ttl(byte1) 在中继转发时会递减,不属于 AAD: + /// 转发后 ttl 变化的包必须仍能正常解密。 + #[test] + fn test_ttl_change_still_decrypts() { + let crypto = PacketCrypto::new([7u8; 32]); + + let payload_len = 20; + let mut pkt = build_test_packet(payload_len); + pkt.set_ttl(15); // 初始 ttl + let original: Vec = pkt.buffer()[HEAD_LENGTH..HEAD_LENGTH + payload_len].to_vec(); + crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); + + // 模拟中继递减 ttl + pkt.set_ttl(14); + + crypto + .decrypt_in_place(&mut pkt) + .expect("ttl change must not break decryption"); + assert_eq!( + &pkt.buffer()[HEAD_LENGTH..HEAD_LENGTH + payload_len], + &original[..] + ); + } }