File indexing completed on 2024-04-28 15:25:52
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2011 Dawit Alemayehu <adawit@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "httpauthenticationtest.h" 0009 0010 #include <QTest> 0011 0012 #include <QByteArray> 0013 #include <QList> 0014 #include <QtEndian> 0015 0016 #include <KConfig> 0017 0018 #define ENABLE_HTTP_AUTH_NONCE_SETTER 0019 #include "httpauthentication.cpp" 0020 0021 // QT5 TODO QTEST_GUILESS_MAIN(HTTPAuthenticationTest) 0022 QTEST_MAIN(HTTPAuthenticationTest) 0023 0024 static void parseAuthHeader(const QByteArray &header, QByteArray *bestOffer, QByteArray *scheme, QList<QByteArray> *result) 0025 { 0026 const QList<QByteArray> authHeaders = KAbstractHttpAuthentication::splitOffers(QList<QByteArray>{header}); 0027 QByteArray chosenHeader = KAbstractHttpAuthentication::bestOffer(authHeaders); 0028 0029 if (bestOffer) { 0030 *bestOffer = chosenHeader; 0031 } 0032 0033 if (!scheme && !result) { 0034 return; 0035 } 0036 0037 QByteArray authScheme; 0038 const QList<QByteArray> parseResult = parseChallenge(chosenHeader, &authScheme); 0039 0040 if (scheme) { 0041 *scheme = authScheme; 0042 } 0043 0044 if (result) { 0045 *result = parseResult; 0046 } 0047 } 0048 0049 static QByteArray hmacMD5(const QByteArray &data, const QByteArray &key) 0050 { 0051 QByteArray ipad(64, 0x36); 0052 QByteArray opad(64, 0x5c); 0053 0054 Q_ASSERT(key.size() <= 64); 0055 0056 for (int i = qMin(key.size(), 64) - 1; i >= 0; i--) { 0057 ipad.data()[i] ^= key[i]; 0058 opad.data()[i] ^= key[i]; 0059 } 0060 0061 QByteArray content(ipad + data); 0062 0063 QCryptographicHash md5(QCryptographicHash::Md5); 0064 md5.addData(content); 0065 content = opad + md5.result(); 0066 0067 md5.reset(); 0068 md5.addData(content); 0069 0070 return md5.result(); 0071 } 0072 0073 static QByteArray QString2UnicodeLE(const QString &target) 0074 { 0075 QByteArray unicode(target.length() * 2, 0); 0076 0077 for (int i = 0; i < target.length(); i++) { 0078 ((quint16 *)unicode.data())[i] = qToLittleEndian(target[i].unicode()); 0079 } 0080 0081 return unicode; 0082 } 0083 0084 void HTTPAuthenticationTest::testHeaderParsing_data() 0085 { 0086 QTest::addColumn<QByteArray>("header"); 0087 QTest::addColumn<QByteArray>("resultScheme"); 0088 QTest::addColumn<QByteArray>("resultValues"); 0089 0090 // Tests cases from http://greenbytes.de/tech/tc/httpauth/ 0091 QTest::newRow("greenbytes-simplebasic") << QByteArray("Basic realm=\"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); 0092 QTest::newRow("greenbytes-simplebasictok") << QByteArray("Basic realm=foo") << QByteArray("Basic") << QByteArray("realm,foo"); 0093 QTest::newRow("greenbytes-simplebasiccomma") << QByteArray("Basic , realm=\"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); 0094 // there must be a space after the scheme 0095 QTest::newRow("greenbytes-simplebasiccomma2") << QByteArray("Basic, realm=\"foo\"") << QByteArray() << QByteArray(); 0096 // we accept scheme without any parameters to maintain compatibility with too simple minded servers out there 0097 QTest::newRow("greenbytes-simplebasicnorealm") << QByteArray("Basic") << QByteArray("Basic") << QByteArray(); 0098 QTest::newRow("greenbytes-simplebasicwsrealm") << QByteArray("Basic realm = \"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); 0099 QTest::newRow("greenbytes-simplebasicrealmsqc") << QByteArray("Basic realm=\"\\f\\o\\o\"") << QByteArray("Basic") << QByteArray("realm,foo"); 0100 QTest::newRow("greenbytes-simplebasicrealmsqc2") << QByteArray("Basic realm=\"\\\"foo\\\"\"") << QByteArray("Basic") << QByteArray("realm,\"foo\""); 0101 QTest::newRow("greenbytes-simplebasicnewparam1") << QByteArray("Basic realm=\"foo\", bar=\"xyz\"") << QByteArray("Basic") 0102 << QByteArray("realm,foo,bar,xyz"); 0103 QTest::newRow("greenbytes-simplebasicnewparam2") << QByteArray("Basic bar=\"xyz\", realm=\"foo\"") << QByteArray("Basic") 0104 << QByteArray("bar,xyz,realm,foo"); 0105 // a Basic challenge following an empty one 0106 QTest::newRow("greenbytes-multibasicempty") << QByteArray(",Basic realm=\"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); 0107 QTest::newRow("greenbytes-multibasicunknown") << QByteArray("Basic realm=\"basic\", Newauth realm=\"newauth\"") << QByteArray("Basic") 0108 << QByteArray("realm,basic"); 0109 QTest::newRow("greenbytes-multibasicunknown2") << QByteArray("Newauth realm=\"newauth\", Basic realm=\"basic\"") << QByteArray("Basic") 0110 << QByteArray("realm,basic"); 0111 QTest::newRow("greenbytes-unknown") << QByteArray("Newauth realm=\"newauth\"") << QByteArray() << QByteArray(); 0112 0113 // Misc. test cases 0114 QTest::newRow("ntlm") << QByteArray("NTLM ") << QByteArray("NTLM") << QByteArray(); 0115 QTest::newRow("unterminated-quoted-value") << QByteArray("Basic realm=\"") << QByteArray("Basic") << QByteArray(); 0116 QTest::newRow("spacing-and-tabs") << QByteArray("bAsic bar\t =\t\"baz\", realm =\t\"foo\"") << QByteArray("bAsic") << QByteArray("bar,baz,realm,foo"); 0117 QTest::newRow("empty-fields") << QByteArray("Basic realm=foo , , , ,, bar=\"baz\"\t,") << QByteArray("Basic") << QByteArray("realm,foo,bar,baz"); 0118 QTest::newRow("spacing") << QByteArray("Basic realm=foo, bar = baz") << QByteArray("Basic") << QByteArray("realm,foo,bar,baz"); 0119 QTest::newRow("missing-comma-between-fields") << QByteArray("Basic realm=foo bar = baz") << QByteArray("Basic") << QByteArray("realm,foo"); 0120 // quotes around text, every character needlessly quoted 0121 QTest::newRow("quote-excess") << QByteArray("Basic realm=\"\\\"\\f\\o\\o\\\"\"") << QByteArray("Basic") << QByteArray("realm,\"foo\""); 0122 // quotes around text, quoted backslashes 0123 QTest::newRow("quoted-backslash") << QByteArray("Basic realm=\"\\\"foo\\\\\\\\\"") << QByteArray("Basic") << QByteArray("realm,\"foo\\\\"); 0124 // quotes around text, quoted backslashes, quote hidden behind them 0125 QTest::newRow("quoted-backslash-and-quote") << QByteArray("Basic realm=\"\\\"foo\\\\\\\"\"") << QByteArray("Basic") << QByteArray("realm,\"foo\\\""); 0126 // invalid quoted text 0127 QTest::newRow("invalid-quoted") << QByteArray("Basic realm=\"\\\"foo\\\\\\\"") << QByteArray("Basic") << QByteArray(); 0128 // ends in backslash without quoted value 0129 QTest::newRow("invalid-quote") << QByteArray("Basic realm=\"\\\"foo\\\\\\") << QByteArray("Basic") << QByteArray(); 0130 } 0131 0132 QByteArray joinQByteArray(const QList<QByteArray> &list) 0133 { 0134 QByteArray data; 0135 const int count = list.count(); 0136 0137 for (int i = 0; i < count; ++i) { 0138 if (i > 0) { 0139 data += ','; 0140 } 0141 data += list.at(i); 0142 } 0143 0144 return data; 0145 } 0146 0147 void HTTPAuthenticationTest::testHeaderParsing() 0148 { 0149 QFETCH(QByteArray, header); 0150 QFETCH(QByteArray, resultScheme); 0151 QFETCH(QByteArray, resultValues); 0152 0153 QByteArray chosenHeader; 0154 QByteArray chosenScheme; 0155 QList<QByteArray> parsingResult; 0156 parseAuthHeader(header, &chosenHeader, &chosenScheme, &parsingResult); 0157 QCOMPARE(chosenScheme, resultScheme); 0158 QCOMPARE(joinQByteArray(parsingResult), resultValues); 0159 } 0160 0161 void HTTPAuthenticationTest::testAuthenticationSelection_data() 0162 { 0163 QTest::addColumn<QByteArray>("input"); 0164 QTest::addColumn<QByteArray>("expectedScheme"); 0165 QTest::addColumn<QByteArray>("expectedOffer"); 0166 0167 #if HAVE_LIBGSSAPI 0168 QTest::newRow("all-with-negotiate") << QByteArray("Negotiate , Digest , NTLM , Basic") << QByteArray("Negotiate") << QByteArray("Negotiate"); 0169 #endif 0170 QTest::newRow("all-without-negotiate") << QByteArray("Digest , NTLM , Basic , NewAuth") << QByteArray("Digest") << QByteArray("Digest"); 0171 QTest::newRow("ntlm-basic-unknown") << QByteArray("NTLM , Basic , NewAuth") << QByteArray("NTLM") << QByteArray("NTLM"); 0172 QTest::newRow("basic-unknown") << QByteArray("Basic , NewAuth") << QByteArray("Basic") << QByteArray("Basic"); 0173 QTest::newRow("ntlm-basic+param-ntlm") << QByteArray("NTLM , Basic realm=foo, bar = baz, NTLM") << QByteArray("NTLM") << QByteArray("NTLM"); 0174 QTest::newRow("ntlm-with-type{2|3}") << QByteArray("NTLM VFlQRV8yX09SXzNfTUVTU0FHRQo=") << QByteArray("NTLM") 0175 << QByteArray("NTLM VFlQRV8yX09SXzNfTUVTU0FHRQo="); 0176 0177 // Unknown schemes always return blank, i.e. auth request should be ignored 0178 QTest::newRow("unknown-param") << QByteArray("Newauth realm=\"newauth\"") << QByteArray() << QByteArray(); 0179 QTest::newRow("unknown-unknown") << QByteArray("NewAuth , NewAuth2") << QByteArray() << QByteArray(); 0180 } 0181 0182 void HTTPAuthenticationTest::testAuthenticationSelection() 0183 { 0184 QFETCH(QByteArray, input); 0185 QFETCH(QByteArray, expectedScheme); 0186 QFETCH(QByteArray, expectedOffer); 0187 0188 QByteArray scheme; 0189 QByteArray offer; 0190 parseAuthHeader(input, &offer, &scheme, nullptr); 0191 QCOMPARE(scheme, expectedScheme); 0192 QCOMPARE(offer, expectedOffer); 0193 } 0194 0195 void HTTPAuthenticationTest::testAuthentication_data() 0196 { 0197 QTest::addColumn<QByteArray>("input"); 0198 QTest::addColumn<QByteArray>("expectedResponse"); 0199 QTest::addColumn<QByteArray>("user"); 0200 QTest::addColumn<QByteArray>("pass"); 0201 QTest::addColumn<QByteArray>("url"); 0202 QTest::addColumn<QByteArray>("cnonce"); 0203 0204 // Test cases from RFC 2617... 0205 /* clang-format off */ 0206 QTest::newRow("rfc-2617-basic-example") << QByteArray("Basic realm=\"WallyWorld\"") 0207 << QByteArray("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==") 0208 << QByteArray("Aladdin") 0209 << QByteArray("open sesame") 0210 << QByteArray() 0211 << QByteArray(); 0212 0213 QTest::newRow("rfc-2617-digest-example") 0214 << QByteArray("Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"," 0215 "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"") 0216 << QByteArray("Digest username=\"Mufasa\", realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " 0217 "uri=\"/dir/index.html\", algorithm=MD5, qop=auth, cnonce=\"0a4f113b\", nc=00000001, " 0218 "response=\"6629fae49393a05397450978507c4ef1\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"") 0219 << QByteArray("Mufasa") 0220 << QByteArray("Circle Of Life") 0221 << QByteArray("http://www.nowhere.org/dir/index.html") 0222 << QByteArray("0a4f113b"); 0223 0224 QTest::newRow("ntlm-negotiate-type1") << QByteArray("NTLM") 0225 << QByteArray("NTLM TlRMTVNTUAABAAAABQIAAAAAAAAAAAAAAAAAAAAAAAA=") 0226 << QByteArray() 0227 << QByteArray() 0228 << QByteArray() 0229 << QByteArray(); 0230 0231 QTest::newRow("ntlm-challenge-type2") 0232 << QByteArray("NTLM TlRMTVNTUAACAAAAFAAUACgAAAABggAAU3J2Tm9uY2UAAAAAAAAAAFUAcgBzAGEALQBNAGEAagBvAHIA") 0233 << QByteArray("NTLM " 0234 "TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAABQAFABwAAAADAAMAIQAAAAWABYAkAAAAAAAAAAAAAAAAYIAAODgDeMQShvyBT8Hx92oLTxImumJ4bAA062Hym3v40aFucQ8R3qMQtYAZn1okufol1UAcgBzAGEALQBNAGkAbgBvAHIAWgBhAHAAaABvAGQAVwBPAFIASwBTAFQAQQBUAEkATwBOAA==") 0235 << QByteArray("Ursa-Minor\\Zaphod") 0236 << QByteArray("Beeblebrox") 0237 << QByteArray() 0238 << QByteArray(); 0239 0240 QTest::newRow("ntlm-challenge-type2-no-domain") 0241 << QByteArray("NTLM TlRMTVNTUAACAAAAFAAUACgAAAABggAAU3J2Tm9uY2UAAAAAAAAAAFUAcgBzAGEALQBNAGEAagBvAHIA") 0242 << QByteArray("NTLM " 0243 "TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAABQAFABwAAAADAAMAIQAAAAWABYAkAAAAAAAAAAAAAAAAYIAAODgDeMQShvyBT8Hx92oLTxImumJ4bAA062Hym3v40aFucQ8R3qMQtYAZn1okufol1UAcgBzAGEALQBNAGEAagBvAHIAWgBhAHAAaABvAGQAVwBPAFIASwBTAFQAQQBUAEkATwBOAA==") 0244 << QByteArray("Zaphod") 0245 << QByteArray("Beeblebrox") 0246 << QByteArray() 0247 << QByteArray(); 0248 0249 QTest::newRow("ntlm-challenge-type2-empty-domain") 0250 << QByteArray("NTLM TlRMTVNTUAACAAAAFAAUACgAAAABggAAU3J2Tm9uY2UAAAAAAAAAAFUAcgBzAGEALQBNAGEAagBvAHIA") 0251 << QByteArray("NTLM " 0252 "TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAAAAAAAAAAAAADAAMAHAAAAAWABYAfAAAAAAAAAAAAAAAAYIAAODgDeMQShvyBT8Hx92oLTxImumJ4bAA062Hym3v40aFucQ8R3qMQtYAZn1okufol1oAYQBwAGgAbwBkAFcATwBSAEsAUwBUAEEAVABJAE8ATgA=") 0253 << QByteArray("\\Zaphod") 0254 << QByteArray("Beeblebrox") 0255 << QByteArray() 0256 << QByteArray(); 0257 /* clang-format on */ 0258 } 0259 0260 void HTTPAuthenticationTest::testAuthentication() 0261 { 0262 QFETCH(QByteArray, input); 0263 QFETCH(QByteArray, expectedResponse); 0264 QFETCH(QByteArray, user); 0265 QFETCH(QByteArray, pass); 0266 QFETCH(QByteArray, url); 0267 QFETCH(QByteArray, cnonce); 0268 0269 QByteArray bestOffer; 0270 parseAuthHeader(input, &bestOffer, nullptr, nullptr); 0271 KAbstractHttpAuthentication *authObj = KAbstractHttpAuthentication::newAuth(bestOffer); 0272 QVERIFY(authObj); 0273 if (!cnonce.isEmpty()) { 0274 authObj->setDigestNonceValue(cnonce); 0275 } 0276 authObj->setChallenge(bestOffer, QUrl(url), "GET"); 0277 authObj->generateResponse(QString(user), QString(pass)); 0278 QCOMPARE(authObj->headerFragment().trimmed().constData(), expectedResponse.constData()); 0279 delete authObj; 0280 } 0281 0282 void HTTPAuthenticationTest::testAuthenticationNTLMv2() 0283 { 0284 /* clang-format off */ 0285 QByteArray input( 0286 "NTLM " 0287 "TlRMTVNTUAACAAAABgAGADgAAAAFAokCT0wyUnb4OSQAAAAAAAAAAMYAxgA+AAAABgGxHQAAAA9UAFMAVAACAAYAVABTAFQAAQASAEQAVgBHAFIASwBWAFEAUABEAAQAKgB0AHMAdAAuAGQAagBrAGgAcQBjAGkAaABtAGMAbwBmAGoALgBvAHIAZwADAD4ARABWAEcAUgBLAFYAUQBQAEQALgB0AHMAdAAuAGQAagBrAGgAcQBjAGkAaABtAGMAbwBmAGoALgBvAHIAZwAFACIAZABqAGsAaABxAGMAaQBoAG0AYwBvAGYAagAuAG8AcgBnAAcACABvb9jXZl7RAQAAAAA="); 0288 0289 QByteArray expectedResponse( 0290 "TlRMTVNTUAADAAAAGAAYADYBAAD2APYAQAAAAAYABgBOAQAABgAGAFQBAAAWABYAWgEAAAAAAAAAAAAABQKJArXyhsxZPveKcfcV21viIsUBAQAAAAAAAAC8GQxfX9EBTHOi1kJbHbQAAAAAAgAGAFQAUwBUAAEAEgBEAFYARwBSAEsAVgBRAFAARAAEACoAdABzAHQALgBkAGoAawBoAHEAYwBpAGgAbQBjAG8AZgBqAC4AbwByAGcAAwA+AEQAVgBHAFIASwBWAFEAUABEAC4AdABzAHQALgBkAGoAawBoAHEAYwBpAGgAbQBjAG8AZgBqAC4AbwByAGcABQAiAGQAagBrAGgAcQBjAGkAaABtAGMAbwBmAGoALgBvAHIAZwAHAAgAb2/Y12Ze0QEAAAAAAAAAAOInN0N/15GHBtz3WXvvV159KG/2MbYk0FQAUwBUAGIAbwBiAFcATwBSAEsAUwBUAEEAVABJAE8ATgA="); 0291 /* clang-format on */ 0292 0293 QString user("TST\\bob"); 0294 QString pass("cacamas"); 0295 QString target("TST"); 0296 0297 QByteArray bestOffer; 0298 parseAuthHeader(input, &bestOffer, nullptr, nullptr); 0299 KConfig conf; 0300 KConfigGroup confGroup = conf.group("test"); 0301 confGroup.writeEntry("EnableNTLMv2Auth", true); 0302 KAbstractHttpAuthentication *authObj = KAbstractHttpAuthentication::newAuth(bestOffer, &confGroup); 0303 QVERIFY(authObj); 0304 0305 authObj->setChallenge(bestOffer, QUrl(), "GET"); 0306 authObj->generateResponse(QString(user), QString(pass)); 0307 0308 QByteArray resp(QByteArray::fromBase64(authObj->headerFragment().trimmed().mid(5))); 0309 QByteArray expResp(QByteArray::fromBase64(expectedResponse)); 0310 0311 /* Prepare responses stripped from any data that is variable. */ 0312 QByteArray strippedResp(resp); 0313 memset(strippedResp.data() + 0x40, 0, 0x10); // NTLMv2 MAC 0314 memset(strippedResp.data() + 0x58, 0, 0x10); // timestamp + client nonce 0315 memset(strippedResp.data() + 0x136, 0, 0x18); // LMv2 MAC 0316 QByteArray strippedExpResp(expResp); 0317 memset(strippedExpResp.data() + 0x40, 0, 0x10); // NTLMv2 MAC 0318 memset(strippedExpResp.data() + 0x58, 0, 0x10); // timestamp + client nonce 0319 memset(strippedExpResp.data() + 0x136, 0, 0x18); // LMv2 MAC 0320 0321 /* Compare the stripped responses. */ 0322 QCOMPARE(strippedResp.toBase64(), strippedExpResp.toBase64()); 0323 0324 /* Verify the NTLMv2 response MAC. */ 0325 QByteArray challenge(QByteArray::fromBase64(input.mid(5))); 0326 QByteArray serverNonce(challenge.mid(0x18, 8)); 0327 0328 QByteArray uniPass(QString2UnicodeLE(pass)); 0329 QByteArray ntlmHash(QCryptographicHash::hash(uniPass, QCryptographicHash::Md4)); 0330 int i = user.indexOf('\\'); 0331 QString username; 0332 if (i >= 0) { 0333 username = user.mid(i + 1); 0334 } else { 0335 username = user; 0336 } 0337 0338 QByteArray userTarget(QString2UnicodeLE(username.toUpper() + target)); 0339 QByteArray ntlm2Hash(hmacMD5(userTarget, ntlmHash)); 0340 QByteArray hashData(serverNonce + resp.mid(0x50, 230)); 0341 QByteArray mac(hmacMD5(hashData, ntlm2Hash)); 0342 0343 QCOMPARE(mac.toHex(), resp.mid(0x40, 16).toHex()); 0344 0345 /* Verify the LMv2 response MAC. */ 0346 QByteArray lmHashData(serverNonce + resp.mid(0x146, 8)); 0347 QByteArray lmHash(hmacMD5(lmHashData, ntlm2Hash)); 0348 0349 QCOMPARE(lmHash.toHex(), resp.mid(0x136, 16).toHex()); 0350 0351 delete authObj; 0352 } 0353 0354 #include "moc_httpauthenticationtest.cpp"