Allow Secure Transport backend to use a temporary keychain

Cherry picked:

this change was first merged into dev, but now we also need it in 5.9
to enable SSL socket tests on the new CI VMs (macOS 10.11, 10.12).
As we do not merge dev->5.9, we need this cherry-pick.

Since day one Secure Transport socket has two annoying problems on macOS:
when we call SecPKCS12Import, we indeed import certs and keys into the default keychain
and also (which is more serious) later a dialog can pop up, asking for permission
to use a private key (this is especially annoying if you're running SSL autotests or
have a server application). Apparently, it's possible to work around those problems
if we create our own (temporary) keychain and pass it in the 'options' parameter
to SecPKCS12Import.

[ChangeLog][QtNetwork] Allow QSslSocket to use a temporary keychain on macOS.

Task-number: QTBUG-56102
Change-Id: Ic3a56c905100dc80d907a25fe6ebfa232dcf5b9e
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
(cherry picked from commit 17927392cf)
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
This commit is contained in:
Timur Pocheptsov 2017-02-01 13:55:32 +01:00
parent de4aeade5e
commit 5377f4ec0e
2 changed files with 125 additions and 6 deletions

View File

@ -972,6 +972,13 @@ QList<QSslCertificate> QSslSocket::localCertificateChain() const
sockets, but are also rarely used by client sockets if the server requires
the client to authenticate.
\note Secure Transport SSL backend on macOS may update the default keychain
(the default is probably your login keychain) by importing your local certificates
and keys. This can also result in system dialogs showing up and asking for
permission when your application is using these private keys. If such behavior
is undesired, set the QT_SSL_USE_TEMPORARY_KEYCHAIN environment variable to a
non-zero value; this will prompt QSslSocket to use its own temporary keychain.
\sa localCertificate(), setPrivateKey()
*/
void QSslSocket::setLocalCertificate(const QSslCertificate &certificate)

View File

@ -53,9 +53,12 @@
#include <QtCore/qvector.h>
#include <QtCore/qmutex.h>
#include <QtCore/qdebug.h>
#include <QtCore/quuid.h>
#include <QtCore/qdir.h>
#include <algorithm>
#include <cstddef>
#include <vector>
#include <QtCore/private/qcore_mac_p.h>
@ -65,6 +68,102 @@
QT_BEGIN_NAMESPACE
namespace
{
#ifdef Q_OS_MACOS
/*
Our own temporarykeychain is needed only on macOS where SecPKCS12Import changes
the default keychain and where we see annoying pop-ups asking about accessing a
private key.
*/
struct EphemeralSecKeychain
{
EphemeralSecKeychain();
~EphemeralSecKeychain();
SecKeychainRef keychain = nullptr;
Q_DISABLE_COPY(EphemeralSecKeychain)
};
EphemeralSecKeychain::EphemeralSecKeychain()
{
const auto uuid = QUuid::createUuid();
if (uuid.isNull()) {
qCWarning(lcSsl) << "Failed to create an unique keychain name";
return;
}
QString uuidAsString(uuid.toString());
Q_ASSERT(uuidAsString.size() > 2);
Q_ASSERT(uuidAsString.startsWith(QLatin1Char('{'))
&& uuidAsString.endsWith(QLatin1Char('}')));
uuidAsString = uuidAsString.mid(1, uuidAsString.size() - 2);
QString keychainName(QDir::tempPath());
keychainName.append(QDir::separator());
keychainName += uuidAsString;
keychainName += QLatin1String(".keychain");
// SecKeychainCreate, pathName parameter:
//
// "A constant character string representing the POSIX path indicating where
// to store the keychain."
//
// Internally they seem to use std::string, but this does not really help.
// Fortunately, CFString has a convenient API.
QCFType<CFStringRef> cfName = keychainName.toCFString();
std::vector<char> posixPath;
// "Extracts the contents of a string as a NULL-terminated 8-bit string
// appropriate for passing to POSIX APIs."
posixPath.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfName));
const auto ok = CFStringGetFileSystemRepresentation(cfName, &posixPath[0],
CFIndex(posixPath.size()));
if (!ok) {
qCWarning(lcSsl) << "Failed to create a unique keychain name from"
<< "QDir::tempPath()";
return;
}
std::vector<uint8_t> passUtf8(256);
if (SecRandomCopyBytes(kSecRandomDefault, passUtf8.size(), &passUtf8[0])) {
qCWarning(lcSsl) << "SecRandomCopyBytes: failed to create a key";
return;
}
const OSStatus status = SecKeychainCreate(&posixPath[0], passUtf8.size(),
&passUtf8[0], FALSE, nullptr,
&keychain);
if (status != errSecSuccess || !keychain) {
qCWarning(lcSsl) << "SecKeychainCreate: failed to create a custom keychain";
if (keychain) {
SecKeychainDelete(keychain);
CFRelease(keychain);
keychain = nullptr;
}
}
#ifdef QSSLSOCKET_DEBUG
if (keychain) {
qCDebug(lcSsl) << "Custom keychain with name" << keychainName << "was created"
<< "successfully";
}
#endif
}
EphemeralSecKeychain::~EphemeralSecKeychain()
{
if (keychain) {
// clear file off disk
SecKeychainDelete(keychain);
CFRelease(keychain);
}
}
#endif // Q_OS_MACOS
}
static SSLContextRef qt_createSecureTransportContext(QSslSocket::SslMode mode)
{
const bool isServer = mode == QSslSocket::SslServerMode;
@ -815,11 +914,24 @@ bool QSslSocketBackendPrivate::setSessionCertificate(QString &errorDescription,
QCFType<CFDataRef> pkcs12 = _q_makePkcs12(configuration.localCertificateChain,
configuration.privateKey, passPhrase).toCFData();
QCFType<CFStringRef> password = passPhrase.toCFString();
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
QCFType<CFDictionaryRef> options(CFDictionaryCreate(Q_NULLPTR, keys, values, 1,
Q_NULLPTR, Q_NULLPTR));
CFArrayRef items = Q_NULLPTR;
const void *keys[2] = { kSecImportExportPassphrase };
const void *values[2] = { password };
CFIndex nKeys = 1;
#ifdef Q_OS_MACOS
bool envOk = false;
const int env = qEnvironmentVariableIntValue("QT_SSL_USE_TEMPORARY_KEYCHAIN", &envOk);
if (envOk && env) {
static const EphemeralSecKeychain temporaryKeychain;
if (temporaryKeychain.keychain) {
nKeys = 2;
keys[1] = kSecImportExportKeychain;
values[1] = temporaryKeychain.keychain;
}
}
#endif
QCFType<CFDictionaryRef> options = CFDictionaryCreate(nullptr, keys, values, nKeys,
nullptr, nullptr);
CFArrayRef items = nullptr;
OSStatus err = SecPKCS12Import(pkcs12, options, &items);
if (err != noErr) {
#ifdef QSSLSOCKET_DEBUG
@ -851,7 +963,7 @@ bool QSslSocketBackendPrivate::setSessionCertificate(QString &errorDescription,
return false;
}
QCFType<CFMutableArrayRef> certs = CFArrayCreateMutable(Q_NULLPTR, 0, &kCFTypeArrayCallBacks);
QCFType<CFMutableArrayRef> certs = CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks);
if (!certs) {
errorCode = QAbstractSocket::SslInternalError;
errorDescription = QStringLiteral("Failed to allocate certificates array");