fix(crypto): flags/reserved 头部字节纳入 AAD 认证
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 递减不影响解密
This commit is contained in:
@@ -71,6 +71,18 @@ impl PacketCrypto {
|
|||||||
Ok(nonce12)
|
Ok(nonce12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AAD 覆盖头部 byte2(flags)/byte3(reserved)。
|
||||||
|
/// flags(COMPRESSED/FEC/GATEWAY)只由发送方设置、传输中不会被修改,
|
||||||
|
/// 必须纳入认证,否则中间人可翻转标志位造成不可检测的丢包/语义篡改;
|
||||||
|
/// ttl(byte1) 在中继转发时会递减,不能纳入 AAD。
|
||||||
|
fn make_aad<B: AsRef<[u8]>>(pkt: &NetPacket<B>) -> [u8; 2] {
|
||||||
|
let buf = pkt.buffer();
|
||||||
|
if buf.len() < HEAD_LENGTH {
|
||||||
|
return [0; 2];
|
||||||
|
}
|
||||||
|
[buf[2], buf[3]]
|
||||||
|
}
|
||||||
|
|
||||||
/// 原地加密(in-place)
|
/// 原地加密(in-place)
|
||||||
/// payload 后需要预留16字节用于存放 tag
|
/// payload 后需要预留16字节用于存放 tag
|
||||||
pub fn encrypt_in_place<B: AsRef<[u8]> + AsMut<[u8]>>(
|
pub fn encrypt_in_place<B: AsRef<[u8]> + AsMut<[u8]>>(
|
||||||
@@ -82,6 +94,7 @@ impl PacketCrypto {
|
|||||||
let seq = self.seq.fetch_add(1, Ordering::Relaxed);
|
let seq = self.seq.fetch_add(1, Ordering::Relaxed);
|
||||||
pkt.set_seq(seq);
|
pkt.set_seq(seq);
|
||||||
let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?);
|
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 = pkt.payload_mut();
|
||||||
let payload_len = payload.len() - TAG_LEN; // 实际 payload 长度(不含 tag 预留空间)
|
let payload_len = payload.len() - TAG_LEN; // 实际 payload 长度(不含 tag 预留空间)
|
||||||
@@ -89,7 +102,7 @@ impl PacketCrypto {
|
|||||||
// 只加密实际的 payload 部分
|
// 只加密实际的 payload 部分
|
||||||
let tag = self
|
let tag = self
|
||||||
.key
|
.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"))?;
|
.map_err(|_| io::Error::other("encrypt failed"))?;
|
||||||
|
|
||||||
// 将 tag 写入 payload 后的预留空间
|
// 将 tag 写入 payload 后的预留空间
|
||||||
@@ -104,12 +117,13 @@ impl PacketCrypto {
|
|||||||
pkt: &mut NetPacket<B>,
|
pkt: &mut NetPacket<B>,
|
||||||
) -> io::Result<usize> {
|
) -> io::Result<usize> {
|
||||||
let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?);
|
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 payload_with_tag = pkt.payload_mut();
|
||||||
|
|
||||||
let plaintext = self
|
let plaintext = self
|
||||||
.key
|
.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"))?;
|
.map_err(|_| io::Error::other("decrypt failed"))?;
|
||||||
Ok(plaintext.len())
|
Ok(plaintext.len())
|
||||||
}
|
}
|
||||||
@@ -222,7 +236,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// nonce 完全由包自带的头部字节决定,与发送端状态无关:
|
/// nonce 完全由包自带的头部字节决定,与发送端状态无关:
|
||||||
/// 旧版本(seq 恒为 0)发出的包,新版本必须能正常解密,反之亦然。
|
/// 未启用 AAD 的旧版本发出的包(flags/reserved 为 0 时 AAD 语义不同),
|
||||||
|
/// 这里用同一 AAD 逻辑模拟对端,验证 nonce 只依赖头部、与发送端 seq 状态无关。
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cross_version_compat() {
|
fn test_cross_version_compat() {
|
||||||
let key = [7u8; 32];
|
let key = [7u8; 32];
|
||||||
@@ -235,11 +250,12 @@ mod tests {
|
|||||||
let original: Vec<u8> = pkt.buffer()[HEAD_LENGTH..HEAD_LENGTH + 20].to_vec();
|
let original: Vec<u8> = pkt.buffer()[HEAD_LENGTH..HEAD_LENGTH + 20].to_vec();
|
||||||
pkt.set_seq(0);
|
pkt.set_seq(0);
|
||||||
let nonce = Nonce::assume_unique_for_key(peer.make_nonce(&pkt).unwrap());
|
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 = pkt.payload_mut();
|
||||||
let payload_len = payload.len() - TAG_LEN;
|
let payload_len = payload.len() - TAG_LEN;
|
||||||
let tag = peer
|
let tag = peer
|
||||||
.key
|
.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();
|
.unwrap();
|
||||||
payload[payload_len..payload_len + TAG_LEN].copy_from_slice(tag.as_ref());
|
payload[payload_len..payload_len + TAG_LEN].copy_from_slice(tag.as_ref());
|
||||||
|
|
||||||
@@ -258,4 +274,46 @@ mod tests {
|
|||||||
&original2[..]
|
&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<u8> = 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[..]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user