xref: /aosp_15_r20/external/cronet/net/ntlm/ntlm_client_unittest.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1 // Copyright 2017 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/ntlm/ntlm_client.h"
6 
7 #include <string>
8 
9 #include "base/containers/span.h"
10 #include "base/strings/string_util.h"
11 #include "build/build_config.h"
12 #include "net/ntlm/ntlm.h"
13 #include "net/ntlm/ntlm_buffer_reader.h"
14 #include "net/ntlm/ntlm_buffer_writer.h"
15 #include "net/ntlm/ntlm_test_data.h"
16 #include "testing/gtest/include/gtest/gtest.h"
17 
18 namespace net::ntlm {
19 
20 namespace {
21 
GenerateAuthMsg(const NtlmClient & client,base::span<const uint8_t> challenge_msg)22 std::vector<uint8_t> GenerateAuthMsg(const NtlmClient& client,
23                                      base::span<const uint8_t> challenge_msg) {
24   return client.GenerateAuthenticateMessage(
25       test::kNtlmDomain, test::kUser, test::kPassword, test::kHostnameAscii,
26       reinterpret_cast<const char*>(test::kChannelBindings), test::kNtlmSpn,
27       test::kClientTimestamp, test::kClientChallenge, challenge_msg);
28 }
29 
GenerateAuthMsg(const NtlmClient & client,const NtlmBufferWriter & challenge_writer)30 std::vector<uint8_t> GenerateAuthMsg(const NtlmClient& client,
31                                      const NtlmBufferWriter& challenge_writer) {
32   return GenerateAuthMsg(client, challenge_writer.GetBuffer());
33 }
34 
GetAuthMsgResult(const NtlmClient & client,const NtlmBufferWriter & challenge_writer)35 bool GetAuthMsgResult(const NtlmClient& client,
36                       const NtlmBufferWriter& challenge_writer) {
37   return !GenerateAuthMsg(client, challenge_writer).empty();
38 }
39 
ReadBytesPayload(NtlmBufferReader * reader,base::span<uint8_t> buffer)40 bool ReadBytesPayload(NtlmBufferReader* reader, base::span<uint8_t> buffer) {
41   SecurityBuffer sec_buf;
42   return reader->ReadSecurityBuffer(&sec_buf) &&
43          (sec_buf.length == buffer.size()) &&
44          reader->ReadBytesFrom(sec_buf, buffer);
45 }
46 
47 // Reads bytes from a payload and assigns them to a string. This makes
48 // no assumptions about the underlying encoding.
ReadStringPayload(NtlmBufferReader * reader,std::string * str)49 bool ReadStringPayload(NtlmBufferReader* reader, std::string* str) {
50   SecurityBuffer sec_buf;
51   if (!reader->ReadSecurityBuffer(&sec_buf))
52     return false;
53 
54   str->resize(sec_buf.length);
55   if (!reader->ReadBytesFrom(sec_buf, base::as_writable_byte_span(*str))) {
56     return false;
57   }
58 
59   return true;
60 }
61 
62 // Reads bytes from a payload and assigns them to a string16. This makes
63 // no assumptions about the underlying encoding. This will fail if there
64 // are an odd number of bytes in the payload.
ReadString16Payload(NtlmBufferReader * reader,std::u16string * str)65 bool ReadString16Payload(NtlmBufferReader* reader, std::u16string* str) {
66   SecurityBuffer sec_buf;
67   if (!reader->ReadSecurityBuffer(&sec_buf) || (sec_buf.length % 2 != 0))
68     return false;
69 
70   std::vector<uint8_t> raw(sec_buf.length);
71   if (!reader->ReadBytesFrom(sec_buf, raw))
72     return false;
73 
74 #if defined(ARCH_CPU_BIG_ENDIAN)
75   for (size_t i = 0; i < raw.size(); i += 2) {
76     std::swap(raw[i], raw[i + 1]);
77   }
78 #endif
79 
80   str->assign(reinterpret_cast<const char16_t*>(raw.data()), raw.size() / 2);
81   return true;
82 }
83 
MakeV2ChallengeMessage(size_t target_info_len,std::vector<uint8_t> * out)84 void MakeV2ChallengeMessage(size_t target_info_len, std::vector<uint8_t>* out) {
85   static const size_t kChallengeV2HeaderLen = 56;
86 
87   // Leave room for the AV_PAIR header and the EOL pair.
88   size_t server_name_len = target_info_len - kAvPairHeaderLen * 2;
89 
90   // See [MS-NLP] Section 2.2.1.2.
91   NtlmBufferWriter challenge(kChallengeV2HeaderLen + target_info_len);
92   ASSERT_TRUE(challenge.WriteMessageHeader(MessageType::kChallenge));
93   ASSERT_TRUE(
94       challenge.WriteSecurityBuffer(SecurityBuffer(0, 0)));  // target name
95   ASSERT_TRUE(challenge.WriteFlags(NegotiateFlags::kTargetInfo));
96   ASSERT_TRUE(challenge.WriteZeros(kChallengeLen));  // server challenge
97   ASSERT_TRUE(challenge.WriteZeros(8));              // reserved
98   ASSERT_TRUE(challenge.WriteSecurityBuffer(
99       SecurityBuffer(kChallengeV2HeaderLen, target_info_len)));  // target info
100   ASSERT_TRUE(challenge.WriteZeros(8));                          // version
101   ASSERT_EQ(kChallengeV2HeaderLen, challenge.GetCursor());
102   ASSERT_TRUE(challenge.WriteAvPair(
103       AvPair(TargetInfoAvId::kServerName,
104              std::vector<uint8_t>(server_name_len, 'a'))));
105   ASSERT_TRUE(challenge.WriteAvPairTerminator());
106   ASSERT_TRUE(challenge.IsEndOfBuffer());
107   *out = challenge.Pass();
108 }
109 
110 }  // namespace
111 
TEST(NtlmClientTest,SimpleConstructionV1)112 TEST(NtlmClientTest, SimpleConstructionV1) {
113   NtlmClient client(NtlmFeatures(false));
114 
115   ASSERT_FALSE(client.IsNtlmV2());
116   ASSERT_FALSE(client.IsEpaEnabled());
117   ASSERT_FALSE(client.IsMicEnabled());
118 }
119 
TEST(NtlmClientTest,VerifyNegotiateMessageV1)120 TEST(NtlmClientTest, VerifyNegotiateMessageV1) {
121   NtlmClient client(NtlmFeatures(false));
122 
123   std::vector<uint8_t> result = client.GetNegotiateMessage();
124 
125   ASSERT_EQ(kNegotiateMessageLen, result.size());
126   ASSERT_EQ(0, memcmp(test::kExpectedNegotiateMsg, result.data(),
127                       kNegotiateMessageLen));
128 }
129 
TEST(NtlmClientTest,MinimalStructurallyValidChallenge)130 TEST(NtlmClientTest, MinimalStructurallyValidChallenge) {
131   NtlmClient client(NtlmFeatures(false));
132 
133   NtlmBufferWriter writer(kMinChallengeHeaderLen);
134   ASSERT_TRUE(writer.WriteBytes(base::make_span(test::kMinChallengeMessage)
135                                     .subspan<0, kMinChallengeHeaderLen>()));
136 
137   ASSERT_TRUE(GetAuthMsgResult(client, writer));
138 }
139 
TEST(NtlmClientTest,MinimalStructurallyValidChallengeZeroOffset)140 TEST(NtlmClientTest, MinimalStructurallyValidChallengeZeroOffset) {
141   NtlmClient client(NtlmFeatures(false));
142 
143   // The spec (2.2.1.2) states that the length SHOULD be 0 and the offset
144   // SHOULD be where the payload would be if it was present. This is the
145   // expected response from a compliant server when no target name is sent.
146   // In reality the offset should always be ignored if the length is zero.
147   // Also implementations often just write zeros.
148   uint8_t raw[kMinChallengeHeaderLen];
149   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
150   // Modify the default valid message to overwrite the offset to zero.
151   ASSERT_NE(0x00, raw[16]);
152   raw[16] = 0x00;
153 
154   NtlmBufferWriter writer(kMinChallengeHeaderLen);
155   ASSERT_TRUE(writer.WriteBytes(raw));
156 
157   ASSERT_TRUE(GetAuthMsgResult(client, writer));
158 }
159 
TEST(NtlmClientTest,ChallengeMsgTooShort)160 TEST(NtlmClientTest, ChallengeMsgTooShort) {
161   NtlmClient client(NtlmFeatures(false));
162 
163   // Fail because the minimum size valid message is 32 bytes.
164   NtlmBufferWriter writer(kMinChallengeHeaderLen - 1);
165   ASSERT_TRUE(writer.WriteBytes(base::make_span(test::kMinChallengeMessage)
166                                     .subspan<0, kMinChallengeHeaderLen - 1>()));
167   ASSERT_FALSE(GetAuthMsgResult(client, writer));
168 }
169 
TEST(NtlmClientTest,ChallengeMsgNoSig)170 TEST(NtlmClientTest, ChallengeMsgNoSig) {
171   NtlmClient client(NtlmFeatures(false));
172 
173   // Fail because the first 8 bytes don't match "NTLMSSP\0"
174   uint8_t raw[kMinChallengeHeaderLen];
175   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
176   // Modify the default valid message to overwrite the last byte of the
177   // signature.
178   ASSERT_NE(0xff, raw[7]);
179   raw[7] = 0xff;
180   NtlmBufferWriter writer(kMinChallengeHeaderLen);
181   ASSERT_TRUE(writer.WriteBytes(raw));
182   ASSERT_FALSE(GetAuthMsgResult(client, writer));
183 }
184 
TEST(NtlmClientTest,ChallengeMsgWrongMessageType)185 TEST(NtlmClientTest, ChallengeMsgWrongMessageType) {
186   NtlmClient client(NtlmFeatures(false));
187 
188   // Fail because the message type should be MessageType::kChallenge
189   // (0x00000002)
190   uint8_t raw[kMinChallengeHeaderLen];
191   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
192   // Modify the message type.
193   ASSERT_NE(0x03, raw[8]);
194   raw[8] = 0x03;
195 
196   NtlmBufferWriter writer(kMinChallengeHeaderLen);
197   ASSERT_TRUE(writer.WriteBytes(raw));
198 
199   ASSERT_FALSE(GetAuthMsgResult(client, writer));
200 }
201 
TEST(NtlmClientTest,ChallengeWithNoTargetName)202 TEST(NtlmClientTest, ChallengeWithNoTargetName) {
203   NtlmClient client(NtlmFeatures(false));
204 
205   // The spec (2.2.1.2) states that the length SHOULD be 0 and the offset
206   // SHOULD be where the payload would be if it was present. This is the
207   // expected response from a compliant server when no target name is sent.
208   // In reality the offset should always be ignored if the length is zero.
209   // Also implementations often just write zeros.
210   uint8_t raw[kMinChallengeHeaderLen];
211   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
212   // Modify the default valid message to overwrite the offset to zero.
213   ASSERT_NE(0x00, raw[16]);
214   raw[16] = 0x00;
215 
216   NtlmBufferWriter writer(kMinChallengeHeaderLen);
217   ASSERT_TRUE(writer.WriteBytes(raw));
218 
219   ASSERT_TRUE(GetAuthMsgResult(client, writer));
220 }
221 
TEST(NtlmClientTest,Type2MessageWithTargetName)222 TEST(NtlmClientTest, Type2MessageWithTargetName) {
223   NtlmClient client(NtlmFeatures(false));
224 
225   // One extra byte is provided for target name.
226   uint8_t raw[kMinChallengeHeaderLen + 1];
227   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
228   // Put something in the target name.
229   raw[kMinChallengeHeaderLen] = 'Z';
230 
231   // Modify the default valid message to indicate 1 byte is present in the
232   // target name payload.
233   ASSERT_NE(0x01, raw[12]);
234   ASSERT_EQ(0x00, raw[13]);
235   ASSERT_NE(0x01, raw[14]);
236   ASSERT_EQ(0x00, raw[15]);
237   raw[12] = 0x01;
238   raw[14] = 0x01;
239 
240   NtlmBufferWriter writer(kChallengeHeaderLen + 1);
241   ASSERT_TRUE(writer.WriteBytes(raw));
242   ASSERT_TRUE(GetAuthMsgResult(client, writer));
243 }
244 
TEST(NtlmClientTest,NoTargetNameOverflowFromOffset)245 TEST(NtlmClientTest, NoTargetNameOverflowFromOffset) {
246   NtlmClient client(NtlmFeatures(false));
247 
248   uint8_t raw[kMinChallengeHeaderLen];
249   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
250   // Modify the default valid message to claim that the target name field is 1
251   // byte long overrunning the end of the message message.
252   ASSERT_NE(0x01, raw[12]);
253   ASSERT_EQ(0x00, raw[13]);
254   ASSERT_NE(0x01, raw[14]);
255   ASSERT_EQ(0x00, raw[15]);
256   raw[12] = 0x01;
257   raw[14] = 0x01;
258 
259   NtlmBufferWriter writer(kMinChallengeHeaderLen);
260   ASSERT_TRUE(writer.WriteBytes(raw));
261 
262   // The above malformed message could cause an implementation to read outside
263   // the message buffer because the offset is past the end of the message.
264   // Verify it gets rejected.
265   ASSERT_FALSE(GetAuthMsgResult(client, writer));
266 }
267 
TEST(NtlmClientTest,NoTargetNameOverflowFromLength)268 TEST(NtlmClientTest, NoTargetNameOverflowFromLength) {
269   NtlmClient client(NtlmFeatures(false));
270 
271   // Message has 1 extra byte of space after the header for the target name.
272   // One extra byte is provided for target name.
273   uint8_t raw[kMinChallengeHeaderLen + 1];
274   memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen);
275   // Put something in the target name.
276   raw[kMinChallengeHeaderLen] = 'Z';
277 
278   // Modify the default valid message to indicate 2 bytes are present in the
279   // target name payload (however there is only space for 1).
280   ASSERT_NE(0x02, raw[12]);
281   ASSERT_EQ(0x00, raw[13]);
282   ASSERT_NE(0x02, raw[14]);
283   ASSERT_EQ(0x00, raw[15]);
284   raw[12] = 0x02;
285   raw[14] = 0x02;
286 
287   NtlmBufferWriter writer(kMinChallengeHeaderLen + 1);
288   ASSERT_TRUE(writer.WriteBytes(raw));
289 
290   // The above malformed message could cause an implementation
291   // to read outside the message buffer because the length is
292   // longer than available space. Verify it gets rejected.
293   ASSERT_FALSE(GetAuthMsgResult(client, writer));
294 }
295 
TEST(NtlmClientTest,Type3UnicodeWithSessionSecuritySpecTest)296 TEST(NtlmClientTest, Type3UnicodeWithSessionSecuritySpecTest) {
297   NtlmClient client(NtlmFeatures(false));
298 
299   std::vector<uint8_t> result = GenerateAuthMsg(client, test::kChallengeMsgV1);
300 
301   ASSERT_FALSE(result.empty());
302   ASSERT_EQ(std::size(test::kExpectedAuthenticateMsgSpecResponseV1),
303             result.size());
304   ASSERT_EQ(0, memcmp(test::kExpectedAuthenticateMsgSpecResponseV1,
305                       result.data(), result.size()));
306 }
307 
TEST(NtlmClientTest,Type3WithoutUnicode)308 TEST(NtlmClientTest, Type3WithoutUnicode) {
309   NtlmClient client(NtlmFeatures(false));
310 
311   std::vector<uint8_t> result = GenerateAuthMsg(
312       client, base::make_span(test::kMinChallengeMessageNoUnicode)
313                   .subspan<0, kMinChallengeHeaderLen>());
314   ASSERT_FALSE(result.empty());
315 
316   NtlmBufferReader reader(result);
317   ASSERT_TRUE(reader.MatchMessageHeader(MessageType::kAuthenticate));
318 
319   // Read the LM and NTLM Response Payloads.
320   uint8_t actual_lm_response[kResponseLenV1];
321   uint8_t actual_ntlm_response[kResponseLenV1];
322 
323   ASSERT_TRUE(ReadBytesPayload(&reader, actual_lm_response));
324   ASSERT_TRUE(ReadBytesPayload(&reader, actual_ntlm_response));
325 
326   ASSERT_EQ(0, memcmp(test::kExpectedLmResponseWithV1SS, actual_lm_response,
327                       kResponseLenV1));
328   ASSERT_EQ(0, memcmp(test::kExpectedNtlmResponseWithV1SS, actual_ntlm_response,
329                       kResponseLenV1));
330 
331   std::string domain;
332   std::string username;
333   std::string hostname;
334   ASSERT_TRUE(ReadStringPayload(&reader, &domain));
335   ASSERT_EQ(test::kNtlmDomainAscii, domain);
336   ASSERT_TRUE(ReadStringPayload(&reader, &username));
337   ASSERT_EQ(test::kUserAscii, username);
338   ASSERT_TRUE(ReadStringPayload(&reader, &hostname));
339   ASSERT_EQ(test::kHostnameAscii, hostname);
340 
341   // The session key is not used in HTTP. Since NTLMSSP_NEGOTIATE_KEY_EXCH
342   // was not sent this is empty.
343   ASSERT_TRUE(reader.MatchEmptySecurityBuffer());
344 
345   // Verify the unicode flag is not set and OEM flag is.
346   NegotiateFlags flags;
347   ASSERT_TRUE(reader.ReadFlags(&flags));
348   ASSERT_EQ(NegotiateFlags::kNone, flags & NegotiateFlags::kUnicode);
349   ASSERT_EQ(NegotiateFlags::kOem, flags & NegotiateFlags::kOem);
350 }
351 
TEST(NtlmClientTest,ClientDoesNotDowngradeSessionSecurity)352 TEST(NtlmClientTest, ClientDoesNotDowngradeSessionSecurity) {
353   NtlmClient client(NtlmFeatures(false));
354 
355   std::vector<uint8_t> result =
356       GenerateAuthMsg(client, base::make_span(test::kMinChallengeMessageNoSS)
357                                   .subspan<0, kMinChallengeHeaderLen>());
358   ASSERT_FALSE(result.empty());
359 
360   NtlmBufferReader reader(result);
361   ASSERT_TRUE(reader.MatchMessageHeader(MessageType::kAuthenticate));
362 
363   // Read the LM and NTLM Response Payloads.
364   uint8_t actual_lm_response[kResponseLenV1];
365   uint8_t actual_ntlm_response[kResponseLenV1];
366 
367   ASSERT_TRUE(ReadBytesPayload(&reader, actual_lm_response));
368   ASSERT_TRUE(ReadBytesPayload(&reader, actual_ntlm_response));
369 
370   // The important part of this test is that even though the
371   // server told the client to drop session security. The client
372   // DID NOT drop it.
373   ASSERT_EQ(0, memcmp(test::kExpectedLmResponseWithV1SS, actual_lm_response,
374                       kResponseLenV1));
375   ASSERT_EQ(0, memcmp(test::kExpectedNtlmResponseWithV1SS, actual_ntlm_response,
376                       kResponseLenV1));
377 
378   std::u16string domain;
379   std::u16string username;
380   std::u16string hostname;
381   ASSERT_TRUE(ReadString16Payload(&reader, &domain));
382   ASSERT_EQ(test::kNtlmDomain, domain);
383   ASSERT_TRUE(ReadString16Payload(&reader, &username));
384   ASSERT_EQ(test::kUser, username);
385   ASSERT_TRUE(ReadString16Payload(&reader, &hostname));
386   ASSERT_EQ(test::kHostname, hostname);
387 
388   // The session key is not used in HTTP. Since NTLMSSP_NEGOTIATE_KEY_EXCH
389   // was not sent this is empty.
390   ASSERT_TRUE(reader.MatchEmptySecurityBuffer());
391 
392   // Verify the unicode and session security flag is set.
393   NegotiateFlags flags;
394   ASSERT_TRUE(reader.ReadFlags(&flags));
395   ASSERT_EQ(NegotiateFlags::kUnicode, flags & NegotiateFlags::kUnicode);
396   ASSERT_EQ(NegotiateFlags::kExtendedSessionSecurity,
397             flags & NegotiateFlags::kExtendedSessionSecurity);
398 }
399 
400 // ------------------------------------------------
401 // NTLM V2 specific tests.
402 // ------------------------------------------------
403 
TEST(NtlmClientTest,SimpleConstructionV2)404 TEST(NtlmClientTest, SimpleConstructionV2) {
405   NtlmClient client(NtlmFeatures(true));
406 
407   ASSERT_TRUE(client.IsNtlmV2());
408   ASSERT_TRUE(client.IsEpaEnabled());
409   ASSERT_TRUE(client.IsMicEnabled());
410 }
411 
TEST(NtlmClientTest,VerifyNegotiateMessageV2)412 TEST(NtlmClientTest, VerifyNegotiateMessageV2) {
413   NtlmClient client(NtlmFeatures(true));
414 
415   std::vector<uint8_t> result = client.GetNegotiateMessage();
416   ASSERT_FALSE(result.empty());
417   ASSERT_EQ(std::size(test::kExpectedNegotiateMsg), result.size());
418   ASSERT_EQ(0,
419             memcmp(test::kExpectedNegotiateMsg, result.data(), result.size()));
420 }
421 
TEST(NtlmClientTest,VerifyAuthenticateMessageV2)422 TEST(NtlmClientTest, VerifyAuthenticateMessageV2) {
423   // Generate the auth message from the client based on the test challenge
424   // message.
425   NtlmClient client(NtlmFeatures(true));
426   std::vector<uint8_t> result =
427       GenerateAuthMsg(client, test::kChallengeMsgFromSpecV2);
428   ASSERT_FALSE(result.empty());
429   ASSERT_EQ(std::size(test::kExpectedAuthenticateMsgSpecResponseV2),
430             result.size());
431   ASSERT_EQ(0, memcmp(test::kExpectedAuthenticateMsgSpecResponseV2,
432                       result.data(), result.size()));
433 }
434 
TEST(NtlmClientTest,VerifyAuthenticateMessageInResponseToChallengeWithoutTargetInfoV2)435 TEST(NtlmClientTest,
436      VerifyAuthenticateMessageInResponseToChallengeWithoutTargetInfoV2) {
437   // Test how the V2 client responds when the server sends a challenge that
438   // does not contain target info. eg. Windows 2003 and earlier do not send
439   // this. See [MS-NLMP] Appendix B Item 8. These older Windows servers
440   // support NTLMv2 but don't send target info. Other implementations may
441   // also be affected.
442   NtlmClient client(NtlmFeatures(true));
443   std::vector<uint8_t> result = GenerateAuthMsg(client, test::kChallengeMsgV1);
444   ASSERT_FALSE(result.empty());
445 
446   ASSERT_EQ(std::size(test::kExpectedAuthenticateMsgToOldV1ChallegeV2),
447             result.size());
448   ASSERT_EQ(0, memcmp(test::kExpectedAuthenticateMsgToOldV1ChallegeV2,
449                       result.data(), result.size()));
450 }
451 
452 // When the challenge message's target info is maximum size, adding new AV_PAIRs
453 // to the response will overflow SecurityBuffer. Test that we handle this.
TEST(NtlmClientTest,AvPairsOverflow)454 TEST(NtlmClientTest, AvPairsOverflow) {
455   {
456     NtlmClient client(NtlmFeatures(/*enable_NTLMv2=*/true));
457     std::vector<uint8_t> short_challenge;
458     ASSERT_NO_FATAL_FAILURE(MakeV2ChallengeMessage(0xfff, &short_challenge));
459     EXPECT_FALSE(GenerateAuthMsg(client, short_challenge).empty());
460   }
461   {
462     NtlmClient client(NtlmFeatures(/*enable_NTLMv2=*/true));
463     std::vector<uint8_t> long_challenge;
464     ASSERT_NO_FATAL_FAILURE(MakeV2ChallengeMessage(0xffff, &long_challenge));
465     EXPECT_TRUE(GenerateAuthMsg(client, long_challenge).empty());
466   }
467 }
468 
469 }  // namespace net::ntlm
470