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"