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.h \
access/qhttpmultipart_p.h \ access/qhttpmultipart_p.h \
access/qnetworkfile_p.h \ access/qnetworkfile_p.h \
access/qhttp2protocolhandler_p.h access/qhttp2protocolhandler_p.h \
access/qhsts_p.h
SOURCES += \ SOURCES += \
access/qftp.cpp \ access/qftp.cpp \
@ -72,7 +73,8 @@ SOURCES += \
access/qhttpthreaddelegate.cpp \ access/qhttpthreaddelegate.cpp \
access/qhttpmultipart.cpp \ access/qhttpmultipart.cpp \
access/qnetworkfile.cpp \ access/qnetworkfile.cpp \
access/qhttp2protocolhandler.cpp access/qhttp2protocolhandler.cpp \
access/qhsts.cpp
mac: LIBS_PRIVATE += -framework Security 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 Posts a request to obtain the network headers for \a request
and returns a new QNetworkReply object which will contain such headers. 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") || scheme == QLatin1String("https") || scheme == QLatin1String("preconnect-https")
#endif #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); QNetworkReplyHttpImpl *reply = new QNetworkReplyHttpImpl(this, request, op, outgoingData);
#ifndef QT_NO_BEARERMANAGEMENT #ifndef QT_NO_BEARERMANAGEMENT
connect(this, SIGNAL(networkSessionConnected()), connect(this, SIGNAL(networkSessionConnected()),

View File

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

View File

@ -56,6 +56,7 @@
#include "qnetworkaccesscache_p.h" #include "qnetworkaccesscache_p.h"
#include "qnetworkaccessbackend_p.h" #include "qnetworkaccessbackend_p.h"
#include "qnetworkrequest.h" #include "qnetworkrequest.h"
#include "qhsts_p.h"
#include "private/qobject_p.h" #include "private/qobject_p.h"
#include "QtNetwork/qnetworkproxy.h" #include "QtNetwork/qnetworkproxy.h"
#include "QtNetwork/qnetworksession.h" #include "QtNetwork/qnetworksession.h"
@ -205,8 +206,13 @@ public:
QNetworkAccessCache objectCache; QNetworkAccessCache objectCache;
static inline QNetworkAccessCache *getObjectCache(QNetworkAccessBackend *backend) static inline QNetworkAccessCache *getObjectCache(QNetworkAccessBackend *backend)
{ return &backend->manager->objectCache; } { return &backend->manager->objectCache; }
Q_AUTOTEST_EXPORT static void clearAuthenticationCache(QNetworkAccessManager *manager); Q_AUTOTEST_EXPORT static void clearAuthenticationCache(QNetworkAccessManager *manager);
Q_AUTOTEST_EXPORT static void clearConnectionCache(QNetworkAccessManager *manager); Q_AUTOTEST_EXPORT static void clearConnectionCache(QNetworkAccessManager *manager);
QHstsCache stsCache;
bool stsEnabled = false;
#ifndef QT_NO_BEARERMANAGEMENT #ifndef QT_NO_BEARERMANAGEMENT
Q_AUTOTEST_EXPORT static const QWeakPointer<const QNetworkSession> getNetworkSession(const QNetworkAccessManager *manager); Q_AUTOTEST_EXPORT static const QWeakPointer<const QNetworkSession> getNetworkSession(const QNetworkAccessManager *manager);
#endif #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 You can clear the list of errors you want to ignore by calling this
function with an empty list. 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) void QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
{ {
@ -799,6 +803,9 @@ void QNetworkReply::ignoreSslErrorsImplementation(const QList<QSslError> &)
sslErrors() signal, which indicates which errors were sslErrors() signal, which indicates which errors were
found. found.
\note If HTTP Strict Transport Security is enabled for QNetworkAccessManager,
this function has no effect.
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors() \sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors()
*/ */
void QNetworkReply::ignoreSslErrors() void QNetworkReply::ignoreSslErrors()

View File

@ -52,6 +52,7 @@
#include "QtCore/qelapsedtimer.h" #include "QtCore/qelapsedtimer.h"
#include "QtNetwork/qsslconfiguration.h" #include "QtNetwork/qsslconfiguration.h"
#include "qhttpthreaddelegate_p.h" #include "qhttpthreaddelegate_p.h"
#include "qhsts_p.h"
#include "qthread.h" #include "qthread.h"
#include "QtCore/qcoreapplication.h" #include "QtCore/qcoreapplication.h"
@ -384,6 +385,12 @@ void QNetworkReplyHttpImpl::ignoreSslErrors()
{ {
Q_D(QNetworkReplyHttpImpl); 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; d->pendingIgnoreAllSslErrors = true;
} }
@ -391,6 +398,12 @@ void QNetworkReplyHttpImpl::ignoreSslErrorsImplementation(const QList<QSslError>
{ {
Q_D(QNetworkReplyHttpImpl); 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) // the pending list is set if QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
// is called before QNetworkAccessManager::get() (or post(), etc.) // is called before QNetworkAccessManager::get() (or post(), etc.)
d->pendingIgnoreSslErrorsList = errors; d->pendingIgnoreSslErrorsList = errors;
@ -1179,6 +1192,15 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QList<QPair<QByte
statusCode = sc; statusCode = sc;
reasonPhrase = rp; 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 // Download buffer
if (!db.isNull()) { if (!db.isNull()) {
downloadBufferPointer = db; downloadBufferPointer = db;

View File

@ -13,11 +13,13 @@ SUBDIRS=\
qhttpnetworkreply \ qhttpnetworkreply \
qabstractnetworkcache \ qabstractnetworkcache \
hpack \ hpack \
http2 http2 \
hsts
!qtConfig(private_tests): SUBDIRS -= \ !qtConfig(private_tests): SUBDIRS -= \
qhttpnetworkconnection \ qhttpnetworkconnection \
qhttpnetworkreply \ qhttpnetworkreply \
qftp \ qftp \
hpack \ 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"