Files
markbase/vendor/smb2/src/crypto/kdf.rs
Warren 7eb528d35f
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
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)
2026-06-20 19:42:29 +08:00

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);
}
}