Skip to main content

smb_server_auth/
ntlm.rs

1//! Minimal NTLMSSP ([MS-NLMP]) message construction/parsing plus the SPNEGO
2//! DER wrapping used during extended-security session setup.
3//!
4//! Both SMB1 (`SESSION_SETUP_ANDX` with `CAP_EXTENDED_SECURITY`) and SMB2
5//! session setup speak this exact dialect of tokens, so the module is
6//! shared between protocol implementations.
7
8/// Wire signature every NTLMSSP message begins with.
9pub const NTLMSSP_SIG: &[u8; 8] = b"NTLMSSP\0";
10
11/// NEGOTIATE message identifier.
12pub const MSG_TYPE1: u32 = 1;
13/// CHALLENGE message identifier.
14pub const MSG_TYPE2: u32 = 2;
15/// AUTHENTICATE message identifier.
16pub const MSG_TYPE3: u32 = 3;
17
18/// Request/response Unicode strings.
19pub const NEGOTIATE_UNICODE: u32 = 0x0000_0001;
20/// Server must include a target name in the CHALLENGE.
21pub const REQUEST_TARGET: u32 = 0x0000_0004;
22/// NTLM authentication (as opposed to LM-only).
23pub const NEGOTIATE_NTLM: u32 = 0x0000_0200;
24/// Client requests anonymous authentication.
25pub const NEGOTIATE_ANONYMOUS: u32 = 0x0000_0800;
26/// Domain name supplied by the client.
27pub const NEGOTIATE_DOMAIN_SUPPLIED: u32 = 0x0000_1000;
28/// Always sign messages.
29pub const NEGOTIATE_ALWAYS_SIGN: u32 = 0x0000_8000;
30/// Extended session security (NTLM2).
31pub const NEGOTIATE_EXTENDED_SESSIONSECURITY: u32 = 0x0008_0000;
32/// Target info AV pairs are present.
33pub const NEGOTIATE_TARGET_INFO: u32 = 0x0080_0000;
34/// 128-bit crypto strength requested.
35pub const NEGOTIATE_128: u32 = 0x2000_0000;
36/// Version structure present.
37pub const NEGOTIATE_VERSION: u32 = 0x0200_0000;
38/// Target type is a domain.
39pub const TARGET_TYPE_DOMAIN: u32 = 0x0001_0000;
40/// Message signing is supported.
41pub const NEGOTIATE_SIGN: u32 = 0x0000_0010;
42
43/// Sealing (encryption) is supported.
44pub const NEGOTIATE_SEAL: u32 = 0x0000_0020;
45
46/// Key exchange negotiated ([MS-NLMP] §2.2.2.5): the AUTHENTICATE
47/// message carries an RC4-encrypted RandomSessionKey.
48pub const NEGOTIATE_KEY_EXCH: u32 = 0x4000_0000;
49
50/// Parsed AUTHENTICATE (type 3) message.
51#[derive(Debug, Default)]
52pub struct Type3 {
53    /// Account name supplied by the client.
54    pub user: String,
55    /// Authentication target domain.
56    pub domain: String,
57    /// Client workstation name.
58    pub workstation: String,
59    /// LM response bytes (may be empty for NTLMv2-only clients).
60    pub lm_response: Vec<u8>,
61    /// NTLM/NTLMv2 response bytes (proof + blob for v2).
62    pub ntlm_response: Vec<u8>,
63    /// Encrypted random session key (present with NEGOTIATE_KEY_EXCH).
64    pub encrypted_session_key: Vec<u8>,
65    /// Flags echoed by the client in the type 3 message.
66    pub flags: u32,
67}
68
69#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
70fn rd_u32(b: &[u8], off: usize) -> u32 {
71    b.get(off..off + 4)
72        .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
73        .unwrap_or(0)
74}
75
76#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
77fn rd_u16(b: &[u8], off: usize) -> u16 {
78    b.get(off..off + 2)
79        .map(|s| u16::from_le_bytes(s.try_into().unwrap()))
80        .unwrap_or(0)
81}
82
83/// Find `needle` inside `haystack`, returning its offset.
84fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
85    haystack.windows(needle.len()).position(|w| w == needle)
86}
87
88/// Extract the inner NTLMSSP message from a possibly-SPNEGO-wrapped blob:
89/// returns the slice starting at the `"NTLMSSP\0"` marker, when present.
90pub fn unwrap_blob(blob: &[u8]) -> Option<&[u8]> {
91    find(blob, NTLMSSP_SIG).map(|pos| &blob[pos..])
92}
93
94/// True when the blob looks like DER/SPNEGO (starts with ASN.1 tags) rather
95/// than a raw NTLMSSP message.
96#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
97pub fn is_spnego(blob: &[u8]) -> bool {
98    matches!(blob.first(), Some(0x60) | Some(0xA1)) || (blob.len() > 1 && blob[0] == 0x06)
99}
100
101/// Identify an NTLMSSP message type (1/2/3), if the buffer is one.
102#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
103pub fn msg_type(blob: &[u8]) -> Option<u32> {
104    if blob.len() < 12 || &blob[..8] != NTLMSSP_SIG {
105        return None;
106    }
107    Some(rd_u32(blob, 8))
108}
109
110/// Build an NTLMSSP CHALLENGE (type 2) message carrying our `challenge`,
111/// target `domain`/`hostname` and a complete TargetInfo AV list.
112///
113/// The layout follows [MS-NLMP] §2.2.1.2 including the `Version` field
114/// (NEGOTIATE_VERSION is always granted here, matching what Windows sends).
115/// Build an NTLMSSP CHALLENGE (Type 2) message ([MS-NLMP] §2.2.1.2 fixed layout).
116#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
117pub fn build_type2(challenge: &[u8; 8], domain: &str, hostname: &str) -> Vec<u8> {
118    // TargetInfo AV pairs — required input for client-side NTLMv2.
119    let mut ti = Vec::new();
120    {
121        let put_av = |ti: &mut Vec<u8>, typ: u16, val: &[u16]| {
122            ti.extend_from_slice(&typ.to_le_bytes());
123            ti.extend_from_slice(&((val.len() * 2) as u16).to_le_bytes());
124            for u in val {
125                ti.extend_from_slice(&u.to_le_bytes());
126            }
127        };
128        put_av(&mut ti, 0x0001, &domain.encode_utf16().collect::<Vec<_>>()); // NbDomainName
129        put_av(&mut ti, 0x0002, &hostname.encode_utf16().collect::<Vec<_>>()); // NbComputerName
130        put_av(&mut ti, 0x0003, &domain.encode_utf16().collect::<Vec<_>>()); // DnsDomainName
131        put_av(&mut ti, 0x0004, &hostname.encode_utf16().collect::<Vec<_>>()); // DnsComputerName
132        // MsvAvTimestamp: current FILETIME split into four LE u16 units.
133        let now = smb_server_proto::types::FileTime::now().0;
134        let ts: [u16; 4] = [
135            (now & 0xffff) as u16,
136            ((now >> 16) & 0xffff) as u16,
137            ((now >> 32) & 0xffff) as u16,
138            ((now >> 48) & 0xffff) as u16,
139        ];
140        put_av(&mut ti, 0x0007, &ts);
141           // MsvAvEOL is a full AV_PAIR: type=0 AND length=0.
142        ti.extend_from_slice(&[0u8; 4]);
143    }
144
145    let dom_utf16: Vec<u8> = domain.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
146
147    let mut flags = NEGOTIATE_UNICODE
148        | REQUEST_TARGET
149        | NEGOTIATE_NTLM
150        | NEGOTIATE_EXTENDED_SESSIONSECURITY
151        | NEGOTIATE_TARGET_INFO
152        | TARGET_TYPE_DOMAIN
153        | NEGOTIATE_128;
154    // Extra grants/clears for interop testing ("-0x8" clears a bit).
155    if let Ok(extra) = std::env::var("RUSTSMB_T2_FLAGS") {
156        for part in extra.split(',') {
157            let part = part.trim();
158            if let Some(neg) = part.strip_prefix('-') {
159                if let Ok(v) = u32::from_str_radix(neg.trim_start_matches("0x"), 16) {
160                    flags &= !v;
161                }
162            } else if let Ok(v) = u32::from_str_radix(part.trim_start_matches("0x"), 16) {
163                flags |= v;
164            }
165        }
166    }
167    flags |= NEGOTIATE_VERSION;
168
169    const PAYLOAD_BASE: usize = 56; // fixed header + 8-byte Version
170
171    let mut m = Vec::with_capacity(PAYLOAD_BASE + dom_utf16.len() + ti.len());
172    m.extend_from_slice(NTLMSSP_SIG);
173    m.extend_from_slice(&MSG_TYPE2.to_le_bytes());
174    m.extend_from_slice(&(dom_utf16.len() as u16).to_le_bytes()); // len
175    m.extend_from_slice(&(dom_utf16.len() as u16).to_le_bytes()); // max len
176    m.extend_from_slice(&(PAYLOAD_BASE as u32).to_le_bytes()); // offset
177    m.extend_from_slice(&flags.to_le_bytes());
178    m.extend_from_slice(challenge);
179    m.extend_from_slice(&[0u8; 8]); // reserved
180    m.extend_from_slice(&(ti.len() as u16).to_le_bytes());
181    m.extend_from_slice(&(ti.len() as u16).to_le_bytes());
182    m.extend_from_slice(&((PAYLOAD_BASE + dom_utf16.len()) as u32).to_le_bytes());
183    // Version: Windows 10.0 build 14393, NTLM revision 15.
184    m.extend_from_slice(&[0x0a, 0x00, 0xf9, 0x38, 0x00, 0x00, 0x00, 0x0f]);
185    m.extend_from_slice(&dom_utf16);
186    m.extend_from_slice(&ti);
187    m
188}
189
190/// Session key field ([MS-NLMP] §2.2.1.3): len @52, max @54, offset @56.
191#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
192pub fn parse_type3_session_key(blob: &[u8]) -> Vec<u8> {
193    let len = rd_u16(blob, 52) as usize;
194    let off = rd_u32(blob, 56) as usize;
195    if len == 0 || off + len > blob.len() {
196        return Vec::new();
197    }
198    blob[off..off + len].to_vec()
199}
200
201/// Parse an NTLMSSP AUTHENTICATE (type 3) message per [MS-NLMP] §2.2.1.3.
202/// Parse an NTLMSSP AUTHENTICATE (Type 3) message ([MS-NLMP] §2.2.1.3 fixed layout).
203#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
204pub fn parse_type3(blob: &[u8]) -> Option<Type3> {
205    if blob.len() < 32 || msg_type(blob) != Some(MSG_TYPE3) {
206        return None;
207    }
208    let field = |len_off: usize, off_off: usize| -> Vec<u8> {
209        let len = rd_u16(blob, len_off) as usize;
210        let off = rd_u32(blob, off_off) as usize;
211        if len == 0 || off + len > blob.len() {
212            return Vec::new();
213        }
214        blob[off..off + len].to_vec()
215    };
216    let utf16 = |b: &[u8]| -> String {
217        let units: Vec<u16> = b
218            .chunks_exact(2)
219            .map(|c| c[0] as u16 | ((c[1] as u16) << 8))
220            .collect();
221        String::from_utf16_lossy(&units)
222    };
223    Some(Type3 {
224        lm_response: field(12, 16),
225        ntlm_response: field(20, 24),
226        domain: utf16(&field(28, 32)),
227        user: utf16(&field(36, 40)),
228        workstation: utf16(&field(44, 48)),
229        encrypted_session_key: parse_type3_session_key(blob),
230        flags: rd_u32(blob, 60),
231    })
232}
233
234// ---------------- Minimal DER/SPNEGO helpers ----------------
235
236/// Encode an ASN.1 DER length ([X.690] §8.1.3): short form < 128, else long form.
237#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
238fn der_len(len: usize) -> Vec<u8> {
239    if len < 0x80 {
240        vec![len as u8]
241    } else if len < 0x100 {
242        vec![0x81, len as u8]
243    } else {
244        vec![0x82, (len >> 8) as u8, (len & 0xff) as u8]
245    }
246}
247
248fn der_tlv(tag: u8, content: &[u8]) -> Vec<u8> {
249    let mut out = vec![tag];
250    out.extend_from_slice(&der_len(content.len()));
251    out.extend_from_slice(content);
252    out
253}
254
255/// Wrap an NTLMSSP token in a SPNEGO NegTokenTarg declaring
256/// `negResult = accept-incomplete` and `supportedMech = NTLMSSP`.
257#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SPNEGO negTokenTarg DER
258pub fn wrap_negtoken_targ(token: &[u8]) -> Vec<u8> {
259    let result = der_tlv(0xA0, &der_tlv(0x0A, &[0x01]));
260    let oid_bytes = [0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a]; // 1.3.6.1.4.1.311.2.2.10
261    let mech = der_tlv(0xA1, &der_tlv(0x06, &oid_bytes));
262    let resp = der_tlv(0xA2, &der_tlv(0x04, token));
263    let mut seq_content = result;
264    seq_content.extend_from_slice(&mech);
265    seq_content.extend_from_slice(&resp);
266    der_tlv(0xA1, &der_tlv(0x30, &seq_content))
267}
268
269/// SPNEGO NegTokenTarg carrying only `negResult = accept-completed`.
270#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SPNEGO negTokenTarg DER
271pub fn wrap_accept_complete() -> Vec<u8> {
272    let result = der_tlv(0xA0, &der_tlv(0x0A, &[0x00]));
273    der_tlv(0xA1, &der_tlv(0x30, &result))
274}
275
276/// SPNEGO NegTokenResp with `accept-completed` plus a mechListMIC
277/// ([RFC 4178] §4.2.2): the client validates it against the exported
278/// session key whenever it sent a MIC in its AUTHENTICATE message.
279///
280/// `mic` is HMAC-MD5(ExportedSessionKey, init || targ || auth) computed by
281/// the caller over the exact exchanged SPNEGO blobs.
282#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SPNEGO negTokenTarg DER
283pub fn wrap_accept_complete_with_mic(mic: &[u8]) -> Vec<u8> {
284    let result = der_tlv(0xA0, &der_tlv(0x0A, &[0x00]));
285    let mic_field = der_tlv(0xA3, &der_tlv(0x04, mic));
286    let mut seq = result;
287    seq.extend_from_slice(&mic_field);
288    der_tlv(0xA1, &der_tlv(0x30, &seq))
289}