Introduce QHstsStore - the permanent store for HSTS policies

The store is using QSettings under the hood. A user can enable/disable
storing HSTS policies (via QNAM's setter method) and we take care of
the rest - filling QHstsCache from the store, writing updated/observed
targets, removing expired policies.

Change-Id: I26e4a98761ddfe5005fedd18be56a6303fe7b35a
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Timur Pocheptsov 2017-07-12 12:52:06 +02:00
parent 37dc5bb46c
commit 72cf2339ed
9 changed files with 483 additions and 7 deletions

View File

@ -41,7 +41,8 @@ HEADERS += \
access/qnetworkfile_p.h \
access/qhttp2protocolhandler_p.h \
access/qhsts_p.h \
access/qhstspolicy.h
access/qhstspolicy.h \
access/qhstsstore_p.h
SOURCES += \
access/qftp.cpp \
@ -76,7 +77,8 @@ SOURCES += \
access/qnetworkfile.cpp \
access/qhttp2protocolhandler.cpp \
access/qhsts.cpp \
access/qhstspolicy.cpp
access/qhstspolicy.cpp \
access/qhstsstore.cpp
mac: LIBS_PRIVATE += -framework Security

View File

@ -37,6 +37,7 @@
**
****************************************************************************/
#include "qhstsstore_p.h"
#include "qhsts_p.h"
#include "QtCore/private/qipaddress_p.h"
@ -80,14 +81,24 @@ void QHstsCache::updateFromHeaders(const QList<QPair<QByteArray, QByteArray>> &h
return;
QHstsHeaderParser parser;
if (parser.parse(headers))
if (parser.parse(headers)) {
updateKnownHost(url.host(), parser.expirationDate(), parser.includeSubDomains());
if (hstsStore)
hstsStore->synchronize();
}
}
void QHstsCache::updateFromPolicies(const QVector<QHstsPolicy> &policies)
{
for (const auto &policy : policies)
updateKnownHost(policy.host(), policy.expiry(), policy.includesSubDomains());
if (hstsStore && policies.size()) {
// These policies are coming either from store or from QNAM's setter
// function. As a result we can notice expired or new policies, time
// to sync ...
hstsStore->synchronize();
}
}
void QHstsCache::updateKnownHost(const QUrl &url, const QDateTime &expires,
@ -97,6 +108,8 @@ void QHstsCache::updateKnownHost(const QUrl &url, const QDateTime &expires,
return;
updateKnownHost(url.host(), expires, includeSubDomains);
if (hstsStore)
hstsStore->synchronize();
}
void QHstsCache::updateKnownHost(const QString &host, const QDateTime &expires,
@ -124,13 +137,20 @@ void QHstsCache::updateKnownHost(const QString &host, const QDateTime &expires,
}
knownHosts.insert(pos, hostName, newPolicy);
if (hstsStore)
hstsStore->addToObserved(newPolicy);
return;
}
if (newPolicy.isExpired())
knownHosts.erase(pos);
else
else if (*pos != newPolicy)
*pos = std::move(newPolicy);
else
return;
if (hstsStore)
hstsStore->addToObserved(newPolicy);
}
bool QHstsCache::isKnownHost(const QUrl &url) const
@ -165,10 +185,15 @@ bool QHstsCache::isKnownHost(const QUrl &url) const
while (nameToTest.fragment.size()) {
auto const pos = knownHosts.find(nameToTest);
if (pos != knownHosts.end()) {
if (pos.value().isExpired())
if (pos.value().isExpired()) {
knownHosts.erase(pos);
else if (!superDomainMatch || pos.value().includesSubDomains())
if (hstsStore) {
// Inform our store that this policy has expired.
hstsStore->addToObserved(pos.value());
}
} else if (!superDomainMatch || pos.value().includesSubDomains()) {
return true;
}
}
const int dot = nameToTest.fragment.indexOf(QLatin1Char('.'));
@ -196,6 +221,34 @@ QVector<QHstsPolicy> QHstsCache::policies() const
return values;
}
void QHstsCache::setStore(QHstsStore *store)
{
// Caller retains ownership of store, which must outlive this cache.
if (store != hstsStore) {
hstsStore = store;
if (!hstsStore)
return;
// First we augment our store with the policies we already know about
// (and thus the cached policy takes priority over whatever policy we
// had in the store for the same host, if any).
if (knownHosts.size()) {
const QVector<QHstsPolicy> observed(policies());
for (const auto &policy : observed)
hstsStore->addToObserved(policy);
hstsStore->synchronize();
}
// Now we update the cache with anything we have not observed yet, but
// the store knows about (well, it can happen we synchronize again as a
// result if some policies managed to expire or if we add a new one
// from the store to cache):
const QVector<QHstsPolicy> restored(store->readPolicies());
updateFromPolicies(restored);
}
}
// 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

View File

@ -51,6 +51,8 @@
// We mean it.
//
#include <QtNetwork/private/qtnetworkglobal_p.h>
#include <QtNetwork/qhstspolicy.h>
#include <QtCore/qbytearray.h>
@ -66,6 +68,8 @@ QT_BEGIN_NAMESPACE
template<typename T> class QList;
template <typename T> class QVector;
class QHstsStore;
class Q_AUTOTEST_EXPORT QHstsCache
{
public:
@ -80,6 +84,8 @@ public:
QVector<QHstsPolicy> policies() const;
void setStore(QHstsStore *store);
private:
void updateKnownHost(const QString &hostName, const QDateTime &expires,
@ -112,6 +118,7 @@ private:
};
mutable QMap<HostName, QHstsPolicy> knownHosts;
QHstsStore *hstsStore = nullptr;
};
class Q_AUTOTEST_EXPORT QHstsHeaderParser

View File

@ -0,0 +1,202 @@
/****************************************************************************
**
** 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 "qhstsstore_p.h"
#include "qhstspolicy.h"
#include "qstandardpaths.h"
#include "qdatastream.h"
#include "qbytearray.h"
#include "qdatetime.h"
#include "qvariant.h"
#include "qstring.h"
#include "qdir.h"
#include <utility>
QT_BEGIN_NAMESPACE
static QString host_name_to_settings_key(const QString &hostName)
{
const QByteArray hostNameAsHex(hostName.toUtf8().toHex());
return QString::fromLatin1(hostNameAsHex);
}
static QString settings_key_to_host_name(const QString &key)
{
const QByteArray hostNameAsUtf8(QByteArray::fromHex(key.toLatin1()));
return QString::fromUtf8(hostNameAsUtf8);
}
QHstsStore::QHstsStore(const QString &dirName)
: store(absoluteFilePath(dirName), QSettings::IniFormat)
{
// Disable fallbacks, we do not want to use anything but our own ini file.
store.setFallbacksEnabled(false);
}
QHstsStore::~QHstsStore()
{
synchronize();
}
QVector<QHstsPolicy> QHstsStore::readPolicies()
{
// This function only attempts to read policies, making no decision about
// expired policies. It's up to a user (QHstsCache) to mark these policies
// for deletion and sync the store later. But we immediately remove keys/values
// (if the store isWritable) for the policies that we fail to read.
QVector<QHstsPolicy> policies;
beginHstsGroups();
const QStringList keys = store.childKeys();
for (const auto &key : keys) {
QHstsPolicy restoredPolicy;
if (deserializePolicy(key, restoredPolicy)) {
restoredPolicy.setHost(settings_key_to_host_name(key));
policies.push_back(std::move(restoredPolicy));
} else if (isWritable()) {
evictPolicy(key);
}
}
endHstsGroups();
return policies;
}
void QHstsStore::addToObserved(const QHstsPolicy &policy)
{
observedPolicies.push_back(policy);
}
void QHstsStore::synchronize()
{
if (!isWritable())
return;
if (observedPolicies.size()) {
beginHstsGroups();
for (const QHstsPolicy &policy : observedPolicies) {
const QString key(host_name_to_settings_key(policy.host()));
// If we fail to write a new, updated policy, we also remove the old one.
if (policy.isExpired() || !serializePolicy(key, policy))
evictPolicy(key);
}
observedPolicies.clear();
endHstsGroups();
}
store.sync();
}
bool QHstsStore::isWritable() const
{
return store.isWritable();
}
QString QHstsStore::absoluteFilePath(const QString &dirName)
{
const QDir dir(dirName.isEmpty() ? QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
: dirName);
return dir.absoluteFilePath(QLatin1String("hstsstore"));
}
void QHstsStore::beginHstsGroups()
{
store.beginGroup(QLatin1String("StrictTransportSecurity"));
store.beginGroup(QLatin1String("Policies"));
}
void QHstsStore::endHstsGroups()
{
store.endGroup();
store.endGroup();
}
bool QHstsStore::deserializePolicy(const QString &key, QHstsPolicy &policy)
{
Q_ASSERT(store.contains(key));
const QVariant data(store.value(key));
if (data.isNull() || !data.canConvert<QByteArray>())
return false;
const QByteArray serializedData(data.toByteArray());
QDataStream streamer(serializedData);
qint64 expiryInMS = 0;
streamer >> expiryInMS;
if (streamer.status() != QDataStream::Ok)
return false;
bool includesSubDomains = false;
streamer >> includesSubDomains;
if (streamer.status() != QDataStream::Ok)
return false;
policy.setExpiry(QDateTime::fromMSecsSinceEpoch(expiryInMS));
policy.setIncludesSubDomains(includesSubDomains);
return true;
}
bool QHstsStore::serializePolicy(const QString &key, const QHstsPolicy &policy)
{
Q_ASSERT(store.isWritable());
QByteArray serializedData;
QDataStream streamer(&serializedData, QIODevice::WriteOnly);
streamer << policy.expiry().toMSecsSinceEpoch();
streamer << policy.includesSubDomains();
if (streamer.status() != QDataStream::Ok)
return false;
store.setValue(key, serializedData);
return true;
}
void QHstsStore::evictPolicy(const QString &key)
{
Q_ASSERT(store.isWritable());
if (store.contains(key))
store.remove(key);
}
QT_END_NAMESPACE

View File

@ -0,0 +1,93 @@
/****************************************************************************
**
** 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 QHSTSSTORE_P_H
#define QHSTSSTORE_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 <QtNetwork/private/qtnetworkglobal_p.h>
#include <QtCore/qsettings.h>
#include <QtCore/qvector.h>
QT_BEGIN_NAMESPACE
class QHstsPolicy;
class QByteArray;
class QString;
class Q_AUTOTEST_EXPORT QHstsStore
{
public:
explicit QHstsStore(const QString &dirName);
~QHstsStore();
QVector<QHstsPolicy> readPolicies();
void addToObserved(const QHstsPolicy &policy);
void synchronize();
bool isWritable() const;
static QString absoluteFilePath(const QString &dirName);
private:
void beginHstsGroups();
bool serializePolicy(const QString &key, const QHstsPolicy &policy);
bool deserializePolicy(const QString &key, QHstsPolicy &policy);
void evictPolicy(const QString &key);
void endHstsGroups();
QVector<QHstsPolicy> observedPolicies;
QSettings store;
Q_DISABLE_COPY(QHstsStore)
};
QT_END_NAMESPACE
#endif // QHSTSSTORE_P_H

View File

@ -729,6 +729,48 @@ bool QNetworkAccessManager::isStrictTransportSecurityEnabled() const
return d->stsEnabled;
}
/*!
\since 5.10
If \a enabled is \c true, the internal HSTS cache will use a persistent store
to read and write HSTS policies. \a storeDir defines where this store will be
located. The default location is defined by QStandardPaths::CacheLocation.
If there is no writable QStandartPaths::CacheLocation and \a storeDir is an
empty string, the store will be located in the program's working directory.
\note If HSTS cache already contains HSTS policies by the time persistent
store is enabled, these policies will be preserved in the store. In case both
cache and store contain the same known hosts, policies from cache are considered
to be more up-to-date (and thus will overwrite the previous values in the store).
If this behavior is undesired, enable HSTS store before enabling Strict Tranport
Security. By default, the persistent store of HSTS policies is disabled.
\sa isStrictTransportSecurityStoreEnabled(), setStrictTransportSecurityEnabled(),
QStandardPaths::standardLocations()
*/
void QNetworkAccessManager::enableStrictTransportSecurityStore(bool enabled, const QString &storeDir)
{
Q_D(QNetworkAccessManager);
d->stsStore.reset(enabled ? new QHstsStore(storeDir) : nullptr);
d->stsCache.setStore(d->stsStore.data());
}
/*!
\since 5.10
Returns true if HSTS cache uses a permanent store to load and store HSTS
policies.
\sa enableStrictTransportSecurityStore()
*/
bool QNetworkAccessManager::isStrictTransportSecurityStoreEnabled() const
{
Q_D(const QNetworkAccessManager);
return bool(d->stsStore.data());
}
/*!
\since 5.9
@ -744,7 +786,7 @@ bool QNetworkAccessManager::isStrictTransportSecurityEnabled() const
policies, but this information can be overridden by "Strict-Transport-Security"
response headers.
\sa addStrictTransportSecurityHosts(), QHstsPolicy
\sa addStrictTransportSecurityHosts(), enableStrictTransportSecurityStore(), QHstsPolicy
*/
void QNetworkAccessManager::addStrictTransportSecurityHosts(const QVector<QHstsPolicy> &knownHosts)

View File

@ -42,6 +42,7 @@
#include <QtNetwork/qtnetworkglobal.h>
#include <QtNetwork/qnetworkrequest.h>
#include <QtCore/QString>
#include <QtCore/QVector>
#include <QtCore/QObject>
#ifndef QT_NO_SSL
@ -124,6 +125,8 @@ public:
void setStrictTransportSecurityEnabled(bool enabled);
bool isStrictTransportSecurityEnabled() const;
void enableStrictTransportSecurityStore(bool enabled, const QString &storeDir = QString());
bool isStrictTransportSecurityStoreEnabled() const;
void addStrictTransportSecurityHosts(const QVector<QHstsPolicy> &knownHosts);
QVector<QHstsPolicy> strictTransportSecurityHosts() const;

View File

@ -56,6 +56,7 @@
#include "qnetworkaccesscache_p.h"
#include "qnetworkaccessbackend_p.h"
#include "qnetworkrequest.h"
#include "qhstsstore_p.h"
#include "qhsts_p.h"
#include "private/qobject_p.h"
#include "QtNetwork/qnetworkproxy.h"
@ -211,6 +212,7 @@ public:
Q_AUTOTEST_EXPORT static void clearConnectionCache(QNetworkAccessManager *manager);
QHstsCache stsCache;
QScopedPointer<QHstsStore> stsStore;
bool stsEnabled = false;
#ifndef QT_NO_BEARERMANAGEMENT

View File

@ -32,7 +32,9 @@
#include <QtCore/qvector.h>
#include <QtCore/qpair.h>
#include <QtCore/qurl.h>
#include <QtCore/qdir.h>
#include <QtNetwork/private/qhstsstore_p.h>
#include <QtNetwork/private/qhsts_p.h>
QT_USE_NAMESPACE
@ -46,6 +48,7 @@ private Q_SLOTS:
void testMultilpeKnownHosts();
void testPolicyExpiration();
void testSTSHeaderParser();
void testStore();
};
void tst_QHsts::testSingleKnownHost_data()
@ -313,6 +316,75 @@ void tst_QHsts::testSTSHeaderParser()
QVERIFY(!parser.expirationDate().isValid());
}
const QLatin1String storeDir(".");
struct TestStoreDeleter
{
~TestStoreDeleter()
{
QDir cwd;
if (!cwd.remove(QHstsStore::absoluteFilePath(storeDir)))
qWarning() << "tst_QHsts::testStore: failed to remove the hsts store file";
}
};
void tst_QHsts::testStore()
{
// Delete the store's file after we finish the test.
TestStoreDeleter cleaner;
const QUrl exampleCom(QStringLiteral("http://example.com"));
const QUrl subDomain(QStringLiteral("http://subdomain.example.com"));
const QDateTime validDate(QDateTime::currentDateTimeUtc().addDays(1));
{
// We start from an empty cache and empty store:
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(!cache.isKnownHost(exampleCom));
QVERIFY(!cache.isKnownHost(subDomain));
// (1) This will also store the policy:
cache.updateKnownHost(exampleCom, validDate, true);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subDomain));
}
{
// Test the policy stored at (1):
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subDomain));
// (2) Remove subdomains:
cache.updateKnownHost(exampleCom, validDate, false);
QVERIFY(!cache.isKnownHost(subDomain));
}
{
// Test the previous update (2):
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(!cache.isKnownHost(subDomain));
}
{
QHstsCache cache;
cache.updateKnownHost(subDomain, validDate, false);
QVERIFY(cache.isKnownHost(subDomain));
QHstsStore store(storeDir);
// (3) This should store policy from cache, over old policy from store:
cache.setStore(&store);
}
{
// Test that (3) was stored:
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(cache.isKnownHost(subDomain));
}
}
QTEST_MAIN(tst_QHsts)
#include "tst_qhsts.moc"