From 83f4f9b40135f137f4f6fb009067392884f82426 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Fri, 6 Jan 2017 19:04:22 +0100 Subject: Add HTTP strict tranport security support to QNAM HTTP Strict Transport Security (HSTS) is a web security policy that allows a web server to declare that user agents should only interact with it using secure HTTPS connections. HSTS is described by RFC6797. This patch introduces a new API in Network Access Manager to enable this policy or disable it (default - STS is disabled). We also implement QHstsCache which caches known HTTS hosts, does host name lookup and domain name matching; QHstsHeaderParser to parse HSTS headers with HSTS policies. A new autotest added to test the caching, host name matching and headers parsing. [ChangeLog][QtNetwork] Added HTTP Strict Transport Security to QNAM Task-number: QTPM-238 Change-Id: Iabb5920344bf204a0d3036284f0d60675c29315c Reviewed-by: Timur Pocheptsov --- tests/auto/network/access/hsts/hsts.pro | 6 + tests/auto/network/access/hsts/tst_qhsts.cpp | 318 +++++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 tests/auto/network/access/hsts/hsts.pro create mode 100644 tests/auto/network/access/hsts/tst_qhsts.cpp (limited to 'tests/auto/network/access/hsts') diff --git a/tests/auto/network/access/hsts/hsts.pro b/tests/auto/network/access/hsts/hsts.pro new file mode 100644 index 0000000000..07bdea5f62 --- /dev/null +++ b/tests/auto/network/access/hsts/hsts.pro @@ -0,0 +1,6 @@ +QT += core core-private network network-private testlib +CONFIG += testcase parallel_test c++11 +TEMPLATE = app +TARGET = tst_qhsts + +SOURCES += tst_qhsts.cpp diff --git a/tests/auto/network/access/hsts/tst_qhsts.cpp b/tests/auto/network/access/hsts/tst_qhsts.cpp new file mode 100644 index 0000000000..656516f46b --- /dev/null +++ b/tests/auto/network/access/hsts/tst_qhsts.cpp @@ -0,0 +1,318 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include + +#include +#include +#include +#include + +#include + +QT_USE_NAMESPACE + +class tst_QHsts : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testSingleKnownHost_data(); + void testSingleKnownHost(); + void testMultilpeKnownHosts(); + void testPolicyExpiration(); + void testSTSHeaderParser(); +}; + +void tst_QHsts::testSingleKnownHost_data() +{ + QTest::addColumn("knownHost"); + QTest::addColumn("policyExpires"); + QTest::addColumn("includeSubDomains"); + QTest::addColumn("hostToTest"); + QTest::addColumn("isKnown"); + + const QDateTime currentUTC = QDateTime::currentDateTimeUtc(); + const QUrl knownHost(QLatin1String("http://example.com")); + const QUrl validSubdomain(QLatin1String("https://sub.example.com/ohoho")); + const QUrl unknownDomain(QLatin1String("http://example.org")); + const QUrl subSubdomain(QLatin1String("https://level3.level2.example.com")); + + const QDateTime validDate(currentUTC.addSecs(1000)); + QTest::newRow("same-known") << knownHost << validDate << false << knownHost << true; + QTest::newRow("subexcluded") << knownHost << validDate << false << validSubdomain << false; + QTest::newRow("subincluded") << knownHost << validDate << true << validSubdomain << true; + QTest::newRow("unknown-subexcluded") << knownHost << validDate << false << unknownDomain << false; + QTest::newRow("unknown-subincluded") << knownHost << validDate << true << unknownDomain << false; + QTest::newRow("sub-subdomain-subincluded") << knownHost << validDate << true << subSubdomain << true; + QTest::newRow("sub-subdomain-subexcluded") << knownHost << validDate << false << subSubdomain << false; + + const QDateTime invalidDate; + QTest::newRow("invalid-time") << knownHost << invalidDate << false << knownHost << false; + QTest::newRow("invalid-time-subexcluded") << knownHost << invalidDate << false + << validSubdomain << false; + QTest::newRow("invalid-time-subincluded") << knownHost << invalidDate << true + << validSubdomain << false; + + const QDateTime expiredDate(currentUTC.addSecs(-1000)); + QTest::newRow("expired-time") << knownHost << expiredDate << false << knownHost << false; + QTest::newRow("expired-time-subexcluded") << knownHost << expiredDate << false + << validSubdomain << false; + QTest::newRow("expired-time-subincluded") << knownHost << expiredDate << true + << validSubdomain << false; + const QUrl ipAsHost(QLatin1String("http://127.0.0.1")); + QTest::newRow("ip-address-in-hostname") << ipAsHost << validDate << false + << ipAsHost << false; + + const QUrl anyIPv4AsHost(QLatin1String("http://0.0.0.0")); + QTest::newRow("anyip4-address-in-hostname") << anyIPv4AsHost << validDate + << false << anyIPv4AsHost << false; + const QUrl anyIPv6AsHost(QLatin1String("http://[::]")); + QTest::newRow("anyip6-address-in-hostname") << anyIPv6AsHost << validDate + << false << anyIPv6AsHost << false; + +} + +void tst_QHsts::testSingleKnownHost() +{ + QFETCH(const QUrl, knownHost); + QFETCH(const QDateTime, policyExpires); + QFETCH(const bool, includeSubDomains); + QFETCH(const QUrl, hostToTest); + QFETCH(const bool, isKnown); + + QHstsCache cache; + cache.updateKnownHost(knownHost, policyExpires, includeSubDomains); + QCOMPARE(cache.isKnownHost(hostToTest), isKnown); +} + +void tst_QHsts::testMultilpeKnownHosts() +{ + const QDateTime currentUTC = QDateTime::currentDateTimeUtc(); + const QDateTime validDate(currentUTC.addSecs(10000)); + const QDateTime expiredDate(currentUTC.addSecs(-10000)); + const QUrl exampleCom(QLatin1String("https://example.com")); + const QUrl subExampleCom(QLatin1String("https://sub.example.com")); + + QHstsCache cache; + // example.com is HSTS and includes subdomains: + cache.updateKnownHost(exampleCom, validDate, true); + QVERIFY(cache.isKnownHost(exampleCom)); + QVERIFY(cache.isKnownHost(subExampleCom)); + // example.com can set its policy not to include subdomains: + cache.updateKnownHost(exampleCom, validDate, false); + QVERIFY(!cache.isKnownHost(subExampleCom)); + // but sub.example.com can set its own policy: + cache.updateKnownHost(subExampleCom, validDate, false); + QVERIFY(cache.isKnownHost(subExampleCom)); + // let's say example.com's policy has expired: + cache.updateKnownHost(exampleCom, expiredDate, false); + QVERIFY(!cache.isKnownHost(exampleCom)); + // it should not affect sub.example.com's policy: + QVERIFY(cache.isKnownHost(subExampleCom)); + + // clear cache and invalidate all policies: + cache.clear(); + QVERIFY(!cache.isKnownHost(exampleCom)); + QVERIFY(!cache.isKnownHost(subExampleCom)); + + // siblings: + const QUrl anotherSub(QLatin1String("https://sub2.example.com")); + cache.updateKnownHost(subExampleCom, validDate, true); + cache.updateKnownHost(anotherSub, validDate, true); + QVERIFY(cache.isKnownHost(subExampleCom)); + QVERIFY(cache.isKnownHost(anotherSub)); + // they cannot set superdomain's policy: + QVERIFY(!cache.isKnownHost(exampleCom)); + // a sibling cannot set another sibling's policy: + cache.updateKnownHost(anotherSub, expiredDate, false); + QVERIFY(cache.isKnownHost(subExampleCom)); + QVERIFY(!cache.isKnownHost(anotherSub)); + QVERIFY(!cache.isKnownHost(exampleCom)); + // let's make example.com known again: + cache.updateKnownHost(exampleCom, validDate, true); + // a subdomain cannot affect its superdomain's policy: + cache.updateKnownHost(subExampleCom, expiredDate, true); + QVERIFY(cache.isKnownHost(exampleCom)); + // and this superdomain includes subdomains in its HSTS policy: + QVERIFY(cache.isKnownHost(subExampleCom)); + QVERIFY(cache.isKnownHost(anotherSub)); + + // a subdomain (with its subdomains) cannot affect its superdomain's policy: + cache.updateKnownHost(exampleCom, expiredDate, true); + cache.updateKnownHost(subExampleCom, validDate, true); + QVERIFY(cache.isKnownHost(subExampleCom)); + QVERIFY(!cache.isKnownHost(exampleCom)); +} + +void tst_QHsts::testPolicyExpiration() +{ + QDateTime currentUTC = QDateTime::currentDateTimeUtc(); + const QUrl exampleCom(QLatin1String("http://example.com")); + const QUrl subdomain(QLatin1String("http://subdomain.example.com")); + const qint64 lifeTimeMS = 50; + + QHstsCache cache; + // start with 'includeSubDomains' and 5 s. lifetime: + cache.updateKnownHost(exampleCom, currentUTC.addMSecs(lifeTimeMS), true); + QVERIFY(cache.isKnownHost(exampleCom)); + QVERIFY(cache.isKnownHost(subdomain)); + // wait for approx. a half of lifetime: + QTest::qWait(lifeTimeMS / 2); + + if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(lifeTimeMS)) { + // Should still be valid: + QVERIFY(cache.isKnownHost(exampleCom)); + QVERIFY(cache.isKnownHost(subdomain)); + } + + QTest::qWait(lifeTimeMS); + // expired: + QVERIFY(!cache.isKnownHost(exampleCom)); + QVERIFY(!cache.isKnownHost(subdomain)); + + // now check that superdomain's policy expires, but not subdomain's policy: + currentUTC = QDateTime::currentDateTimeUtc(); + cache.updateKnownHost(exampleCom, currentUTC.addMSecs(lifeTimeMS / 5), true); + cache.updateKnownHost(subdomain, currentUTC.addMSecs(lifeTimeMS), true); + QVERIFY(cache.isKnownHost(exampleCom)); + QVERIFY(cache.isKnownHost(subdomain)); + QTest::qWait(lifeTimeMS / 2); + if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(lifeTimeMS)) { + QVERIFY(!cache.isKnownHost(exampleCom)); + QVERIFY(cache.isKnownHost(subdomain)); + } +} + +void tst_QHsts::testSTSHeaderParser() +{ + QHstsHeaderParser parser; + using Header = QPair; + using Headers = QList
; + + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + Headers list; + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + + list << Header("Strict-Transport-security", "200"); + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + + // This header is missing REQUIRED max-age directive, so we'll ignore it: + list << Header("Strict-Transport-Security", "includeSubDomains"); + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + + list.pop_back(); + list << Header("Strict-Transport-Security", "includeSubDomains;max-age=1000"); + QVERIFY(parser.parse(list)); + QVERIFY(parser.expirationDate() > QDateTime::currentDateTimeUtc()); + QVERIFY(parser.includeSubDomains()); + + list.pop_back(); + // Invalid (includeSubDomains twice): + list << Header("Strict-Transport-Security", "max-age = 1000 ; includeSubDomains;includeSubDomains"); + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + + list.pop_back(); + // Invalid (weird number of seconds): + list << Header("Strict-Transport-Security", "max-age=-1000 ; includeSubDomains"); + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + + list.pop_back(); + // Note, directives are case-insensitive + we should ignore unknown directive. + list << Header("Strict-Transport-Security", ";max-age=1000 ;includesubdomains;;" + "nowsomeunknownheader=\"somevaluewithescapes\\;\""); + QVERIFY(parser.parse(list)); + QVERIFY(parser.includeSubDomains()); + QVERIFY(parser.expirationDate().isValid()); + + list.pop_back(); + // Check that we know how to unescape max-age: + list << Header("Strict-Transport-Security", "max-age=\"1000\""); + QVERIFY(parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(parser.expirationDate().isValid()); + + list.pop_back(); + // The only STS header, with invalid syntax though, to be ignored: + list << Header("Strict-Transport-Security", "max-age; max-age=15768000"); + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); + + // Now we check that our parse chosses the first valid STS header and ignores + // others: + list.clear(); + list << Header("Strict-Transport-Security", "includeSubdomains; max-age=\"hehehe\";"); + list << Header("Strict-Transport-Security", "max-age=10101"); + QVERIFY(parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(parser.expirationDate().isValid()); + + + list.clear(); + list << Header("Strict-Transport-Security", "max-age=0"); + QVERIFY(parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(parser.expirationDate() <= QDateTime::currentDateTimeUtc()); + + // Parsing is case-insensitive: + list.pop_back(); + list << Header("Strict-Transport-Security", "Max-aGE=1000; InclUdesUbdomains"); + QVERIFY(parser.parse(list)); + QVERIFY(parser.includeSubDomains()); + QVERIFY(parser.expirationDate().isValid()); + + // Grammar of STS header is quite permissive, let's check we can parse + // some weird but valid header: + list.pop_back(); + list << Header("Strict-Transport-Security", ";;; max-age = 17; ; ; ; ;;; ;;" + ";;; ; includeSubdomains ;;thisIsUnknownDirective;;;;"); + QVERIFY(parser.parse(list)); + QVERIFY(parser.includeSubDomains()); + QVERIFY(parser.expirationDate().isValid()); + + list.pop_back(); + list << Header("Strict-Transport-Security", "max-age=1000; includeSubDomains bogon"); + QVERIFY(!parser.parse(list)); + QVERIFY(!parser.includeSubDomains()); + QVERIFY(!parser.expirationDate().isValid()); +} + +QTEST_MAIN(tst_QHsts) + +#include "tst_qhsts.moc" -- cgit v1.2.3