SMB Server Phase 2: VFS backend build fix + integration test
- Add VfsFile: Send supertrait for Mutex compatibility - Fix SmbServerCommand: struct → Subcommand enum with Start variant - Fix tracing_subscriber::init() → try_init() to avoid panic when logger already initialized - Fix CLI subcommand name: smb-server → smb-start (flatten naming) - Add #[command(name = "smb-start")] for CLI disambiguation - Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs) - Remove unused VfsFile imports (webdav.rs, scp_handler.rs) - Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
+259
@@ -0,0 +1,259 @@
|
||||
//! SMB2/3 message signing per MS-SMB2 §3.1.4.1.
|
||||
//!
|
||||
//! Two algorithms are supported:
|
||||
//! 1. **HMAC-SHA-256** for SMB 2.0.2 / 2.1 / 3.0 negotiating without 3.x
|
||||
//! signing.
|
||||
//! 2. **AES-CMAC** for SMB 3.0+.
|
||||
//!
|
||||
//! Both produce a 16-byte signature that lives at bytes 48..64 of the SMB2
|
||||
//! header (the `Signature` field, MS-SMB2 §2.2.1.2).
|
||||
//!
|
||||
//! Algorithm:
|
||||
//! 1. Zero out bytes 48..64 of the message.
|
||||
//! 2. Compute MAC over the **entire** message (header + body).
|
||||
//! 3. Place the first 16 bytes of MAC at bytes 48..64.
|
||||
|
||||
use aes::Aes128;
|
||||
use cmac::Cmac;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::proto::error::{ProtoError, ProtoResult};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
type CmacAes128 = Cmac<Aes128>;
|
||||
|
||||
/// SMB2 header is 64 bytes; the 16-byte signature field starts at offset 48.
|
||||
const SIG_OFF: usize = 48;
|
||||
const SIG_LEN: usize = 16;
|
||||
const SMB2_HEADER_LEN: usize = 64;
|
||||
|
||||
/// Which signing algorithm to use for a given session/dialect.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SigningAlgo {
|
||||
/// HMAC-SHA-256, used by SMB 2.x.
|
||||
HmacSha256,
|
||||
/// AES-CMAC over AES-128, used by SMB 3.0+.
|
||||
AesCmac,
|
||||
}
|
||||
|
||||
/// Compute the 16-byte MAC over `msg` as if the SMB2 signature field were
|
||||
/// zeroed, without copying the whole message.
|
||||
fn compute_mac_zeroed_signature(msg: &[u8], key: &[u8; 16], algo: SigningAlgo) -> [u8; SIG_LEN] {
|
||||
let mut out = [0u8; SIG_LEN];
|
||||
let zero_signature = [0u8; SIG_LEN];
|
||||
let prefix = &msg[..SIG_OFF];
|
||||
let suffix = &msg[SIG_OFF + SIG_LEN..];
|
||||
|
||||
match algo {
|
||||
SigningAlgo::HmacSha256 => {
|
||||
let mut mac = <HmacSha256 as Mac>::new_from_slice(key)
|
||||
.expect("HMAC-SHA-256 accepts keys of any length");
|
||||
mac.update(prefix);
|
||||
mac.update(&zero_signature);
|
||||
mac.update(suffix);
|
||||
let full = mac.finalize().into_bytes();
|
||||
out.copy_from_slice(&full[..SIG_LEN]);
|
||||
}
|
||||
SigningAlgo::AesCmac => {
|
||||
let mut mac = <CmacAes128 as Mac>::new_from_slice(key)
|
||||
.expect("AES-128-CMAC requires a 16-byte key, which we have");
|
||||
mac.update(prefix);
|
||||
mac.update(&zero_signature);
|
||||
mac.update(suffix);
|
||||
let full = mac.finalize().into_bytes();
|
||||
out.copy_from_slice(&full[..SIG_LEN]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Compute and embed a signature in `msg`. Mutates `msg` in place.
|
||||
///
|
||||
/// The caller is responsible for setting the SMB2 SIGNED flag (`0x00000008`)
|
||||
/// on the header *before* calling — it is part of the bytes that get MAC'd.
|
||||
///
|
||||
/// Errors if `msg` is too short to contain an SMB2 header (< 64 bytes).
|
||||
pub fn sign(msg: &mut [u8], key: &[u8; 16], algo: SigningAlgo) -> ProtoResult<()> {
|
||||
if msg.len() < SMB2_HEADER_LEN {
|
||||
return Err(ProtoError::Crypto("message too short to sign"));
|
||||
}
|
||||
|
||||
// Compute MAC over the whole message with the signature field treated as
|
||||
// zero, then place the MAC into the signature field.
|
||||
let mac = compute_mac_zeroed_signature(msg, key, algo);
|
||||
msg[SIG_OFF..SIG_OFF + SIG_LEN].copy_from_slice(&mac);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the signature in `msg`. Does **not** modify `msg`.
|
||||
///
|
||||
/// Uses constant-time comparison. Returns `Ok(())` if the embedded signature
|
||||
/// matches the freshly computed MAC.
|
||||
pub fn verify(msg: &[u8], key: &[u8; 16], algo: SigningAlgo) -> ProtoResult<()> {
|
||||
if msg.len() < SMB2_HEADER_LEN {
|
||||
return Err(ProtoError::Crypto("message too short to verify"));
|
||||
}
|
||||
|
||||
// Capture the embedded signature.
|
||||
let mut embedded = [0u8; SIG_LEN];
|
||||
embedded.copy_from_slice(&msg[SIG_OFF..SIG_OFF + SIG_LEN]);
|
||||
|
||||
let computed = compute_mac_zeroed_signature(msg, key, algo);
|
||||
|
||||
if constant_time_eq(&embedded, &computed) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProtoError::Crypto("signature mismatch"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Constant-time comparison of two 16-byte arrays.
|
||||
#[inline]
|
||||
fn constant_time_eq(a: &[u8; SIG_LEN], b: &[u8; SIG_LEN]) -> bool {
|
||||
let mut diff: u8 = 0;
|
||||
for i in 0..SIG_LEN {
|
||||
diff |= a[i] ^ b[i];
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a 100-byte message: a plausible 64-byte SMB2 header followed by
|
||||
/// 36 bytes of body. The signature region (bytes 48..64) is left zero;
|
||||
/// `sign` will overwrite it.
|
||||
fn fixture_message() -> Vec<u8> {
|
||||
let mut msg = vec![0u8; 100];
|
||||
// Magic: 0xFE 'S' 'M' 'B'
|
||||
msg[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
|
||||
// StructureSize = 64
|
||||
msg[4..6].copy_from_slice(&64u16.to_le_bytes());
|
||||
// Pretend ChannelSequence = 0
|
||||
msg[6..8].copy_from_slice(&0u16.to_le_bytes());
|
||||
// Command = NEGOTIATE (0)
|
||||
msg[12..14].copy_from_slice(&0u16.to_le_bytes());
|
||||
// Flags: SIGNED (0x00000008)
|
||||
msg[16..20].copy_from_slice(&0x0000_0008u32.to_le_bytes());
|
||||
// Body filler
|
||||
for (i, b) in msg[64..].iter_mut().enumerate() {
|
||||
*b = (i as u8).wrapping_mul(7);
|
||||
}
|
||||
msg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_and_verify_hmac_sha256() {
|
||||
let key = [0xAAu8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||
|
||||
// Signature should now be non-zero (overwhelmingly likely).
|
||||
assert_ne!(&msg[SIG_OFF..SIG_OFF + SIG_LEN], &[0u8; 16]);
|
||||
|
||||
verify(&msg, &key, SigningAlgo::HmacSha256).expect("verify ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_and_verify_aes_cmac() {
|
||||
let key = [0x55u8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
|
||||
assert_ne!(&msg[SIG_OFF..SIG_OFF + SIG_LEN], &[0u8; 16]);
|
||||
verify(&msg, &key, SigningAlgo::AesCmac).expect("verify ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tamper_outside_sig_fails_verify_hmac() {
|
||||
let key = [0xAAu8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||
|
||||
// Flip one body byte.
|
||||
msg[80] ^= 0x01;
|
||||
let res = verify(&msg, &key, SigningAlgo::HmacSha256);
|
||||
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tamper_outside_sig_fails_verify_cmac() {
|
||||
let key = [0x55u8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
|
||||
|
||||
// Flip a header byte (not in the sig region).
|
||||
msg[10] ^= 0xFF;
|
||||
let res = verify(&msg, &key, SigningAlgo::AesCmac);
|
||||
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tamper_signature_fails_verify() {
|
||||
let key = [0xAAu8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||
msg[SIG_OFF] ^= 0x01;
|
||||
let res = verify(&msg, &key, SigningAlgo::HmacSha256);
|
||||
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_key_fails_verify() {
|
||||
let key = [0xAAu8; 16];
|
||||
let bad_key = [0xBBu8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||
let res = verify(&msg, &bad_key, SigningAlgo::HmacSha256);
|
||||
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_short_message_errors() {
|
||||
let mut tiny = [0u8; 10];
|
||||
let key = [0u8; 16];
|
||||
let res = sign(&mut tiny, &key, SigningAlgo::HmacSha256);
|
||||
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||
let res = verify(&tiny, &key, SigningAlgo::HmacSha256);
|
||||
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_does_not_mutate_message_hmac_sha256() {
|
||||
let key = [0xAAu8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||
let snapshot = msg.clone();
|
||||
verify(&msg, &key, SigningAlgo::HmacSha256).expect("verify ok");
|
||||
assert_eq!(msg, snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_does_not_mutate_message_aes_cmac() {
|
||||
let key = [0x55u8; 16];
|
||||
let mut msg = fixture_message();
|
||||
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
|
||||
let snapshot = msg.clone();
|
||||
verify(&msg, &key, SigningAlgo::AesCmac).expect("verify ok");
|
||||
assert_eq!(msg, snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_ignores_existing_signature_bytes() {
|
||||
let key = [0xAAu8; 16];
|
||||
let mut clean = fixture_message();
|
||||
let mut dirty = fixture_message();
|
||||
dirty[SIG_OFF..SIG_OFF + SIG_LEN].fill(0xCC);
|
||||
|
||||
sign(&mut clean, &key, SigningAlgo::HmacSha256).expect("sign clean");
|
||||
sign(&mut dirty, &key, SigningAlgo::HmacSha256).expect("sign dirty");
|
||||
|
||||
assert_eq!(
|
||||
&clean[SIG_OFF..SIG_OFF + SIG_LEN],
|
||||
&dirty[SIG_OFF..SIG_OFF + SIG_LEN]
|
||||
);
|
||||
verify(&dirty, &key, SigningAlgo::HmacSha256).expect("verify dirty");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user