refactor(crypto): nonce 与 AAD 职责分离
nonce 只承担唯一性职责:seq(随机起始计数器,现占满 4 字节, 每流约 43 亿包才回绕)+ src + dst;不再用 msg_type 覆盖 seq 低位。 AAD 承担认证职责:msg_type(byte0)/flags(byte2)/reserved(byte3), 即所有传输中不变且不参与 nonce 的头部字节;ttl(byte1) 中继会递减, 仍不认证。 线上协议头部布局不变,但 nonce 推导方式改变,与旧版本 (含 48ba12d)加密不互通。 新增测试 test_tampered_msg_type_rejected:篡改 msg_type 必须解密失败。
This commit is contained in:
@@ -47,7 +47,10 @@ impl PacketCrypto {
|
||||
key_bytes.copy_from_slice(hash.as_ref());
|
||||
Self::new(key_bytes)
|
||||
}
|
||||
/// 根据包头生成 12 字节 nonce
|
||||
/// 根据包头生成 12 字节 nonce。
|
||||
/// nonce 只承担"唯一性"职责:seq(随机起始计数器)+ src + dst,
|
||||
/// 三者构成每个 (src, dst) 流内不重复的 96 位值;
|
||||
/// 头部其余字段的完整性认证由 AAD 负责,与 nonce 无关。
|
||||
pub fn make_nonce<B: AsRef<[u8]>>(&self, pkt: &NetPacket<B>) -> io::Result<[u8; 12]> {
|
||||
let buf = pkt.buffer();
|
||||
|
||||
@@ -57,7 +60,6 @@ impl PacketCrypto {
|
||||
"buffer too small",
|
||||
));
|
||||
}
|
||||
let msg_type = buf[0];
|
||||
let seq = &buf[4..8];
|
||||
let src = &buf[8..12];
|
||||
let dst = &buf[12..16];
|
||||
@@ -66,21 +68,21 @@ impl PacketCrypto {
|
||||
nonce12[0..4].copy_from_slice(seq);
|
||||
nonce12[4..8].copy_from_slice(dst);
|
||||
nonce12[8..12].copy_from_slice(src);
|
||||
nonce12[0] = msg_type;
|
||||
|
||||
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] {
|
||||
/// AAD 承担"认证"职责:覆盖传输中不变、但不参与 nonce 的头部字节
|
||||
/// byte0(msg_type)/byte2(flags)/byte3(reserved)。
|
||||
/// msg_type 与 flags(COMPRESSED/FEC/GATEWAY)只由发送方设置、
|
||||
/// 传输中不会被修改,必须纳入认证,否则中间人可翻转造成不可检测的
|
||||
/// 丢包/语义篡改;ttl(byte1) 在中继转发时会递减,不能纳入 AAD。
|
||||
fn make_aad<B: AsRef<[u8]>>(pkt: &NetPacket<B>) -> [u8; 3] {
|
||||
let buf = pkt.buffer();
|
||||
if buf.len() < HEAD_LENGTH {
|
||||
return [0; 2];
|
||||
return [0; 3];
|
||||
}
|
||||
[buf[2], buf[3]]
|
||||
[buf[0], buf[2], buf[3]]
|
||||
}
|
||||
|
||||
/// 原地加密(in-place)
|
||||
@@ -89,8 +91,8 @@ impl PacketCrypto {
|
||||
&self,
|
||||
pkt: &mut NetPacket<B>,
|
||||
) -> io::Result<()> {
|
||||
// 为每个出站包分配递增 seq,保证 (msg_type, src, dst) 相同包之间 nonce 不重复
|
||||
// (nonce 中 seq 占 3 字节,同一四元组约 1600 万个包后才会回绕)
|
||||
// 为每个出站包分配递增 seq,保证同一 (src, dst) 流内 nonce 不重复
|
||||
// (seq 占满 4 字节,约 43 亿个包后才回绕)
|
||||
let seq = self.seq.fetch_add(1, Ordering::Relaxed);
|
||||
pkt.set_seq(seq);
|
||||
let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?);
|
||||
@@ -132,6 +134,7 @@ impl PacketCrypto {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::ip_packet_protocol::MsgType;
|
||||
use bytes::BytesMut;
|
||||
|
||||
// 用于构造一个简单的 NetPacket,包含头 16 字节 + payload + 16 字节 TAG 预留
|
||||
@@ -235,9 +238,8 @@ mod tests {
|
||||
assert_eq!(pkt1.seq() + 1, pkt2.seq());
|
||||
}
|
||||
|
||||
/// nonce 完全由包自带的头部字节决定,与发送端状态无关:
|
||||
/// 未启用 AAD 的旧版本发出的包(flags/reserved 为 0 时 AAD 语义不同),
|
||||
/// 这里用同一 AAD 逻辑模拟对端,验证 nonce 只依赖头部、与发送端 seq 状态无关。
|
||||
/// nonce 与 AAD 完全由包自带的头部字节推导,与发送端状态无关:
|
||||
/// 即使对端用自己的 seq 状态发包,本端仅凭头部即可正确解密。
|
||||
#[test]
|
||||
fn test_cross_version_compat() {
|
||||
let key = [7u8; 32];
|
||||
@@ -293,6 +295,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。
|
||||
#[test]
|
||||
fn test_tampered_msg_type_rejected() {
|
||||
let crypto = PacketCrypto::new([7u8; 32]);
|
||||
|
||||
let mut pkt = build_test_packet(20);
|
||||
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
||||
|
||||
pkt.set_msg_type(MsgType::Pong);
|
||||
|
||||
assert!(
|
||||
crypto.decrypt_in_place(&mut pkt).is_err(),
|
||||
"tampered msg_type must fail authentication"
|
||||
);
|
||||
}
|
||||
|
||||
/// ttl(byte1) 在中继转发时会递减,不属于 AAD:
|
||||
/// 转发后 ttl 变化的包必须仍能正常解密。
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user