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 <timur.pocheptsov@qt.io>
This commit is contained in:
Timur Pocheptsov 2017-01-06 19:04:22 +01:00
parent da0241a2e7
commit 83f4f9b401
11 changed files with 1130 additions and 5 deletions

View File

@ -39,7 +39,8 @@ HEADERS += \
access/qhttpmultipart.h \
access/qhttpmultipart_p.h \
access/qnetworkfile_p.h \
access/qhttp2protocolhandler_p.h
access/qhttp2protocolhandler_p.h \
access/qhsts_p.h
SOURCES += \
access/qftp.cpp \
@ -72,7 +73,8 @@ SOURCES += \
access/qhttpthreaddelegate.cpp \
access/qhttpmultipart.cpp \
access/qnetworkfile.cpp \
access/qhttp2protocolhandler.cpp
access/qhttp2protocolhandler.cpp \
access/qhsts.cpp
mac: LIBS_PRIVATE += -framework Security

View File

@ -0,0 +1,522 @@
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtNetwork module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qhsts_p.h"
#include "QtCore/qstringlist.h"
#include "QtCore/private/qipaddress_p.h"
QT_BEGIN_NAMESPACE
static bool expired_policy(const QDateTime &expires)
{
return !expires.isValid() || expires <= QDateTime::currentDateTimeUtc();
}
static bool has_valid_domain_name(const QUrl &url)
{
if (!url.isValid())
return false;
const QString host(url.host());
if (!host.size())
return false;
// RFC6797 8.1.1
// If the substring matching the host production from the Request-URI
// (of the message to which the host responded) syntactically matches
//the IP-literal or IPv4address productions from Section 3.2.2 of
//[RFC3986], then the UA MUST NOT note this host as a Known HSTS Host.
using namespace QIPAddressUtils;
IPv4Address ipv4Addr = {};
if (parseIp4(ipv4Addr, host.constBegin(), host.constEnd()))
return false;
IPv6Address ipv6Addr = {};
// Unlike parseIp4, parseIp6 returns nullptr if it managed to parse IPv6
// address successfully.
if (!parseIp6(ipv6Addr, host.constBegin(), host.constEnd()))
return false;
// TODO: for now we do not test IPvFuture address, it must be addressed
// by introducing parseIpFuture (actually, there is an implementation
// in QUrl that can be adopted/modified/moved to QIPAddressUtils).
return true;
}
QHstsCache::QHstsCache()
{
// Top-level domain without any label.
children.push_back(Domain());
}
void QHstsCache::updateFromHeaders(const QList<QPair<QByteArray, QByteArray>> &headers,
const QUrl &url)
{
if (!has_valid_domain_name(url))
return;
QHstsHeaderParser parser;
if (parser.parse(headers))
updateKnownHost(url, parser.expirationDate(), parser.includeSubDomains());
}
void QHstsCache::updateKnownHost(const QUrl &originalUrl, const QDateTime &expires,
bool includeSubDomains)
{
if (!has_valid_domain_name(originalUrl))
return;
// HSTS is a per-host policy, regardless of protocol, port or any of the other
// details in an URL; so we only want the host part. We still package this as
// a QUrl since this handles IDNA 2003 (RFC3490) for us, as required by
// HSTS (RFC6797, section 10).
QUrl url;
url.setHost(originalUrl.host());
// 1. Update our hosts:
QStringList labels(url.host().split(QLatin1Char('.')));
std::reverse(labels.begin(), labels.end());
size_type domainIndex = 0;
for (int i = 0, e = labels.size(); i < e; ++i) {
Q_ASSERT(domainIndex < children.size());
auto &subDomains = children[domainIndex].labels;
const auto &label = labels[i];
auto pos = std::lower_bound(subDomains.begin(), subDomains.end(), label);
if (pos == subDomains.end() || pos->label != label) {
// A new, previously unknown host.
if (expired_policy(expires)) {
// Nothing to do at all - we did not know this host previously,
// we do not have to - since its policy expired.
return;
}
pos = subDomains.insert(pos, label);
domainIndex = children.size();
pos->domainIndex = domainIndex;
children.resize(children.size() + (e - i));
for (int j = i + 1; j < e; ++j) {
auto &newDomain = children[domainIndex];
newDomain.labels.push_back(labels[j]);
newDomain.labels.back().domainIndex = ++domainIndex;
}
break;
}
domainIndex = pos->domainIndex;
}
Q_ASSERT(domainIndex > 0 && domainIndex < children.size());
children[domainIndex].setHostPolicy(expires, includeSubDomains);
}
bool QHstsCache::isKnownHost(const QUrl &originalUrl) const
{
if (!has_valid_domain_name(originalUrl))
return false;
QUrl url;
url.setHost(originalUrl.host());
QStringList labels(url.host().split(QLatin1Char('.')));
std::reverse(labels.begin(), labels.end());
Q_ASSERT(children.size());
size_type domainIndex = 0;
for (int i = 0, e = labels.size(); i < e; ++i) {
Q_ASSERT(domainIndex < children.size());
const auto &subDomains = children[domainIndex].labels;
auto pos = std::lower_bound(subDomains.begin(), subDomains.end(), labels[i]);
if (pos == subDomains.end() || pos->label != labels[i])
return false;
Q_ASSERT(pos->domainIndex < children.size());
domainIndex = pos->domainIndex;
auto &domain = children[domainIndex];
if (domain.validateHostPolicy() && (i + 1 == e || domain.includeSubDomains)) {
/*
RFC6797, 8.2. Known HSTS Host Domain Name Matching
* Superdomain Match
If a label-for-label match between an entire Known HSTS Host's
domain name and a right-hand portion of the given domain name
is found, then this Known HSTS Host's domain name is a
superdomain match for the given domain name. There could be
multiple superdomain matches for a given domain name.
* Congruent Match
If a label-for-label match between a Known HSTS Host's domain
name and the given domain name is found -- i.e., there are no
further labels to compare -- then the given domain name
congruently matches this Known HSTS Host.
*/
return true;
}
}
return false;
}
void QHstsCache::clear()
{
children.resize(1);
children[0].labels.clear();
// Top-level is never known:
Q_ASSERT(!children[0].isKnownHost);
}
// The parser is quite simple: 'nextToken' knowns exactly what kind of tokens
// are valid and it will return false if something else was found; then
// we immediately stop parsing. 'parseDirective' knows how these tokens can
// be combined into a valid directive and if some weird combination of
// valid tokens is found - we immediately stop.
// And finally we call parseDirective again and again until some error found or
// we have no more bytes in the header.
// The following isXXX functions are based on RFC2616, 2.2 Basic Rules.
static bool isCHAR(int c)
{
// CHAR = <any US-ASCII character (octets 0 - 127)>
return c >= 0 && c <= 127;
}
static bool isCTL(int c)
{
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
return (c >= 0 && c <= 31) || c == 127;
}
static bool isLWS(int c)
{
// LWS = [CRLF] 1*( SP | HT )
//
// CRLF = CR LF
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
//
// CRLF is handled by the time we parse a header (they were replaced with
// spaces). We only have to deal with remaining SP|HT
return c == ' ' || c == '\t';
}
static bool isTEXT(char c)
{
// TEXT = <any OCTET except CTLs,
// but including LWS>
return !isCTL(c) || isLWS(c);
}
static bool isSeparator(char c)
{
// separators = "(" | ")" | "<" | ">" | "@"
// | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "="
// | "{" | "}" | SP | HT
static const char separators[] = "()<>@,;:\\\"/[]?={}";
static const char *end = separators + sizeof separators - 1;
return isLWS(c) || std::find(separators, end, c) != end;
}
static QByteArray unescapeMaxAge(const QByteArray &value)
{
if (value.size() < 2 || value[0] != '"')
return value;
Q_ASSERT(value[value.size() - 1] == '"');
return value.mid(1, value.size() - 2);
}
static bool isTOKEN(char c)
{
// token = 1*<any CHAR except CTLs or separators>
return isCHAR(c) && !isCTL(c) && !isSeparator(c);
}
/*
RFC6797, 6.1 Strict-Transport-Security HTTP Response Header Field.
Syntax:
Strict-Tranposrt-Security = "Strict-Transport-Security" ":"
[ directive ] *( ";" [ directive ] )
directive = directive-name [ "=" directive-value ]
directive-name = token
directive-value = token | quoted-string
RFC 2616, 2.2 Basic Rules.
token = 1*<any CHAR except CTLs or separators>
quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
qdtext = <any TEXT except <">>
quoted-pair = "\" CHAR
*/
bool QHstsHeaderParser::parse(const QList<QPair<QByteArray, QByteArray>> &headers)
{
for (const auto &h : headers) {
// We use '==' since header name was already 'trimmed' for us:
if (h.first == "Strict-Transport-Security") {
header = h.second;
// RFC6797, 8.1:
//
// The UA MUST ignore any STS header fields not conforming to the
// grammar specified in Section 6.1 ("Strict-Transport-Security HTTP
// Response Header Field").
//
// If a UA receives more than one STS header field in an HTTP
// response message over secure transport, then the UA MUST process
// only the first such header field.
//
// We read this as: ignore all invalid headers and take the first valid:
if (parseSTSHeader() && maxAgeFound) {
expiry = QDateTime::currentDateTimeUtc().addSecs(maxAge);
return true;
}
}
}
// In case it was set by a syntactically correct header (but without
// REQUIRED max-age directive):
subDomainsFound = false;
return false;
}
bool QHstsHeaderParser::parseSTSHeader()
{
expiry = QDateTime();
maxAgeFound = false;
subDomainsFound = false;
maxAge = 0;
tokenPos = 0;
token.clear();
while (tokenPos < header.size()) {
if (!parseDirective())
return false;
if (token.size() && token != ";") {
// After a directive we can only have a ";" or no more tokens.
// Invalid syntax.
return false;
}
}
return true;
}
bool QHstsHeaderParser::parseDirective()
{
// RFC 6797, 6.1:
//
// directive = directive-name [ "=" directive-value ]
// directive-name = token
// directive-value = token | quoted-string
// RFC 2616, 2.2:
//
// token = 1*<any CHAR except CTLs or separators>
if (!nextToken())
return false;
if (!token.size()) // No more data, but no error.
return true;
if (token == ";") // That's a weird grammar, but that's what it is.
return true;
if (!isTOKEN(token[0])) // Not a valid directive-name.
return false;
const QByteArray directiveName = token;
// 2. Try to read "=" or ";".
if (!nextToken())
return false;
QByteArray directiveValue;
if (token == ";") // No directive-value
return processDirective(directiveName, directiveValue);
if (token == "=") {
// We expect a directive-value now:
if (!nextToken() || !token.size())
return false;
directiveValue = token;
} else if (token.size()) {
// Invalid syntax:
return false;
}
if (!processDirective(directiveName, directiveValue))
return false;
// Read either ";", or 'end of header', or some invalid token.
return nextToken();
}
bool QHstsHeaderParser::processDirective(const QByteArray &name, const QByteArray &value)
{
Q_ASSERT(name.size());
// RFC6797 6.1/3 Directive names are case-insensitive
const auto lcName = name.toLower();
if (lcName == "max-age") {
// RFC 6797, 6.1.1
// The syntax of the max-age directive's REQUIRED value (after
// quoted-string unescaping, if necessary) is defined as:
//
// max-age-value = delta-seconds
if (maxAgeFound) {
// RFC 6797, 6.1/2:
// All directives MUST appear only once in an STS header field.
return false;
}
const QByteArray unescapedValue = unescapeMaxAge(value);
if (!unescapedValue.size())
return false;
bool ok = false;
const qint64 age = unescapedValue.toLongLong(&ok);
if (!ok || age < 0)
return false;
maxAge = age;
maxAgeFound = true;
} else if (lcName == "includesubdomains") {
// RFC 6797, 6.1.2. The includeSubDomains Directive.
// The OPTIONAL "includeSubDomains" directive is a valueless directive.
if (subDomainsFound) {
// RFC 6797, 6.1/2:
// All directives MUST appear only once in an STS header field.
return false;
}
subDomainsFound = true;
} // else we do nothing, skip unknown directives (RFC 6797, 6.1/5)
return true;
}
bool QHstsHeaderParser::nextToken()
{
// Returns true if we found a valid token or we have no more data (token is
// empty then).
token.clear();
// Fortunately enough, by this point qhttpnetworkreply already got rid of
// [CRLF] parts, but we can have 1*(SP|HT) yet.
while (tokenPos < header.size() && isLWS(header[tokenPos]))
++tokenPos;
if (tokenPos == header.size())
return true;
const char ch = header[tokenPos];
if (ch == ';' || ch == '=') {
token.append(ch);
++tokenPos;
return true;
}
// RFC 2616, 2.2.
//
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
// qdtext = <any TEXT except <">>
if (ch == '"') {
int last = tokenPos + 1;
while (last < header.size()) {
if (header[last] == '"') {
// The end of a quoted-string.
break;
} else if (header[last] == '\\') {
// quoted-pair = "\" CHAR
if (last + 1 < header.size() && isCHAR(header[last + 1]))
last += 2;
else
return false;
} else {
if (!isTEXT(header[last]))
return false;
++last;
}
}
if (last >= header.size()) // no closing '"':
return false;
token = header.mid(tokenPos, last - tokenPos + 1);
tokenPos = last + 1;
return true;
}
// RFC 2616, 2.2:
//
// token = 1*<any CHAR except CTLs or separators>
if (!isTOKEN(ch))
return false;
int last = tokenPos + 1;
while (last < header.size() && isTOKEN(header[last]))
++last;
token = header.mid(tokenPos, last - tokenPos);
tokenPos = last;
return true;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,169 @@
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtNetwork module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QHSTS_P_H
#define QHSTS_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists for the convenience
// of the Network Access API. This header file may change from
// version to version without notice, or even be removed.
//
// We mean it.
//
#include <QtCore/qbytearray.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qstring.h>
#include <QtCore/qglobal.h>
#include <QtCore/qvector.h>
#include <QtCore/qlist.h>
#include <QtCore/qpair.h>
#include <QtCore/qurl.h>
#include <algorithm>
#include <vector>
QT_BEGIN_NAMESPACE
class Q_AUTOTEST_EXPORT QHstsCache
{
public:
QHstsCache();
void updateFromHeaders(const QList<QPair<QByteArray, QByteArray>> &headers,
const QUrl &url);
void updateKnownHost(const QUrl &url, const QDateTime &expires,
bool includeSubDomains);
bool isKnownHost(const QUrl &url) const;
void clear();
private:
using size_type = std::vector<int>::size_type;
struct DomainLabel
{
DomainLabel() = default;
DomainLabel(const QString &name) : label(name) { }
bool operator < (const DomainLabel &rhs) const
{ return label < rhs.label; }
QString label;
size_type domainIndex;
};
struct Domain
{
void setHostPolicy(const QDateTime &expiration, bool subs)
{
expirationTime = expiration;
isKnownHost = expirationTime.isValid()
&& expirationTime > QDateTime::currentDateTimeUtc();
includeSubDomains = subs;
}
bool validateHostPolicy()
{
if (!isKnownHost)
return false;
if (expirationTime > QDateTime::currentDateTimeUtc())
return true;
isKnownHost = false;
includeSubDomains = false;
return false;
}
bool isKnownHost = false;
bool includeSubDomains = false;
QDateTime expirationTime;
std::vector<DomainLabel> labels;
};
/*
Each Domain represents a DNS name or prefix thereof; each entry in its
std::vector<DomainLabel> labels pairs the next fragment of a DNS name
with the index into 'children' at which to find another Domain object.
The root Domain, children[0], has top-level-domain DomainLabel entries,
such as "com", "org" and "net"; the entry in 'children' at the index it
pairs with "com" is the Domain entry for .com; if that has "example" in
its labels, it'll be paired with the index of the entry in 'children'
that represents example.com; from which, in turn, we can find the
Domain object for www.example.com, and so on.
*/
mutable std::vector<Domain> children;
};
class Q_AUTOTEST_EXPORT QHstsHeaderParser
{
public:
bool parse(const QList<QPair<QByteArray, QByteArray>> &headers);
QDateTime expirationDate() const { return expiry; }
bool includeSubDomains() const { return subDomainsFound; }
private:
bool parseSTSHeader();
bool parseDirective();
bool processDirective(const QByteArray &name, const QByteArray &value);
bool nextToken();
QByteArray header;
QByteArray token;
QDateTime expiry;
int tokenPos = 0;
bool maxAgeFound = false;
qint64 maxAge = 0;
bool subDomainsFound = false;
};
QT_END_NAMESPACE
#endif

View File

@ -692,6 +692,55 @@ void QNetworkAccessManager::setCookieJar(QNetworkCookieJar *cookieJar)
}
}
/*!
\since 5.9
Enables HTTP Strict Transport Security (HSTS, RFC6797). When processing a
request, QNetworkAccessManager automatically replaces "http" scheme with
"https" and uses a secure transport if a host is a known HSTS host.
Port 80 if it's set explicitly is replaced by port 443.
When HSTS is enabled, for each HTTP response containing HSTS header and
received over a secure transport, QNetworkAccessManager will update its HSTS
cache, either remembering a host with a valid policy or removing a host with
expired/disabled HSTS policy.
\sa disableStrictTransportSecurity(), strictTransportSecurityEnabled()
*/
void QNetworkAccessManager::enableStrictTransportSecurity()
{
Q_D(QNetworkAccessManager);
d->stsEnabled = true;
}
/*!
\since 5.9
Disables HTTP Strict Transport Security (HSTS). HSTS headers in responses would
be ignored, no scheme/port mapping is done.
\sa enableStrictTransportSecurity()
*/
void QNetworkAccessManager::disableStrictTransportSecurity()
{
Q_D(QNetworkAccessManager);
d->stsEnabled = false;
}
/*!
\since 5.9
Returns true if HTTP Strict Transport Security (HSTS) was enabled. By default
HSTS is disabled.
\sa enableStrictTransportSecurity
*/
bool QNetworkAccessManager::strictTransportSecurityEnabled() const
{
Q_D(const QNetworkAccessManager);
return d->stsEnabled;
}
/*!
Posts a request to obtain the network headers for \a request
and returns a new QNetworkReply object which will contain such headers.
@ -1299,6 +1348,24 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera
|| scheme == QLatin1String("https") || scheme == QLatin1String("preconnect-https")
#endif
) {
#ifndef QT_NO_SSL
if (strictTransportSecurityEnabled() && d->stsCache.isKnownHost(request.url())) {
QUrl stsUrl(request.url());
// RFC6797, 8.3:
// The UA MUST replace the URI scheme with "https" [RFC2818],
// and if the URI contains an explicit port component of "80",
// then the UA MUST convert the port component to be "443", or
// if the URI contains an explicit port component that is not
// equal to "80", the port component value MUST be preserved;
// otherwise,
// if the URI does not contain an explicit port component, the UA
// MUST NOT add one.
if (stsUrl.port() == 80)
stsUrl.setPort(443);
stsUrl.setScheme(QLatin1String("https"));
request.setUrl(stsUrl);
}
#endif
QNetworkReplyHttpImpl *reply = new QNetworkReplyHttpImpl(this, request, op, outgoingData);
#ifndef QT_NO_BEARERMANAGEMENT
connect(this, SIGNAL(networkSessionConnected()),

View File

@ -120,6 +120,10 @@ public:
QNetworkCookieJar *cookieJar() const;
void setCookieJar(QNetworkCookieJar *cookieJar);
void enableStrictTransportSecurity();
void disableStrictTransportSecurity();
bool strictTransportSecurityEnabled() const;
QNetworkReply *head(const QNetworkRequest &request);
QNetworkReply *get(const QNetworkRequest &request);
QNetworkReply *post(const QNetworkRequest &request, QIODevice *data);

View File

@ -56,6 +56,7 @@
#include "qnetworkaccesscache_p.h"
#include "qnetworkaccessbackend_p.h"
#include "qnetworkrequest.h"
#include "qhsts_p.h"
#include "private/qobject_p.h"
#include "QtNetwork/qnetworkproxy.h"
#include "QtNetwork/qnetworksession.h"
@ -205,8 +206,13 @@ public:
QNetworkAccessCache objectCache;
static inline QNetworkAccessCache *getObjectCache(QNetworkAccessBackend *backend)
{ return &backend->manager->objectCache; }
Q_AUTOTEST_EXPORT static void clearAuthenticationCache(QNetworkAccessManager *manager);
Q_AUTOTEST_EXPORT static void clearConnectionCache(QNetworkAccessManager *manager);
QHstsCache stsCache;
bool stsEnabled = false;
#ifndef QT_NO_BEARERMANAGEMENT
Q_AUTOTEST_EXPORT static const QWeakPointer<const QNetworkSession> getNetworkSession(const QNetworkAccessManager *manager);
#endif

View File

@ -732,7 +732,11 @@ void QNetworkReply::setSslConfiguration(const QSslConfiguration &config)
You can clear the list of errors you want to ignore by calling this
function with an empty list.
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors()
\note If HTTP Strict Transport Security is enabled for QNetworkAccessManager,
this function has no effect.
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors(),
QNetworkAccessManager::enableStrictTransportSecurity()
*/
void QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
{
@ -799,6 +803,9 @@ void QNetworkReply::ignoreSslErrorsImplementation(const QList<QSslError> &)
sslErrors() signal, which indicates which errors were
found.
\note If HTTP Strict Transport Security is enabled for QNetworkAccessManager,
this function has no effect.
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors()
*/
void QNetworkReply::ignoreSslErrors()

View File

@ -52,6 +52,7 @@
#include "QtCore/qelapsedtimer.h"
#include "QtNetwork/qsslconfiguration.h"
#include "qhttpthreaddelegate_p.h"
#include "qhsts_p.h"
#include "qthread.h"
#include "QtCore/qcoreapplication.h"
@ -384,6 +385,12 @@ void QNetworkReplyHttpImpl::ignoreSslErrors()
{
Q_D(QNetworkReplyHttpImpl);
if (d->managerPrivate && d->managerPrivate->stsEnabled
&& d->managerPrivate->stsCache.isKnownHost(url())) {
// We cannot ignore any Security Transport-related errors for this host.
return;
}
d->pendingIgnoreAllSslErrors = true;
}
@ -391,6 +398,12 @@ void QNetworkReplyHttpImpl::ignoreSslErrorsImplementation(const QList<QSslError>
{
Q_D(QNetworkReplyHttpImpl);
if (d->managerPrivate && d->managerPrivate->stsEnabled
&& d->managerPrivate->stsCache.isKnownHost(url())) {
// We cannot ignore any Security Transport-related errors for this host.
return;
}
// the pending list is set if QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
// is called before QNetworkAccessManager::get() (or post(), etc.)
d->pendingIgnoreSslErrorsList = errors;
@ -1179,6 +1192,15 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QList<QPair<QByte
statusCode = sc;
reasonPhrase = rp;
#ifndef QT_NO_SSL
// We parse this header only if we're using secure transport:
//
// RFC6797, 8.1
// If an HTTP response is received over insecure transport, the UA MUST
// ignore any present STS header field(s).
if (url.scheme() == QLatin1String("https") && managerPrivate->stsEnabled)
managerPrivate->stsCache.updateFromHeaders(hm, url);
#endif
// Download buffer
if (!db.isNull()) {
downloadBufferPointer = db;

View File

@ -13,11 +13,13 @@ SUBDIRS=\
qhttpnetworkreply \
qabstractnetworkcache \
hpack \
http2
http2 \
hsts
!qtConfig(private_tests): SUBDIRS -= \
qhttpnetworkconnection \
qhttpnetworkreply \
qftp \
hpack \
http2
http2 \
hsts

View File

@ -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

View File

@ -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 <QtTest/QtTest>
#include <QtCore/qdatetime.h>
#include <QtCore/qvector.h>
#include <QtCore/qpair.h>
#include <QtCore/qurl.h>
#include <QtNetwork/private/qhsts_p.h>
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<QUrl>("knownHost");
QTest::addColumn<QDateTime>("policyExpires");
QTest::addColumn<bool>("includeSubDomains");
QTest::addColumn<QUrl>("hostToTest");
QTest::addColumn<bool>("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<QByteArray, QByteArray>;
using Headers = QList<Header>;
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"