# SPDX-License-Identifier: AGPL-3.0-or-later """NTLMSSP Type 3 parser tests. Builds Type 3 buffers field-by-field per MS-NLMP §2.2.1.3 and asserts the parser returns the universal Credential SD shape. Shared infrastructure for SMB and RDP-NLA cred capture. """ from __future__ import annotations import base64 import importlib.util import struct from pathlib import Path import pytest def _load_ntlmssp(): repo = Path(__file__).resolve().parents[2] path = repo / "decnet" / "templates" / "_shared" / "ntlmssp.py" spec = importlib.util.spec_from_file_location("_ntlmssp_under_test", path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod @pytest.fixture(scope="module") def ntlmssp(): return _load_ntlmssp() def _build_type3( *, username: str, domain: str, nt_response: bytes, unicode: bool = True, ) -> bytes: """Build a syntactically-valid NTLMSSP Type 3 message.""" if unicode: u = username.encode("utf-16-le") d = domain.encode("utf-16-le") flags = 0x00000001 # NEGOTIATE_UNICODE else: u = username.encode("ascii") d = domain.encode("ascii") flags = 0x00000000 # Layout: 8 sig + 4 type + 6×8 field records + 4 flags = 64 bytes # of header, then payload (concat of nt_response, domain, username). header_size = 64 nt_off = header_size dom_off = nt_off + len(nt_response) user_off = dom_off + len(d) hdr = bytearray(header_size) hdr[0:8] = b"NTLMSSP\x00" struct.pack_into("= 28 bytes in practice). Parser flags this as secret_kind=ntlmssp_v2.""" nt_response = b"\xab" * 16 + b"\x01\x01\x00\x00" + b"\x00" * 28 # ~48 bytes blob = _build_type3( username="alice", domain="ACME", nt_response=nt_response, ) cred = ntlmssp.parse_type3(blob) assert cred is not None assert cred["username"] == "alice" assert cred["domain"] == "ACME" assert cred["principal"] == "ACME\\alice" assert cred["secret_kind"] == "ntlmssp_v2" assert base64.b64decode(cred["secret_b64"]) == nt_response def test_parse_type3_ntlmv1(ntlmssp): """NTLMv1 NTChallengeResponse is exactly 24 bytes.""" nt_response = b"\xcd" * 24 blob = _build_type3( username="bob", domain="WORKGROUP", nt_response=nt_response, ) cred = ntlmssp.parse_type3(blob) assert cred["secret_kind"] == "ntlmssp_v1" assert cred["principal"] == "WORKGROUP\\bob" def test_parse_type3_no_domain(ntlmssp): nt_response = b"\xff" * 24 blob = _build_type3( username="lonely", domain="", nt_response=nt_response, ) cred = ntlmssp.parse_type3(blob) assert cred["domain"] == "" assert cred["principal"] == "lonely" def test_parse_type3_oem_strings(ntlmssp): """Older clients without NEGOTIATE_UNICODE send ASCII strings.""" nt_response = b"\x11" * 24 blob = _build_type3( username="ascii_user", domain="WIN2000", nt_response=nt_response, unicode=False, ) cred = ntlmssp.parse_type3(blob) assert cred["username"] == "ascii_user" assert cred["domain"] == "WIN2000" def test_parse_type3_rejects_non_signature(ntlmssp): assert ntlmssp.parse_type3(b"NotNtlmssp") is None assert ntlmssp.parse_type3(b"") is None # Right magic but wrong message type: blob = bytearray(64) blob[0:8] = b"NTLMSSP\x00" struct.pack_into("= 0 cred = ntlmssp.parse_type3(outer[off:]) assert cred["username"] == "x"