- 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)
526 lines
17 KiB
Rust
526 lines
17 KiB
Rust
//! SP800-108 key derivation and preauthentication integrity hashing for SMB2/3.
|
|
//!
|
|
//! SMB 3.x uses NIST SP800-108 KDF in counter mode with HMAC-SHA256 as the PRF
|
|
//! to derive signing, encryption, and decryption keys from the session key.
|
|
//!
|
|
//! SMB 3.1.1 additionally requires a preauthentication integrity hash (SHA-512)
|
|
//! computed over the raw wire bytes of NEGOTIATE and SESSION_SETUP exchanges,
|
|
//! which feeds into the KDF as the "context" parameter.
|
|
|
|
use crate::types::Dialect;
|
|
use digest::{Digest, KeyInit};
|
|
use hmac::{Hmac, Mac};
|
|
use sha2::{Sha256, Sha512};
|
|
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
|
|
/// Derive a key using SP800-108 KDF in counter mode with HMAC-SHA256.
|
|
///
|
|
/// This implements the algorithm from NIST SP800-108 section 5.1 as required
|
|
/// by MS-SMB2 section 3.1.4.2. The counter width ('r') is 32 bits, and the
|
|
/// PRF is HMAC-SHA256.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `key` - The key to derive from (the session key from authentication).
|
|
/// * `label` - Label string (including null terminator).
|
|
/// * `context` - Context string or preauth hash (including null terminator for
|
|
/// string contexts).
|
|
/// * `key_length_bits` - Desired output key length in bits (128 or 256).
|
|
pub fn sp800_108_kdf(key: &[u8], label: &[u8], context: &[u8], key_length_bits: u32) -> Vec<u8> {
|
|
let iterations = key_length_bits.div_ceil(256);
|
|
let mut result = Vec::with_capacity((iterations * 32) as usize);
|
|
|
|
for i in 1..=iterations {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC-SHA256 accepts any key length");
|
|
|
|
// counter (32-bit big-endian)
|
|
mac.update(&i.to_be_bytes());
|
|
// label
|
|
mac.update(label);
|
|
// separator byte 0x00
|
|
mac.update(&[0x00]);
|
|
// context
|
|
mac.update(context);
|
|
// L = key length in bits (32-bit big-endian)
|
|
mac.update(&key_length_bits.to_be_bytes());
|
|
|
|
result.extend_from_slice(&mac.finalize().into_bytes());
|
|
}
|
|
|
|
result.truncate((key_length_bits / 8) as usize);
|
|
result
|
|
}
|
|
|
|
/// Derived session keys for signing, encryption, and decryption.
|
|
#[derive(Debug, Clone)]
|
|
pub struct DerivedKeys {
|
|
/// Key used to sign outgoing messages.
|
|
pub signing_key: Vec<u8>,
|
|
/// Key used to encrypt outgoing messages.
|
|
pub encryption_key: Vec<u8>,
|
|
/// Key used to decrypt incoming messages.
|
|
pub decryption_key: Vec<u8>,
|
|
}
|
|
|
|
/// Derive session keys for the given dialect.
|
|
///
|
|
/// For SMB 3.0 and 3.0.2, the context is a fixed ASCII string.
|
|
/// For SMB 3.1.1, the context is the preauthentication integrity hash value
|
|
/// (64 bytes from SHA-512).
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `dialect` is SMB 3.1.1 and `preauth_hash` is `None`.
|
|
/// Panics if `dialect` is not in the SMB 3.x family.
|
|
pub fn derive_session_keys(
|
|
session_key: &[u8],
|
|
dialect: Dialect,
|
|
preauth_hash: Option<&[u8; 64]>,
|
|
key_length_bits: u32,
|
|
) -> DerivedKeys {
|
|
assert!(
|
|
matches!(
|
|
dialect,
|
|
Dialect::Smb3_0 | Dialect::Smb3_0_2 | Dialect::Smb3_1_1
|
|
),
|
|
"Key derivation is only applicable for the SMB 3.x dialect family"
|
|
);
|
|
|
|
let (signing_label, signing_context): (&[u8], &[u8]);
|
|
let (enc_label, enc_context): (&[u8], &[u8]);
|
|
let (dec_label, dec_context): (&[u8], &[u8]);
|
|
|
|
if dialect == Dialect::Smb3_1_1 {
|
|
let hash = preauth_hash
|
|
.expect("SMB 3.1.1 requires a preauthentication integrity hash for key derivation");
|
|
// SMB 3.1.1 labels include null terminator (matches smb-rs and
|
|
// the MS-SMB2 spec's Label field definitions)
|
|
signing_label = b"SMBSigningKey\0";
|
|
signing_context = hash.as_slice();
|
|
enc_label = b"SMBC2SCipherKey\0";
|
|
enc_context = hash.as_slice();
|
|
dec_label = b"SMBS2CCipherKey\0";
|
|
dec_context = hash.as_slice();
|
|
} else {
|
|
// SMB 3.0 and 3.0.2
|
|
signing_label = b"SMB2AESCMAC\0";
|
|
signing_context = b"SmbSign\0";
|
|
enc_label = b"SMB2AESCCM\0";
|
|
enc_context = b"ServerIn \0";
|
|
dec_label = b"SMB2AESCCM\0";
|
|
dec_context = b"ServerOut\0";
|
|
}
|
|
|
|
DerivedKeys {
|
|
signing_key: sp800_108_kdf(session_key, signing_label, signing_context, key_length_bits),
|
|
encryption_key: sp800_108_kdf(session_key, enc_label, enc_context, key_length_bits),
|
|
decryption_key: sp800_108_kdf(session_key, dec_label, dec_context, key_length_bits),
|
|
}
|
|
}
|
|
|
|
/// Running hash over negotiate and session-setup exchange bytes.
|
|
///
|
|
/// Used as the "context" parameter to the KDF for SMB 3.1.1. The hash
|
|
/// algorithm is SHA-512, producing a 64-byte value.
|
|
///
|
|
/// The hash is computed incrementally:
|
|
/// 1. Initialize with 64 zero bytes
|
|
/// 2. `update()` with negotiate request raw bytes
|
|
/// 3. `update()` with negotiate response raw bytes
|
|
/// 4. (Clone for session hash)
|
|
/// 5. `update()` with session setup request raw bytes
|
|
/// 6. `update()` with session setup response raw bytes
|
|
/// 7. Repeat 5-6 for each SESSION_SETUP round-trip
|
|
///
|
|
/// Each `update()` computes: `hash = SHA-512(previous_hash || message_bytes)`
|
|
pub struct PreauthHasher {
|
|
hash: [u8; 64],
|
|
}
|
|
|
|
impl PreauthHasher {
|
|
/// Create a new hasher initialized with 64 zero bytes.
|
|
pub fn new() -> Self {
|
|
Self { hash: [0u8; 64] }
|
|
}
|
|
|
|
/// Update the hash with a message's raw wire bytes.
|
|
///
|
|
/// Computes `hash = SHA-512(previous_hash || message_bytes)`.
|
|
pub fn update(&mut self, message_bytes: &[u8]) {
|
|
let mut hasher = Sha512::new();
|
|
hasher.update(self.hash);
|
|
hasher.update(message_bytes);
|
|
self.hash.copy_from_slice(&hasher.finalize());
|
|
}
|
|
|
|
/// Get the current hash value (64 bytes).
|
|
pub fn value(&self) -> &[u8; 64] {
|
|
&self.hash
|
|
}
|
|
}
|
|
|
|
impl Default for PreauthHasher {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Clone for PreauthHasher {
|
|
fn clone(&self) -> Self {
|
|
Self { hash: self.hash }
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ========================================================================
|
|
// SP800-108 KDF tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn kdf_128_bit_output_is_16_bytes() {
|
|
let key = [0xAA; 16];
|
|
let result = sp800_108_kdf(&key, b"label\0", b"context\0", 128);
|
|
assert_eq!(result.len(), 16);
|
|
}
|
|
|
|
#[test]
|
|
fn kdf_256_bit_output_is_32_bytes() {
|
|
let key = [0xBB; 16];
|
|
let result = sp800_108_kdf(&key, b"label\0", b"context\0", 256);
|
|
assert_eq!(result.len(), 32);
|
|
}
|
|
|
|
#[test]
|
|
fn kdf_is_deterministic() {
|
|
let key = [0x42; 16];
|
|
let label = b"TestLabel\0";
|
|
let context = b"TestContext\0";
|
|
let r1 = sp800_108_kdf(&key, label, context, 128);
|
|
let r2 = sp800_108_kdf(&key, label, context, 128);
|
|
assert_eq!(r1, r2);
|
|
}
|
|
|
|
#[test]
|
|
fn kdf_different_labels_produce_different_keys() {
|
|
let key = [0x42; 16];
|
|
let context = b"ctx\0";
|
|
let k1 = sp800_108_kdf(&key, b"LabelA\0", context, 128);
|
|
let k2 = sp800_108_kdf(&key, b"LabelB\0", context, 128);
|
|
assert_ne!(k1, k2);
|
|
}
|
|
|
|
#[test]
|
|
fn kdf_different_contexts_produce_different_keys() {
|
|
let key = [0x42; 16];
|
|
let label = b"label\0";
|
|
let k1 = sp800_108_kdf(&key, label, b"ContextA\0", 128);
|
|
let k2 = sp800_108_kdf(&key, label, b"ContextB\0", 128);
|
|
assert_ne!(k1, k2);
|
|
}
|
|
|
|
#[test]
|
|
fn kdf_different_session_keys_produce_different_derived_keys() {
|
|
let label = b"SMB2AESCMAC\0";
|
|
let context = b"SmbSign\0";
|
|
let k1 = sp800_108_kdf(&[0x11; 16], label, context, 128);
|
|
let k2 = sp800_108_kdf(&[0x22; 16], label, context, 128);
|
|
assert_ne!(k1, k2);
|
|
}
|
|
|
|
/// Verify KDF output against a manually computed value.
|
|
///
|
|
/// For a single iteration (128-bit output), the KDF computes:
|
|
/// HMAC-SHA256(key, 0x00000001 || label || 0x00 || context || 0x00000080)
|
|
/// and takes the first 16 bytes.
|
|
#[test]
|
|
fn kdf_known_vector_single_iteration() {
|
|
let key = [0x00u8; 16];
|
|
let label = b"SMB2AESCMAC\0";
|
|
let context = b"SmbSign\0";
|
|
|
|
// Manually compute the expected value.
|
|
let mut mac = HmacSha256::new_from_slice(&key).unwrap();
|
|
mac.update(&1u32.to_be_bytes()); // counter = 1
|
|
mac.update(label); // label
|
|
mac.update(&[0x00]); // separator
|
|
mac.update(context); // context
|
|
mac.update(&128u32.to_be_bytes()); // L = 128
|
|
let full = mac.finalize().into_bytes();
|
|
let expected = &full[..16];
|
|
|
|
let result = sp800_108_kdf(&key, label, context, 128);
|
|
assert_eq!(result.as_slice(), expected);
|
|
}
|
|
|
|
/// Verify that 256-bit KDF uses two iterations and concatenates correctly.
|
|
#[test]
|
|
fn kdf_known_vector_two_iterations() {
|
|
let key = [0xFFu8; 16];
|
|
let label = b"TestLabel\0";
|
|
let context = b"TestCtx\0";
|
|
|
|
// Compute iteration 1
|
|
let mut mac1 = HmacSha256::new_from_slice(&key).unwrap();
|
|
mac1.update(&1u32.to_be_bytes());
|
|
mac1.update(label);
|
|
mac1.update(&[0x00]);
|
|
mac1.update(context);
|
|
mac1.update(&256u32.to_be_bytes());
|
|
let block1 = mac1.finalize().into_bytes();
|
|
|
|
// 256 bits = 32 bytes = exactly one HMAC-SHA256 block, so only one
|
|
// iteration is needed. But let's verify with the formula:
|
|
// ceil(256 / 256) = 1 iteration. So 256-bit also needs just one.
|
|
let result = sp800_108_kdf(&key, label, context, 256);
|
|
assert_eq!(result.len(), 32);
|
|
assert_eq!(result.as_slice(), block1.as_slice());
|
|
}
|
|
|
|
// ========================================================================
|
|
// derive_session_keys tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn derive_keys_smb3_0_uses_legacy_labels() {
|
|
let session_key = [0x42; 16];
|
|
let keys = derive_session_keys(&session_key, Dialect::Smb3_0, None, 128);
|
|
|
|
// Verify each key matches what we'd get calling KDF directly with the
|
|
// SMB 3.0 label/context pairs.
|
|
assert_eq!(
|
|
keys.signing_key,
|
|
sp800_108_kdf(&session_key, b"SMB2AESCMAC\0", b"SmbSign\0", 128)
|
|
);
|
|
assert_eq!(
|
|
keys.encryption_key,
|
|
sp800_108_kdf(&session_key, b"SMB2AESCCM\0", b"ServerIn \0", 128)
|
|
);
|
|
assert_eq!(
|
|
keys.decryption_key,
|
|
sp800_108_kdf(&session_key, b"SMB2AESCCM\0", b"ServerOut\0", 128)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_keys_smb3_0_2_uses_legacy_labels() {
|
|
let session_key = [0x42; 16];
|
|
let keys = derive_session_keys(&session_key, Dialect::Smb3_0_2, None, 128);
|
|
|
|
assert_eq!(
|
|
keys.signing_key,
|
|
sp800_108_kdf(&session_key, b"SMB2AESCMAC\0", b"SmbSign\0", 128)
|
|
);
|
|
assert_eq!(
|
|
keys.encryption_key,
|
|
sp800_108_kdf(&session_key, b"SMB2AESCCM\0", b"ServerIn \0", 128)
|
|
);
|
|
assert_eq!(
|
|
keys.decryption_key,
|
|
sp800_108_kdf(&session_key, b"SMB2AESCCM\0", b"ServerOut\0", 128)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_keys_smb3_1_1_uses_new_labels_with_preauth_hash() {
|
|
let session_key = [0x42; 16];
|
|
let preauth_hash = [0xAB; 64];
|
|
let keys = derive_session_keys(&session_key, Dialect::Smb3_1_1, Some(&preauth_hash), 128);
|
|
|
|
assert_eq!(
|
|
keys.signing_key,
|
|
sp800_108_kdf(&session_key, b"SMBSigningKey\0", &preauth_hash, 128)
|
|
);
|
|
assert_eq!(
|
|
keys.encryption_key,
|
|
sp800_108_kdf(&session_key, b"SMBC2SCipherKey\0", &preauth_hash, 128)
|
|
);
|
|
assert_eq!(
|
|
keys.decryption_key,
|
|
sp800_108_kdf(&session_key, b"SMBS2CCipherKey\0", &preauth_hash, 128)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_keys_smb3_1_1_256_bit() {
|
|
let session_key = [0x42; 16];
|
|
let preauth_hash = [0xCD; 64];
|
|
let keys = derive_session_keys(&session_key, Dialect::Smb3_1_1, Some(&preauth_hash), 256);
|
|
|
|
assert_eq!(keys.signing_key.len(), 32);
|
|
assert_eq!(keys.encryption_key.len(), 32);
|
|
assert_eq!(keys.decryption_key.len(), 32);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_keys_all_three_are_different() {
|
|
let session_key = [0x42; 16];
|
|
let keys = derive_session_keys(&session_key, Dialect::Smb3_0, None, 128);
|
|
|
|
assert_ne!(keys.signing_key, keys.encryption_key);
|
|
assert_ne!(keys.signing_key, keys.decryption_key);
|
|
assert_ne!(keys.encryption_key, keys.decryption_key);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "preauthentication integrity hash")]
|
|
fn derive_keys_smb3_1_1_panics_without_preauth_hash() {
|
|
let session_key = [0x42; 16];
|
|
derive_session_keys(&session_key, Dialect::Smb3_1_1, None, 128);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "SMB 3.x dialect family")]
|
|
fn derive_keys_panics_for_smb2() {
|
|
let session_key = [0x42; 16];
|
|
derive_session_keys(&session_key, Dialect::Smb2_0_2, None, 128);
|
|
}
|
|
|
|
// ========================================================================
|
|
// PreauthHasher tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn preauth_hasher_starts_with_64_zero_bytes() {
|
|
let hasher = PreauthHasher::new();
|
|
assert_eq!(hasher.value(), &[0u8; 64]);
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_default_equals_new() {
|
|
let h1 = PreauthHasher::new();
|
|
let h2 = PreauthHasher::default();
|
|
assert_eq!(h1.value(), h2.value());
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_update_changes_hash() {
|
|
let mut hasher = PreauthHasher::new();
|
|
let initial = *hasher.value();
|
|
hasher.update(b"negotiate request bytes");
|
|
assert_ne!(hasher.value(), &initial);
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_two_updates_differ_from_one() {
|
|
let mut hasher1 = PreauthHasher::new();
|
|
hasher1.update(b"message1");
|
|
|
|
let mut hasher2 = PreauthHasher::new();
|
|
hasher2.update(b"message1");
|
|
hasher2.update(b"message2");
|
|
|
|
assert_ne!(hasher1.value(), hasher2.value());
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_is_deterministic() {
|
|
let mut h1 = PreauthHasher::new();
|
|
h1.update(b"negotiate request");
|
|
h1.update(b"negotiate response");
|
|
|
|
let mut h2 = PreauthHasher::new();
|
|
h2.update(b"negotiate request");
|
|
h2.update(b"negotiate response");
|
|
|
|
assert_eq!(h1.value(), h2.value());
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_empty_update_changes_hash() {
|
|
// SHA-512(64_zeros || empty) != 64_zeros
|
|
let mut hasher = PreauthHasher::new();
|
|
let initial = *hasher.value();
|
|
hasher.update(b"");
|
|
assert_ne!(hasher.value(), &initial);
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_known_value() {
|
|
// Verify against direct SHA-512 computation.
|
|
let mut hasher = PreauthHasher::new();
|
|
hasher.update(b"test");
|
|
|
|
let mut expected_hasher = Sha512::new();
|
|
expected_hasher.update([0u8; 64]);
|
|
expected_hasher.update(b"test");
|
|
let expected = expected_hasher.finalize();
|
|
|
|
assert_eq!(hasher.value().as_slice(), expected.as_slice());
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_chained_known_value() {
|
|
// Two updates: hash1 = SHA-512(zeros || msg1), hash2 = SHA-512(hash1 || msg2)
|
|
let mut hasher = PreauthHasher::new();
|
|
hasher.update(b"negotiate");
|
|
hasher.update(b"response");
|
|
|
|
// Compute manually
|
|
let mut h = Sha512::new();
|
|
h.update([0u8; 64]);
|
|
h.update(b"negotiate");
|
|
let hash1: [u8; 64] = h.finalize().into();
|
|
|
|
let mut h2 = Sha512::new();
|
|
h2.update(hash1);
|
|
h2.update(b"response");
|
|
let hash2: [u8; 64] = h2.finalize().into();
|
|
|
|
assert_eq!(hasher.value(), &hash2);
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_clone_is_independent() {
|
|
let mut hasher = PreauthHasher::new();
|
|
hasher.update(b"negotiate request");
|
|
hasher.update(b"negotiate response");
|
|
|
|
// Clone for session hash (spec step 4)
|
|
let mut session_hasher = hasher.clone();
|
|
session_hasher.update(b"session setup request");
|
|
|
|
// Original should not be affected
|
|
assert_ne!(hasher.value(), session_hasher.value());
|
|
}
|
|
|
|
#[test]
|
|
fn preauth_hasher_output_is_64_bytes() {
|
|
let mut hasher = PreauthHasher::new();
|
|
hasher.update(b"some data");
|
|
assert_eq!(hasher.value().len(), 64);
|
|
}
|
|
|
|
/// Full end-to-end test: preauth hash feeds into KDF for SMB 3.1.1.
|
|
#[test]
|
|
fn preauth_hash_feeds_into_kdf() {
|
|
// Simulate the protocol flow
|
|
let mut conn_hasher = PreauthHasher::new();
|
|
conn_hasher.update(b"negotiate request bytes");
|
|
conn_hasher.update(b"negotiate response bytes");
|
|
|
|
let mut session_hasher = conn_hasher.clone();
|
|
session_hasher.update(b"session setup request bytes");
|
|
session_hasher.update(b"session setup response bytes");
|
|
|
|
let session_key = [0x42; 16];
|
|
let keys = derive_session_keys(
|
|
&session_key,
|
|
Dialect::Smb3_1_1,
|
|
Some(session_hasher.value()),
|
|
128,
|
|
);
|
|
|
|
// Keys should all be 16 bytes and different from each other
|
|
assert_eq!(keys.signing_key.len(), 16);
|
|
assert_eq!(keys.encryption_key.len(), 16);
|
|
assert_eq!(keys.decryption_key.len(), 16);
|
|
assert_ne!(keys.signing_key, keys.encryption_key);
|
|
assert_ne!(keys.signing_key, keys.decryption_key);
|
|
assert_ne!(keys.encryption_key, keys.decryption_key);
|
|
}
|
|
}
|