From 4a6950af0f324091553f7ab7271de45721b8667f Mon Sep 17 00:00:00 2001 From: Tobias Markmann Date: Fri, 23 Oct 2015 16:31:09 +0200 Subject: Add support for OS X Secure Transport TLS backend Added integration tests for certificate validation and revocation behavior checking. Test-Information: Tested client login over TLS against Prosody and M-Link. Verified client certificate authentication works against M-Link. Change-Id: I6ad870f17adbf279f3bac913a3076909308a0021 diff --git a/BuildTools/SCons/SConstruct b/BuildTools/SCons/SConstruct index 4ead554..3fd093b 100644 --- a/BuildTools/SCons/SConstruct +++ b/BuildTools/SCons/SConstruct @@ -506,12 +506,18 @@ conf.Finish() if env["qt"] : env["QTDIR"] = env["qt"] +# Check for OS X Secure Transport +if not env.get("openssl_force_bundled", False) and env["PLATFORM"] == "darwin" and env["target"] == "native" : + env["HAVE_SECURETRANSPORT"] = True +else : + env["HAVE_SECURETRANSPORT"] = False + # OpenSSL openssl_env = conf_env.Clone() if env.get("openssl_force_bundled", False) or env["target"] in ("iphone-device", "iphone-simulator", "xcode", "android") : env["OPENSSL_BUNDLED"] = True env["HAVE_OPENSSL"] = True -else : +elif not env["HAVE_SECURETRANSPORT"] : use_openssl = bool(env["openssl"]) openssl_prefix = "" if isinstance(env["openssl"], str) : @@ -716,6 +722,6 @@ print " Projects: " + ' '.join(env["PROJECTS"]) print "" print " XML Parsers: " + ' '.join(parsers) -print " TLS Support: " + (env.get("HAVE_OPENSSL",0) and "OpenSSL" or env.get("HAVE_SCHANNEL", 0) and "Schannel" or "Disabled") +print " TLS Support: " + (env.get("HAVE_OPENSSL",0) and "OpenSSL" or env.get("HAVE_SECURETRANSPORT",0) and "Secure Transport" or env.get("HAVE_SCHANNEL", 0) and "Schannel" or "Disabled") print " DNSSD Support: " + (env.get("HAVE_BONJOUR") and "Bonjour" or (env.get("HAVE_AVAHI") and "Avahi" or "Disabled")) print diff --git a/Swift/SConscript b/Swift/SConscript index 31b0b94..bf19873 100644 --- a/Swift/SConscript +++ b/Swift/SConscript @@ -5,7 +5,7 @@ Import("env") SConscript("Controllers/SConscript") if env["SCONS_STAGE"] == "build" : - if not GetOption("help") and not env.get("HAVE_OPENSSL", 0) and not env.get("HAVE_SCHANNEL", 0) : + if not GetOption("help") and not env.get("HAVE_OPENSSL", 0) and not env.get("HAVE_SCHANNEL", 0) and not env.get("HAVE_SECURETRANSPORT", 0): print "Error: Swift requires OpenSSL support, and OpenSSL was not found." if "Swift" in env["PROJECTS"] : env["PROJECTS"].remove("Swift") diff --git a/Swiften/Crypto/UnitTest/CryptoProviderTest.cpp b/Swiften/Crypto/UnitTest/CryptoProviderTest.cpp index 575c75e..a53a0cf 100644 --- a/Swiften/Crypto/UnitTest/CryptoProviderTest.cpp +++ b/Swiften/Crypto/UnitTest/CryptoProviderTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2013 Isode Limited. + * Copyright (c) 2010-2015 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -17,7 +17,7 @@ #ifdef HAVE_OPENSSL_CRYPTO_PROVIDER #include #endif -#ifdef HAVE_OPENSSL_CRYPTO_PROVIDER +#ifdef HAVE_COMMONCRYPTO_CRYPTO_PROVIDER #include #endif #include diff --git a/Swiften/Network/TLSConnection.cpp b/Swiften/Network/TLSConnection.cpp index 149548a..c69547d 100644 --- a/Swiften/Network/TLSConnection.cpp +++ b/Swiften/Network/TLSConnection.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Isode Limited. + * Copyright (c) 2011-2015 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -66,6 +66,10 @@ HostAddressPort TLSConnection::getLocalAddress() const { return connection->getLocalAddress(); } +TLSContext* TLSConnection::getTLSContext() const { + return context; +} + void TLSConnection::handleRawConnectFinished(bool error) { connection->onConnectFinished.disconnect(boost::bind(&TLSConnection::handleRawConnectFinished, this, _1)); if (error) { diff --git a/Swiften/Network/TLSConnection.h b/Swiften/Network/TLSConnection.h index 96525ad..a037eb1 100644 --- a/Swiften/Network/TLSConnection.h +++ b/Swiften/Network/TLSConnection.h @@ -6,16 +6,15 @@ #pragma once -#include #include -#include +#include #include #include +#include #include #include - namespace Swift { class HostAddressPort; class TLSContextFactory; @@ -34,6 +33,8 @@ namespace Swift { virtual HostAddressPort getLocalAddress() const; + TLSContext* getTLSContext() const; + private: void handleRawConnectFinished(bool error); void handleRawDisconnected(const boost::optional& error); @@ -42,6 +43,7 @@ namespace Swift { void handleTLSConnectFinished(bool error); void handleTLSDataForNetwork(const SafeByteArray& data); void handleTLSDataForApplication(const SafeByteArray& data); + private: TLSContext* context; Connection::ref connection; diff --git a/Swiften/QA/TLSTest/CertificateErrorTest.cpp b/Swiften/QA/TLSTest/CertificateErrorTest.cpp new file mode 100644 index 0000000..d7c2c55 --- /dev/null +++ b/Swiften/QA/TLSTest/CertificateErrorTest.cpp @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + + /* + This file uses http://www.tls-o-matic.com/ to test the currently configured TLS backend for correct certificate validation behavior. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Swift; + +class CertificateErrorTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(CertificateErrorTest); + + // These test require the TLS-O-Matic testing CA to be trusted. For more info see https://www.tls-o-matic.com/https/test1 . + CPPUNIT_TEST(testTLS_O_MaticTrusted); + CPPUNIT_TEST(testTLS_O_MaticCertificateFromTheFuture); + CPPUNIT_TEST(testTLS_O_MaticCertificateFromThePast); + CPPUNIT_TEST(testTLS_O_MaticCertificateFromUnknownCA); + CPPUNIT_TEST(testTLS_O_MaticCertificateWrongPurpose); + +#if !defined(HAVE_OPENSSL) + // Our OpenSSL backend does not support revocation. We excluded it from the revocation tests. + CPPUNIT_TEST(testRevokedCertificateRevocationDisabled); + CPPUNIT_TEST(testRevokedCertificateRevocationEnabled); +#endif + + CPPUNIT_TEST_SUITE_END(); + + public: + void setUp() { + SWIFT_LOG(debug) << std::endl << std::endl; + eventLoop_ = new DummyEventLoop(); + boostIOServiceThread_ = new BoostIOServiceThread(); + boostIOService_ = boost::make_shared(); + connectionFactory_ = new BoostConnectionFactory(boostIOServiceThread_->getIOService(), eventLoop_); + idnConverter_ = PlatformIDNConverter::create(); + domainNameResolver_ = new PlatformDomainNameResolver(idnConverter_, eventLoop_), + + tlsFactories_ = new PlatformTLSFactories(); + tlsContextFactory_ = tlsFactories_->getTLSContextFactory(); + + tlsContextFactory_->setCheckCertificateRevocation(false); + + tlsConnectionFactory_ = new TLSConnectionFactory(tlsContextFactory_, connectionFactory_, TLSOptions()); + + connectFinished_ = false; + connectFinishedWithError_ = false; + } + + void tearDown() { + delete tlsConnectionFactory_; + delete tlsFactories_; + + delete domainNameResolver_; + delete idnConverter_; + delete connectionFactory_; + delete boostIOServiceThread_; + while (eventLoop_->hasEvents()) { + eventLoop_->processEvents(); + } + delete eventLoop_; + } + + HostAddress resolveName(const std::string& name) { + boost::shared_ptr query = domainNameResolver_->createAddressQuery(name); + query->onResult.connect(boost::bind(&CertificateErrorTest::handleAddressQueryResult, this, _1, _2)); + lastResoverResult_ = HostAddress(); + resolvingDone_ = false; + + query->run(); + while(!resolvingDone_) { + eventLoop_->processEvents(); + } + + return lastResoverResult_; + } + + void connectToServer(boost::shared_ptr connection, const std::string& hostname, int port) { + Log::setLogLevel(Log::debug); + connection->onConnectFinished.connect(boost::bind(&CertificateErrorTest::handleConnectFinished, this, _1)); + + HostAddress address = resolveName(hostname); + + connection->connect(HostAddressPort(address, port)); + + while (!connectFinished_) { + eventLoop_->processEvents(); + } + } + + void testTLS_O_MaticTrusted() { + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "test1.tls-o-matic.com", 443); + + CPPUNIT_ASSERT_EQUAL(false, connectFinishedWithError_); + CPPUNIT_ASSERT_EQUAL(CertificateVerificationError::ref(), context->getPeerCertificateVerificationError()); + } + + void testTLS_O_MaticCertificateFromTheFuture() { + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "test5.tls-o-matic.com", 405); + + CPPUNIT_ASSERT_EQUAL(true, connectFinishedWithError_); + CPPUNIT_ASSERT(context->getPeerCertificateVerificationError()); + CPPUNIT_ASSERT_EQUAL(CertificateVerificationError::NotYetValid, context->getPeerCertificateVerificationError()->getType()); + } + + void testTLS_O_MaticCertificateFromThePast() { + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "test6.tls-o-matic.com", 406); + + CPPUNIT_ASSERT_EQUAL(true, connectFinishedWithError_); + CPPUNIT_ASSERT(context->getPeerCertificateVerificationError()); + CPPUNIT_ASSERT_EQUAL(CertificateVerificationError::Expired, context->getPeerCertificateVerificationError()->getType()); + } + + void testTLS_O_MaticCertificateFromUnknownCA() { + Log::setLogLevel(Log::debug); + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "test7.tls-o-matic.com", 407); + + CPPUNIT_ASSERT_EQUAL(true, connectFinishedWithError_); + CPPUNIT_ASSERT(context->getPeerCertificateVerificationError()); + CPPUNIT_ASSERT_EQUAL(CertificateVerificationError::Untrusted, context->getPeerCertificateVerificationError()->getType()); + } + + // test14.tls-o-matic.com:414 + void testTLS_O_MaticCertificateWrongPurpose() { + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "test14.tls-o-matic.com", 414); + + CPPUNIT_ASSERT_EQUAL(true, connectFinishedWithError_); + CPPUNIT_ASSERT(context->getPeerCertificateVerificationError()); + CPPUNIT_ASSERT_EQUAL(CertificateVerificationError::InvalidPurpose, context->getPeerCertificateVerificationError()->getType()); + } + + void testRevokedCertificateRevocationDisabled() { + tlsContextFactory_->setCheckCertificateRevocation(false); + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "revoked.grc.com", 443); + + CPPUNIT_ASSERT_EQUAL(false, connectFinishedWithError_); + CPPUNIT_ASSERT(!context->getPeerCertificateVerificationError()); + } + + void testRevokedCertificateRevocationEnabled() { + tlsContextFactory_->setCheckCertificateRevocation(true); + boost::shared_ptr connection = boost::dynamic_pointer_cast(tlsConnectionFactory_->createConnection()); + TLSContext* context = connection->getTLSContext(); + + connectToServer(connection, "revoked.grc.com", 443); + + CPPUNIT_ASSERT_EQUAL(true, connectFinishedWithError_); + CPPUNIT_ASSERT(context->getPeerCertificateVerificationError()); + CPPUNIT_ASSERT_EQUAL(CertificateVerificationError::Revoked, context->getPeerCertificateVerificationError()->getType()); + } + + private: + void handleAddressQueryResult(const std::vector& address, boost::optional /* error */) { + if (address.size() > 0) { + lastResoverResult_ = address[0]; + } + resolvingDone_ = true; + } + + void handleConnectFinished(bool error) { + connectFinished_ = true; + connectFinishedWithError_ = error; + } + + private: + BoostIOServiceThread* boostIOServiceThread_; + boost::shared_ptr boostIOService_; + DummyEventLoop* eventLoop_; + ConnectionFactory* connectionFactory_; + PlatformTLSFactories* tlsFactories_; + TLSContextFactory* tlsContextFactory_; + TLSConnectionFactory* tlsConnectionFactory_; + + IDNConverter* idnConverter_; + DomainNameResolver* domainNameResolver_; + HostAddress lastResoverResult_; + bool resolvingDone_; + + bool connectFinished_; + bool connectFinishedWithError_; +}; + + +CPPUNIT_TEST_SUITE_REGISTRATION(CertificateErrorTest); diff --git a/Swiften/QA/TLSTest/SConscript b/Swiften/QA/TLSTest/SConscript index 18f6998..c597ab1 100644 --- a/Swiften/QA/TLSTest/SConscript +++ b/Swiften/QA/TLSTest/SConscript @@ -12,5 +12,6 @@ if env["TEST"] : tester = myenv.Program("TLSTest", [ "CertificateTest.cpp", + "CertificateErrorTest.cpp" ]) myenv.Test(tester, "system") diff --git a/Swiften/SConscript b/Swiften/SConscript index 9216b39..9b82434 100644 --- a/Swiften/SConscript +++ b/Swiften/SConscript @@ -563,7 +563,7 @@ if env["SCONS_STAGE"] == "build" : continue # Library-specific files - if file.endswith("_Private.h") or file.startswith("Schannel") or file.startswith("CAPI") or file.startswith("MacOSX") or file.startswith("Windows") or file.endswith("_Windows.h") or file.startswith("SQLite") or file == "ICUConverter.h" or file == "UnboundDomainNameResolver.h" : + if file.endswith("_Private.h") or file.startswith("Schannel") or file.startswith("CAPI") or file.startswith("MacOSX") or file.startswith("SecureTransport") or file.startswith("Windows") or file.endswith("_Windows.h") or file.startswith("SQLite") or file == "ICUConverter.h" or file == "UnboundDomainNameResolver.h" : continue # Specific headers we don't want to globally include diff --git a/Swiften/TLS/Certificate.h b/Swiften/TLS/Certificate.h index a39126c..00d618e 100644 --- a/Swiften/TLS/Certificate.h +++ b/Swiften/TLS/Certificate.h @@ -1,14 +1,16 @@ /* - * Copyright (c) 2010-2013 Isode Limited. + * Copyright (c) 2010-2015 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include +#include + #include -#include #include #include diff --git a/Swiften/TLS/OpenSSL/OpenSSLContext.h b/Swiften/TLS/OpenSSL/OpenSSLContext.h index 73e4322..73fe75c 100644 --- a/Swiften/TLS/OpenSSL/OpenSSLContext.h +++ b/Swiften/TLS/OpenSSL/OpenSSLContext.h @@ -1,25 +1,26 @@ /* - * Copyright (c) 2010 Isode Limited. + * Copyright (c) 2010-2015 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once -#include -#include #include -#include +#include + #include +#include #include +#include namespace Swift { class OpenSSLContext : public TLSContext, boost::noncopyable { public: OpenSSLContext(); - ~OpenSSLContext(); + virtual ~OpenSSLContext(); void connect(); bool setClientCertificate(CertificateWithKey::ref cert); diff --git a/Swiften/TLS/PlatformTLSFactories.cpp b/Swiften/TLS/PlatformTLSFactories.cpp index 2492840..588e0e1 100644 --- a/Swiften/TLS/PlatformTLSFactories.cpp +++ b/Swiften/TLS/PlatformTLSFactories.cpp @@ -18,6 +18,10 @@ #include #include #endif +#ifdef HAVE_SECURETRANSPORT + #include + #include +#endif namespace Swift { @@ -30,6 +34,10 @@ PlatformTLSFactories::PlatformTLSFactories() : contextFactory(NULL), certificate contextFactory = new SchannelContextFactory(); certificateFactory = new SchannelCertificateFactory(); #endif +#ifdef HAVE_SECURETRANSPORT + contextFactory = new SecureTransportContextFactory(); + certificateFactory = new SecureTransportCertificateFactory(); +#endif } PlatformTLSFactories::~PlatformTLSFactories() { diff --git a/Swiften/TLS/SConscript b/Swiften/TLS/SConscript index fb327b9..f5eb053 100644 --- a/Swiften/TLS/SConscript +++ b/Swiften/TLS/SConscript @@ -27,6 +27,14 @@ elif myenv.get("HAVE_SCHANNEL", 0) : "Schannel/SchannelContextFactory.cpp", ]) myenv.Append(CPPDEFINES = "HAVE_SCHANNEL") +elif myenv.get("HAVE_SECURETRANSPORT", 0) : + #swiften_env.Append(LIBS = ["Winscard"]) + objects += myenv.StaticObject([ + "SecureTransport/SecureTransportContext.mm", + "SecureTransport/SecureTransportCertificate.mm", + "SecureTransport/SecureTransportContextFactory.cpp", + ]) + myenv.Append(CPPDEFINES = "HAVE_SECURETRANSPORT") objects += myenv.SwiftenObject(["PlatformTLSFactories.cpp"]) diff --git a/Swiften/TLS/Schannel/SchannelContext.h b/Swiften/TLS/Schannel/SchannelContext.h index 36a3f0c..be30a7c 100644 --- a/Swiften/TLS/Schannel/SchannelContext.h +++ b/Swiften/TLS/Schannel/SchannelContext.h @@ -39,7 +39,7 @@ namespace Swift public: SchannelContext(bool tls1_0Workaround); - ~SchannelContext(); + virtual ~SchannelContext(); // // TLSContext diff --git a/Swiften/TLS/SecureTransport/SecureTransportCertificate.h b/Swiften/TLS/SecureTransport/SecureTransportCertificate.h new file mode 100644 index 0000000..b8d3728 --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportCertificate.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace Swift { + +class SecureTransportCertificate : public Certificate { +public: + SecureTransportCertificate(SecCertificateRef certificate); + SecureTransportCertificate(const ByteArray& der); + virtual ~SecureTransportCertificate(); + + virtual std::string getSubjectName() const; + virtual std::vector getCommonNames() const; + virtual std::vector getSRVNames() const; + virtual std::vector getDNSNames() const; + virtual std::vector getXMPPAddresses() const; + + virtual ByteArray toDER() const; + +private: + void parse(); + typedef boost::remove_pointer::type SecCertificate; + +private: + boost::shared_ptr certificateHandle_; + std::string subjectName_; + std::vector commonNames_; + std::vector srvNames_; + std::vector dnsNames_; + std::vector xmppAddresses_; +}; + +} diff --git a/Swiften/TLS/SecureTransport/SecureTransportCertificate.mm b/Swiften/TLS/SecureTransport/SecureTransportCertificate.mm new file mode 100644 index 0000000..3b4e00f --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportCertificate.mm @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include + +#include +#include + +#include + +namespace { + +template +T bridge_cast(S source) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" + return (__bridge T)(source); +#pragma clang diagnostic pop +} + +} + +namespace Swift { + +SecureTransportCertificate::SecureTransportCertificate(SecCertificateRef certificate) { + assert(certificate); + CFRetain(certificate); + certificateHandle_ = boost::shared_ptr(certificate, CFRelease); + parse(); +} + + +SecureTransportCertificate::SecureTransportCertificate(const ByteArray& der) { + CFDataRef derData = CFDataCreateWithBytesNoCopy(NULL, der.data(), static_cast(der.size()), NULL); + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, derData); + if (certificate) { + certificateHandle_ = boost::shared_ptr(certificate, CFRelease); + parse(); + } +} + +SecureTransportCertificate::~SecureTransportCertificate() { + +} + +#define NS2STDSTRING(a) (a == nil ? std::string() : std::string([a cStringUsingEncoding:NSUTF8StringEncoding])) + + +void SecureTransportCertificate::parse() { + assert(certificateHandle_); + CFErrorRef error = NULL; + + // The SecCertificateCopyValues function is not part of the iOS Secure Transport API. + CFDictionaryRef valueDict = SecCertificateCopyValues(certificateHandle_.get(), 0, &error); + if (error) { + CFRelease(error); + } + else { + // Handle subject. + CFStringRef subject = SecCertificateCopySubjectSummary(certificateHandle_.get()); + if (subject) { + NSString* subjectStr = bridge_cast(subject); + subjectName_ = NS2STDSTRING(subjectStr); + CFRelease(subject); + } + + // Handle a single Common Name. + CFStringRef commonName; + OSStatus error = SecCertificateCopyCommonName(certificateHandle_.get(), &commonName); + if (!error) { + NSString* commonNameStr = bridge_cast(commonName); + commonNames_.push_back(NS2STDSTRING(commonNameStr)); + CFRelease(commonName); + } + + // Handle Subject Alternative Names + NSDictionary* certDict = bridge_cast(valueDict); + NSDictionary* subjectAltNamesDict = certDict[@"2.5.29.17"][@"value"]; + + for (NSDictionary* entry in subjectAltNamesDict) { + if ([entry[@"label"] isEqualToString:[NSString stringWithUTF8String:ID_ON_XMPPADDR_OID]]) { + xmppAddresses_.push_back(NS2STDSTRING(entry[@"value"])); + } + else if ([entry[@"label"] isEqualToString:[NSString stringWithUTF8String:ID_ON_DNSSRV_OID]]) { + srvNames_.push_back(NS2STDSTRING(entry[@"value"])); + } + else if ([entry[@"label"] isEqualToString:@"DNS Name"]) { + dnsNames_.push_back(NS2STDSTRING(entry[@"value"])); + } + } + CFRelease(valueDict); + } +} + +std::string SecureTransportCertificate::getSubjectName() const { + return subjectName_; +} + +std::vector SecureTransportCertificate::getCommonNames() const { + return commonNames_; +} + +std::vector SecureTransportCertificate::getSRVNames() const { + return srvNames_; +} + +std::vector SecureTransportCertificate::getDNSNames() const { + return dnsNames_; +} + +std::vector SecureTransportCertificate::getXMPPAddresses() const { + return xmppAddresses_; +} + +ByteArray SecureTransportCertificate::toDER() const { + ByteArray der; + if (certificateHandle_) { + CFDataRef derData = SecCertificateCopyData(certificateHandle_.get()); + if (derData) { + try { + size_t dataSize = boost::numeric_cast(CFDataGetLength(derData)); + der.resize(dataSize); + CFDataGetBytes(derData, CFRangeMake(0,CFDataGetLength(derData)), der.data()); + } catch (...) { + } + CFRelease(derData); + } + } + return der; +} + +} diff --git a/Swiften/TLS/SecureTransport/SecureTransportCertificateFactory.h b/Swiften/TLS/SecureTransport/SecureTransportCertificateFactory.h new file mode 100644 index 0000000..1f86541 --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportCertificateFactory.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + +class SecureTransportCertificateFactory : public CertificateFactory { + public: + virtual Certificate* createCertificateFromDER(const ByteArray& der) { + return new SecureTransportCertificate(der); + } + }; +} diff --git a/Swiften/TLS/SecureTransport/SecureTransportContext.h b/Swiften/TLS/SecureTransport/SecureTransportContext.h new file mode 100644 index 0000000..aa17c66 --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportContext.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +#include + +namespace Swift { + +class SecureTransportContext : public TLSContext { + public: + SecureTransportContext(bool checkCertificateRevocation); + virtual ~SecureTransportContext(); + + virtual void connect(); + + virtual bool setClientCertificate(CertificateWithKey::ref cert); + + virtual void handleDataFromNetwork(const SafeByteArray&); + virtual void handleDataFromApplication(const SafeByteArray&); + + virtual std::vector getPeerCertificateChain() const; + virtual CertificateVerificationError::ref getPeerCertificateVerificationError() const; + + virtual ByteArray getFinishMessage() const; + + private: + static OSStatus SSLSocketReadCallback(SSLConnectionRef connection, void *data, size_t *dataLength); + static OSStatus SSLSocketWriteCallback(SSLConnectionRef connection, const void *data, size_t *dataLength); + + private: + enum State { None, Handshake, HandshakeDone, Error}; + static std::string stateToString(State state); + void setState(State newState); + + static boost::shared_ptr nativeToTLSError(OSStatus error); + boost::shared_ptr CSSMErrorToVerificationError(OSStatus resultCode); + + void processHandshake(); + void verifyServerCertificate(); + + void fatalError(boost::shared_ptr error, boost::shared_ptr certificateError); + + private: + boost::shared_ptr sslContext_; + SafeByteArray readingBuffer_; + State state_; + CertificateVerificationError::ref verificationError_; + CertificateWithKey::ref clientCertificate_; + bool checkCertificateRevocation_; +}; + +} diff --git a/Swiften/TLS/SecureTransport/SecureTransportContext.mm b/Swiften/TLS/SecureTransport/SecureTransportContext.mm new file mode 100644 index 0000000..7f44f7d --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportContext.mm @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#import +#import + +namespace { + typedef boost::remove_pointer::type CFArray; + typedef boost::remove_pointer::type SecTrust; +} + +template +T bridge_cast(S source) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" + return (__bridge T)(source); +#pragma clang diagnostic pop +} + +namespace Swift { + +namespace { + + +CFArrayRef getClientCertificateChainAsCFArrayRef(CertificateWithKey::ref key) { + boost::shared_ptr pkcs12 = boost::dynamic_pointer_cast(key); + if (!key) { + return NULL; + } + + SafeByteArray safePassword = pkcs12->getPassword(); + CFIndex passwordSize = 0; + try { + passwordSize = boost::numeric_cast(safePassword.size()); + } catch (...) { + return NULL; + } + + CFMutableArrayRef certChain = CFArrayCreateMutable(NULL, 0, 0); + + OSStatus securityError = errSecSuccess; + CFStringRef password = CFStringCreateWithBytes(kCFAllocatorDefault, safePassword.data(), passwordSize, kCFStringEncodingUTF8, false); + const void* keys[] = { kSecImportExportPassphrase }; + const void* values[] = { password }; + + CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); + + CFArrayRef items = NULL; + CFDataRef pkcs12Data = bridge_cast([NSData dataWithBytes: static_cast(pkcs12->getData().data()) length:pkcs12->getData().size()]); + securityError = SecPKCS12Import(pkcs12Data, options, &items); + NSArray* nsItems = bridge_cast(items); + + switch(securityError) { + case errSecSuccess: + break; + case errSecAuthFailed: + // Password did not work for decoding the certificate. + case errSecDecode: + // Other decoding error. + default: + CFRelease(certChain); + CFRelease(items); + CFRelease(options); + certChain = NULL; + } + + if (certChain) { + CFArrayAppendValue(certChain, nsItems[0][@"identity"]); + + for (CFIndex index = 0; index < CFArrayGetCount(bridge_cast(nsItems[0][@"chain"])); index++) { + CFArrayAppendValue(certChain, CFArrayGetValueAtIndex(bridge_cast(nsItems[0][@"chain"]), index)); + } + } + return certChain; +} + +} + +SecureTransportContext::SecureTransportContext(bool checkCertificateRevocation) : state_(None), checkCertificateRevocation_(checkCertificateRevocation) { + sslContext_ = boost::shared_ptr(SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType), CFRelease); + + OSStatus error = noErr; + // set IO callbacks + error = SSLSetIOFuncs(sslContext_.get(), &SecureTransportContext::SSLSocketReadCallback, &SecureTransportContext::SSLSocketWriteCallback); + if (error != noErr) { + SWIFT_LOG(error) << "Unable to set IO functions to SSL context." << std::endl; + sslContext_.reset(); + } + + error = SSLSetConnection(sslContext_.get(), this); + if (error != noErr) { + SWIFT_LOG(error) << "Unable to set connection to SSL context." << std::endl; + sslContext_.reset(); + } + + + error = SSLSetSessionOption(sslContext_.get(), kSSLSessionOptionBreakOnServerAuth, true); + if (error != noErr) { + SWIFT_LOG(error) << "Unable to set kSSLSessionOptionBreakOnServerAuth on session." << std::endl; + sslContext_.reset(); + } +} + +SecureTransportContext::~SecureTransportContext() { + if (sslContext_) { + SSLClose(sslContext_.get()); + } +} + +std::string SecureTransportContext::stateToString(State state) { + std::string returnValue; + switch(state) { + case Handshake: + returnValue = "Handshake"; + break; + case HandshakeDone: + returnValue = "HandshakeDone"; + break; + case None: + returnValue = "None"; + break; + case Error: + returnValue = "Error"; + break; + } + return returnValue; +} + +void SecureTransportContext::setState(State newState) { + SWIFT_LOG(debug) << "Switch state from " << stateToString(state_) << " to " << stateToString(newState) << "." << std::endl; + state_ = newState; +} + +void SecureTransportContext::connect() { + SWIFT_LOG_ASSERT(state_ == None, error) << "current state '" << stateToString(state_) << " invalid." << std::endl; + if (clientCertificate_) { + CFArrayRef certs = getClientCertificateChainAsCFArrayRef(clientCertificate_); + if (certs) { + boost::shared_ptr certRefs(certs, CFRelease); + OSStatus result = SSLSetCertificate(sslContext_.get(), certRefs.get()); + if (result != noErr) { + SWIFT_LOG(error) << "SSLSetCertificate failed with error " << result << "." << std::endl; + } + } + } + processHandshake(); +} + +void SecureTransportContext::processHandshake() { + SWIFT_LOG_ASSERT(state_ == None || state_ == Handshake, error) << "current state '" << stateToString(state_) << " invalid." << std::endl; + OSStatus error = SSLHandshake(sslContext_.get()); + if (error == errSSLWouldBlock) { + setState(Handshake); + } + else if (error == noErr) { + SWIFT_LOG(debug) << "TLS handshake successful." << std::endl; + setState(HandshakeDone); + onConnected(); + } + else if (error == errSSLPeerAuthCompleted) { + SWIFT_LOG(debug) << "Received server certificate. Start verification." << std::endl; + setState(Handshake); + verifyServerCertificate(); + } + else { + SWIFT_LOG(debug) << "Error returned from SSLHandshake call is " << error << "." << std::endl; + fatalError(nativeToTLSError(error), boost::make_shared()); + } +} + + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +void SecureTransportContext::verifyServerCertificate() { + SecTrustRef trust = NULL; + OSStatus error = SSLCopyPeerTrust(sslContext_.get(), &trust); + if (error != noErr) { + fatalError(boost::make_shared(), boost::make_shared()); + return; + } + boost::shared_ptr trustRef = boost::shared_ptr(trust, CFRelease); + + if (checkCertificateRevocation_) { + error = SecTrustSetOptions(trust, kSecTrustOptionRequireRevPerCert | kSecTrustOptionFetchIssuerFromNet); + if (error != noErr) { + fatalError(boost::make_shared(), boost::make_shared()); + return; + } + } + + SecTrustResultType trustResult; + error = SecTrustEvaluate(trust, &trustResult); + if (error != errSecSuccess) { + fatalError(boost::make_shared(), boost::make_shared()); + return; + } + + OSStatus cssmResult = 0; + switch(trustResult) { + case kSecTrustResultUnspecified: + SWIFT_LOG(warning) << "Successful implicit validation. Result unspecified." << std::endl; + break; + case kSecTrustResultProceed: + SWIFT_LOG(warning) << "Validation resulted in explicitly trusted." << std::endl; + break; + case kSecTrustResultRecoverableTrustFailure: + SWIFT_LOG(warning) << "recoverable trust failure" << std::endl; + error = SecTrustGetCssmResultCode(trust, &cssmResult); + if (error == errSecSuccess) { + verificationError_ = CSSMErrorToVerificationError(cssmResult); + if (cssmResult == CSSMERR_TP_VERIFY_ACTION_FAILED || cssmResult == CSSMERR_APPLETP_INCOMPLETE_REVOCATION_CHECK ) { + // Find out the reason why the verification failed. + CFArrayRef certChain; + CSSM_TP_APPLE_EVIDENCE_INFO* statusChain; + error = SecTrustGetResult(trustRef.get(), &trustResult, &certChain, &statusChain); + if (error == errSecSuccess) { + boost::shared_ptr certChainRef = boost::shared_ptr(certChain, CFRelease); + for (CFIndex index = 0; index < CFArrayGetCount(certChainRef.get()); index++) { + for (CFIndex n = 0; n < statusChain[index].NumStatusCodes; n++) { + // Even though Secure Transport reported CSSMERR_APPLETP_INCOMPLETE_REVOCATION_CHECK on the whole certificate + // chain, the actual cause can be that a revocation check for a specific cert returned CSSMERR_TP_CERT_REVOKED. + if (!verificationError_ || verificationError_->getType() == CertificateVerificationError::RevocationCheckFailed) { + verificationError_ = CSSMErrorToVerificationError(statusChain[index].StatusCodes[n]); + } + } + } + } + else { + + } + } + } + else { + verificationError_ = boost::make_shared(CertificateVerificationError::UnknownError); + } + break; + case kSecTrustResultOtherError: + verificationError_ = boost::make_shared(CertificateVerificationError::UnknownError); + break; + default: + SWIFT_LOG(warning) << "Unhandled trust result " << trustResult << "." << std::endl; + break; + } + + if (verificationError_) { + setState(Error); + SSLClose(sslContext_.get()); + sslContext_.reset(); + onError(boost::make_shared()); + } + else { + // proceed with handshake + processHandshake(); + } +} + +#pragma clang diagnostic pop + +bool SecureTransportContext::setClientCertificate(CertificateWithKey::ref cert) { + CFArrayRef nativeClientChain = getClientCertificateChainAsCFArrayRef(cert); + if (nativeClientChain) { + clientCertificate_ = cert; + CFRelease(nativeClientChain); + return true; + } + else { + return false; + } +} + +void SecureTransportContext::handleDataFromNetwork(const SafeByteArray& data) { + SWIFT_LOG(debug) << std::endl; + SWIFT_LOG_ASSERT(state_ == HandshakeDone || state_ == Handshake, error) << "current state '" << stateToString(state_) << " invalid." << std::endl; + + append(readingBuffer_, data); + + size_t bytesRead = 0; + OSStatus error = noErr; + SafeByteArray applicationData; + + switch(state_) { + case None: + assert(false && "Invalid state 'None'."); + break; + case Handshake: + processHandshake(); + break; + case HandshakeDone: + while (error == noErr) { + applicationData.resize(readingBuffer_.size()); + error = SSLRead(sslContext_.get(), applicationData.data(), applicationData.size(), &bytesRead); + if (error == noErr) { + // Read successful. + } + else if (error == errSSLWouldBlock) { + // Secure Transport does not want more data. + break; + } + else { + SWIFT_LOG(error) << "SSLRead failed with error " << error << ", read bytes: " << bytesRead << "." << std::endl; + fatalError(boost::make_shared(), boost::make_shared()); + return; + } + + if (bytesRead > 0) { + applicationData.resize(bytesRead); + onDataForApplication(applicationData); + } + else { + break; + } + } + break; + case Error: + SWIFT_LOG(debug) << "Igoring received data in error state." << std::endl; + break; + } +} + + +void SecureTransportContext::handleDataFromApplication(const SafeByteArray& data) { + size_t processedBytes = 0; + OSStatus error = SSLWrite(sslContext_.get(), data.data(), data.size(), &processedBytes); + switch(error) { + case errSSLWouldBlock: + SWIFT_LOG(warning) << "Unexpected because the write callback does not block." << std::endl; + return; + case errSSLClosedGraceful: + case noErr: + return; + default: + SWIFT_LOG(warning) << "SSLWrite returned error code: " << error << ", processed bytes: " << processedBytes << std::endl; + fatalError(boost::make_shared(), boost::shared_ptr()); + } +} + +std::vector SecureTransportContext::getPeerCertificateChain() const { + std::vector peerCertificateChain; + + if (sslContext_) { + typedef boost::remove_pointer::type SecTrust; + boost::shared_ptr securityTrust; + + SecTrustRef secTrust = NULL;; + OSStatus error = SSLCopyPeerTrust(sslContext_.get(), &secTrust); + if (error == noErr) { + securityTrust = boost::shared_ptr(secTrust, CFRelease); + + CFIndex chainSize = SecTrustGetCertificateCount(securityTrust.get()); + for (CFIndex n = 0; n < chainSize; n++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(securityTrust.get(), n); + if (certificate) { + peerCertificateChain.push_back(boost::make_shared(certificate)); + } + } + } + else { + SWIFT_LOG(warning) << "Failed to obtain peer trust structure; error = " << error << "." << std::endl; + } + } + + return peerCertificateChain; +} + +CertificateVerificationError::ref SecureTransportContext::getPeerCertificateVerificationError() const { + return verificationError_; +} + +ByteArray SecureTransportContext::getFinishMessage() const { + SWIFT_LOG(warning) << "Access to TLS handshake finish message is not part of OS X Secure Transport APIs." << std::endl; + return ByteArray(); +} + +/** + * This I/O callback simulates an asynchronous read to the read buffer of the context. If it is empty, it returns errSSLWouldBlock; else + * the data within the buffer is returned. + */ +OSStatus SecureTransportContext::SSLSocketReadCallback(SSLConnectionRef connection, void *data, size_t *dataLength) { + SecureTransportContext* context = const_cast(static_cast(connection)); + OSStatus retValue = noErr; + + if (context->readingBuffer_.size() < *dataLength) { + // Would block because Secure Transport is trying to read more data than there currently is available in the buffer. + *dataLength = 0; + retValue = errSSLWouldBlock; + } + else { + size_t bufferLen = *dataLength; + size_t copyToBuffer = bufferLen < context->readingBuffer_.size() ? bufferLen : context->readingBuffer_.size(); + + memcpy(data, context->readingBuffer_.data(), copyToBuffer); + + context->readingBuffer_ = SafeByteArray(context->readingBuffer_.data() + copyToBuffer, context->readingBuffer_.data() + context->readingBuffer_.size()); + *dataLength = copyToBuffer; + } + return retValue; +} + +OSStatus SecureTransportContext::SSLSocketWriteCallback(SSLConnectionRef connection, const void *data, size_t *dataLength) { + SecureTransportContext* context = const_cast(static_cast(connection)); + OSStatus retValue = noErr; + + SafeByteArray safeData; + safeData.resize(*dataLength); + memcpy(safeData.data(), data, safeData.size()); + + context->onDataForNetwork(safeData); + return retValue; +} + +boost::shared_ptr SecureTransportContext::nativeToTLSError(OSStatus /* error */) { + boost::shared_ptr swiftenError; + swiftenError = boost::make_shared(); + return swiftenError; +} + +boost::shared_ptr SecureTransportContext::CSSMErrorToVerificationError(OSStatus resultCode) { + boost::shared_ptr error; + switch(resultCode) { + case CSSMERR_TP_NOT_TRUSTED: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_TP_NOT_TRUSTED" << std::endl; + error = boost::make_shared(CertificateVerificationError::Untrusted); + break; + case CSSMERR_TP_CERT_NOT_VALID_YET: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_TP_CERT_NOT_VALID_YET" << std::endl; + error = boost::make_shared(CertificateVerificationError::NotYetValid); + break; + case CSSMERR_TP_CERT_EXPIRED: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_TP_CERT_EXPIRED" << std::endl; + error = boost::make_shared(CertificateVerificationError::Expired); + break; + case CSSMERR_TP_CERT_REVOKED: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_TP_CERT_REVOKED" << std::endl; + error = boost::make_shared(CertificateVerificationError::Revoked); + break; + case CSSMERR_TP_VERIFY_ACTION_FAILED: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_TP_VERIFY_ACTION_FAILED" << std::endl; + break; + case CSSMERR_APPLETP_INCOMPLETE_REVOCATION_CHECK: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_APPLETP_INCOMPLETE_REVOCATION_CHECK" << std::endl; + if (checkCertificateRevocation_) { + error = boost::make_shared(CertificateVerificationError::RevocationCheckFailed); + } + break; + case CSSMERR_APPLETP_OCSP_UNAVAILABLE: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_APPLETP_OCSP_UNAVAILABLE" << std::endl; + if (checkCertificateRevocation_) { + error = boost::make_shared(CertificateVerificationError::RevocationCheckFailed); + } + break; + case CSSMERR_APPLETP_SSL_BAD_EXT_KEY_USE: + SWIFT_LOG(debug) << "CSSM result code: CSSMERR_APPLETP_SSL_BAD_EXT_KEY_USE" << std::endl; + error = boost::make_shared(CertificateVerificationError::InvalidPurpose); + break; + default: + SWIFT_LOG(warning) << "unhandled CSSM error: " << resultCode << ", CSSM_TP_BASE_TP_ERROR: " << CSSM_TP_BASE_TP_ERROR << std::endl; + error = boost::make_shared(CertificateVerificationError::UnknownError); + break; + } + return error; +} + +void SecureTransportContext::fatalError(boost::shared_ptr error, boost::shared_ptr certificateError) { + setState(Error); + if (sslContext_) { + SSLClose(sslContext_.get()); + } + verificationError_ = certificateError; + onError(error); +} + +} diff --git a/Swiften/TLS/SecureTransport/SecureTransportContextFactory.cpp b/Swiften/TLS/SecureTransport/SecureTransportContextFactory.cpp new file mode 100644 index 0000000..eb761e9 --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportContextFactory.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include +#include + +namespace Swift { + +SecureTransportContextFactory::SecureTransportContextFactory() : checkCertificateRevocation_(true), disconnectOnCardRemoval_(true) { + +} + +SecureTransportContextFactory::~SecureTransportContextFactory() { + +} + +bool SecureTransportContextFactory::canCreate() const { + return true; +} + +TLSContext* SecureTransportContextFactory::createTLSContext(const TLSOptions& /* tlsOptions */) { + return new SecureTransportContext(checkCertificateRevocation_); +} + +void SecureTransportContextFactory::setCheckCertificateRevocation(bool b) { + checkCertificateRevocation_ = b; +} + +void SecureTransportContextFactory::setDisconnectOnCardRemoval(bool b) { + disconnectOnCardRemoval_ = b; + if (disconnectOnCardRemoval_) { + SWIFT_LOG(warning) << "Smart cards have not been tested yet" << std::endl; + } +} + +} diff --git a/Swiften/TLS/SecureTransport/SecureTransportContextFactory.h b/Swiften/TLS/SecureTransport/SecureTransportContextFactory.h new file mode 100644 index 0000000..f490768 --- /dev/null +++ b/Swiften/TLS/SecureTransport/SecureTransportContextFactory.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +namespace Swift { + +class SecureTransportContextFactory : public TLSContextFactory { + public: + SecureTransportContextFactory(); + virtual ~SecureTransportContextFactory(); + + virtual bool canCreate() const; + + virtual TLSContext* createTLSContext(const TLSOptions& tlsOptions); + virtual void setCheckCertificateRevocation(bool b); + virtual void setDisconnectOnCardRemoval(bool b); + + private: + bool checkCertificateRevocation_; + bool disconnectOnCardRemoval_; +}; + +} -- cgit v0.10.2-6-g49f6