From 430a4988cbe7e4a04e3b827239b13a6dbd9eae47 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 19 Sep 2014 17:25:00 +0200 Subject: [PATCH] Bug 242448 - Add a basic scriptable TLS server. r=honzab --- netwerk/base/public/moz.build | 1 + netwerk/base/public/nsITLSServerSocket.idl | 179 ++++++++ netwerk/base/src/TLSServerSocket.cpp | 476 +++++++++++++++++++++ netwerk/base/src/TLSServerSocket.h | 81 ++++ netwerk/base/src/moz.build | 1 + netwerk/base/src/nsServerSocket.cpp | 60 ++- netwerk/base/src/nsServerSocket.h | 22 +- netwerk/base/src/nsSocketTransport2.cpp | 9 + netwerk/base/src/nsSocketTransport2.h | 6 + netwerk/build/nsNetCID.h | 11 + netwerk/build/nsNetModule.cpp | 7 + netwerk/test/unit/test_tls_server.js | 167 ++++++++ netwerk/test/unit/xpcshell.ini | 3 + 13 files changed, 999 insertions(+), 24 deletions(-) create mode 100644 netwerk/base/public/nsITLSServerSocket.idl create mode 100644 netwerk/base/src/TLSServerSocket.cpp create mode 100644 netwerk/base/src/TLSServerSocket.h create mode 100644 netwerk/test/unit/test_tls_server.js diff --git a/netwerk/base/public/moz.build b/netwerk/base/public/moz.build index 21f0c6be9914..9bde21513032 100644 --- a/netwerk/base/public/moz.build +++ b/netwerk/base/public/moz.build @@ -108,6 +108,7 @@ XPIDL_SOURCES += [ 'nsIThreadRetargetableRequest.idl', 'nsIThreadRetargetableStreamListener.idl', 'nsITimedChannel.idl', + 'nsITLSServerSocket.idl', 'nsITraceableChannel.idl', 'nsITransport.idl', 'nsIUDPSocket.idl', diff --git a/netwerk/base/public/nsITLSServerSocket.idl b/netwerk/base/public/nsITLSServerSocket.idl new file mode 100644 index 000000000000..e22596072ba6 --- /dev/null +++ b/netwerk/base/public/nsITLSServerSocket.idl @@ -0,0 +1,179 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIServerSocket.idl" + +interface nsIX509Cert; +interface nsITLSServerSecurityObserver; +interface nsISocketTransport; + +[scriptable, uuid(2e025b6c-96ba-4781-85fb-d1cf1a653207)] +interface nsITLSServerSocket : nsIServerSocket +{ + /** + * serverCert + * + * The server's certificate that is presented to the client during the TLS + * handshake. This is required to be set before calling |asyncListen|. + */ + attribute nsIX509Cert serverCert; + + /** + * setSessionCache + * + * Whether the server should use a session cache. Defaults to true. This + * should be set before calling |asyncListen| if you wish to change the + * default. + */ + void setSessionCache(in boolean aSessionCache); + + /** + * setSessionTickets + * + * Whether the server should support session tickets. Defaults to true. This + * should be set before calling |asyncListen| if you wish to change the + * default. + */ + void setSessionTickets(in boolean aSessionTickets); + + /** + * Values for setRequestClientCertificate + */ + // Never request + const unsigned long REQUEST_NEVER = 0; + // Request (but do not require) during the first handshake only + const unsigned long REQUEST_FIRST_HANDSHAKE = 1; + // Request (but do not require) during each handshake + const unsigned long REQUEST_ALWAYS = 2; + // Require during the first handshake only + const unsigned long REQUIRE_FIRST_HANDSHAKE = 3; + // Require during each handshake + const unsigned long REQUIRE_ALWAYS = 4; + + /** + * setRequestClientCertificate + * + * Whether the server should request and/or require a client auth certificate + * from the client. Defaults to REQUEST_NEVER. See the possible options + * above. This should be set before calling |asyncListen| if you wish to + * change the default. + */ + void setRequestClientCertificate(in unsigned long aRequestClientCert); +}; + +/** + * Security summary for a given TLS client connection being handled by a + * |nsITLSServerSocket| server. + * + * This is accessible through the security info object on the transport, which + * will be an instance of |nsITLSServerConnectionInfo| (see below). + * + * The values of these attributes are available once the |onHandshakeDone| + * method of the security observer has been called (see + * |nsITLSServerSecurityObserver| below). + */ +[scriptable, uuid(19668ea4-e5ad-4182-9698-7e890d48f327)] +interface nsITLSClientStatus : nsISupports +{ + /** + * peerCert + * + * The client's certificate, if one was requested via |requestCertificate| + * above and supplied by the client. + */ + readonly attribute nsIX509Cert peerCert; + + /** + * Values for tlsVersionUsed, as defined by TLS + */ + const short SSL_VERSION_3 = 0x0300; + const short TLS_VERSION_1 = 0x0301; + const short TLS_VERSION_1_1 = 0x0302; + const short TLS_VERSION_1_2 = 0x0303; + const short TLS_VERSION_UNKNOWN = -1; + + /** + * tlsVersionUsed + * + * The version of TLS used by the connection. See values above. + */ + readonly attribute short tlsVersionUsed; + + /** + * cipherName + * + * Name of the cipher suite used, such as + * "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256". + * See security/nss/lib/ssl/sslinfo.c for the possible values. + */ + readonly attribute ACString cipherName; + + /** + * keyLength + * + * The "effective" key size of the symmetric key in bits. + */ + readonly attribute unsigned long keyLength; + + /** + * macLength + * + * The size of the MAC in bits. + */ + readonly attribute unsigned long macLength; +}; + +/** + * Connection info for a given TLS client connection being handled by a + * |nsITLSServerSocket| server. This object is thread-safe. + * + * This is exposed as the security info object on the transport, so it can be + * accessed via |transport.securityInfo|. + * + * This interface is available by the time the |onSocketAttached| is called, + * which is the first time the TLS server consumer is notified of a new client. + */ +[scriptable, uuid(8a93f5d5-eddd-4c62-a4bd-bfd297653184)] +interface nsITLSServerConnectionInfo : nsISupports +{ + /** + * setSecurityObserver + * + * Set the security observer to be notified when the TLS handshake has + * completed. + */ + void setSecurityObserver(in nsITLSServerSecurityObserver observer); + + /** + * serverSocket + * + * The nsITLSServerSocket instance that accepted this client connection. + */ + readonly attribute nsITLSServerSocket serverSocket; + + /** + * status + * + * Security summary for this TLS client connection. Note that the values of + * this interface are not available until the TLS handshake has completed. + * See |nsITLSClientStatus| above for more details. + */ + readonly attribute nsITLSClientStatus status; +}; + +[scriptable, uuid(1f62e1ae-e546-4a38-8917-d428472ed736)] +interface nsITLSServerSecurityObserver : nsISupports +{ + /** + * onHandsakeDone + * + * This method is called once the TLS handshake is completed. This takes + * place after |onSocketAccepted| has been called, which typically opens the + * streams to keep things moving along. It's important to be aware that the + * handshake has not completed at the point that |onSocketAccepted| is called, + * so no security verification can be done until this method is called. + */ + void onHandshakeDone(in nsITLSServerSocket aServer, + in nsITLSClientStatus aStatus); +}; diff --git a/netwerk/base/src/TLSServerSocket.cpp b/netwerk/base/src/TLSServerSocket.cpp new file mode 100644 index 000000000000..c344b45facfa --- /dev/null +++ b/netwerk/base/src/TLSServerSocket.cpp @@ -0,0 +1,476 @@ +/* vim:set ts=2 sw=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TLSServerSocket.h" + +#include "mozilla/net/DNS.h" +#include "nsAutoPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsIServerSocket.h" +#include "nsITimer.h" +#include "nsIX509Cert.h" +#include "nsIX509CertDB.h" +#include "nsNetCID.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" +#include "nsSocketTransport2.h" +#include "nsThreadUtils.h" +#include "ScopedNSSTypes.h" +#include "ssl.h" + +extern PRThread *gSocketThread; + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// TLSServerSocket +//----------------------------------------------------------------------------- + +TLSServerSocket::TLSServerSocket() + : mServerCert(nullptr) +{ +} + +TLSServerSocket::~TLSServerSocket() +{ +} + +NS_IMPL_ISUPPORTS_INHERITED(TLSServerSocket, + nsServerSocket, + nsITLSServerSocket) + +nsresult +TLSServerSocket::SetSocketDefaults() +{ + // Set TLS options on the listening socket + mFD = SSL_ImportFD(nullptr, mFD); + if (NS_WARN_IF(!mFD)) { + return mozilla::psm::GetXPCOMFromNSSError(PR_GetError()); + } + + SSL_OptionSet(mFD, SSL_SECURITY, true); + SSL_OptionSet(mFD, SSL_HANDSHAKE_AS_CLIENT, false); + SSL_OptionSet(mFD, SSL_HANDSHAKE_AS_SERVER, true); + + // We don't currently notify the server API consumer of renegotiation events + // (to revalidate peer certs, etc.), so disable it for now. + SSL_OptionSet(mFD, SSL_ENABLE_RENEGOTIATION, SSL_RENEGOTIATE_NEVER); + + SetSessionCache(true); + SetSessionTickets(true); + SetRequestClientCertificate(REQUEST_NEVER); + + return NS_OK; +} + +void +TLSServerSocket::CreateClientTransport(PRFileDesc* aClientFD, + const NetAddr& aClientAddr) +{ + MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); + nsresult rv; + + nsRefPtr trans = new nsSocketTransport; + if (NS_WARN_IF(!trans)) { + mCondition = NS_ERROR_OUT_OF_MEMORY; + return; + } + + nsRefPtr info = new TLSServerConnectionInfo(); + info->mServerSocket = this; + info->mTransport = trans; + nsCOMPtr infoSupports = + NS_ISUPPORTS_CAST(nsITLSServerConnectionInfo*, info); + rv = trans->InitWithConnectedSocket(aClientFD, &aClientAddr, infoSupports); + if (NS_WARN_IF(NS_FAILED(rv))) { + mCondition = rv; + return; + } + + // Override the default peer certificate validation, so that server consumers + // can make their own choice after the handshake completes. + SSL_AuthCertificateHook(aClientFD, AuthCertificateHook, nullptr); + // Once the TLS handshake has completed, the server consumer is notified and + // has access to various TLS state details. + // It's safe to pass info here because the socket transport holds it as + // |mSecInfo| which keeps it alive for the lifetime of the socket. + SSL_HandshakeCallback(aClientFD, TLSServerConnectionInfo::HandshakeCallback, + info); + + // Notify the consumer of the new client so it can manage the streams. + // Security details aren't known yet. The security observer will be notified + // later when they are ready. + nsCOMPtr serverSocket = + do_QueryInterface(NS_ISUPPORTS_CAST(nsITLSServerSocket*, this)); + mListener->OnSocketAccepted(serverSocket, trans); +} + +nsresult +TLSServerSocket::OnSocketListen() +{ + if (NS_WARN_IF(!mServerCert)) { + return NS_ERROR_NOT_INITIALIZED; + } + + ScopedCERTCertificate cert(mServerCert->GetCert()); + if (NS_WARN_IF(!cert)) { + return mozilla::psm::GetXPCOMFromNSSError(PR_GetError()); + } + + ScopedSECKEYPrivateKey key(PK11_FindKeyByAnyCert(cert, nullptr)); + if (NS_WARN_IF(!key)) { + return mozilla::psm::GetXPCOMFromNSSError(PR_GetError()); + } + + SSLKEAType certKEA = NSS_FindCertKEAType(cert); + + nsresult rv = MapSECStatus(SSL_ConfigSecureServer(mFD, cert, key, certKEA)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +SECStatus +TLSServerSocket::AuthCertificateHook(void* arg, PRFileDesc* fd, PRBool checksig, + PRBool isServer) +{ + // Allow any client cert here, server consumer code can decide whether it's + // okay after being notified of the new client socket. + return SECSuccess; +} + +//----------------------------------------------------------------------------- +// TLSServerSocket::nsITLSServerSocket +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +TLSServerSocket::GetServerCert(nsIX509Cert** aCert) +{ + if (NS_WARN_IF(!aCert)) { + return NS_ERROR_INVALID_POINTER; + } + *aCert = mServerCert; + NS_IF_ADDREF(*aCert); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerSocket::SetServerCert(nsIX509Cert* aCert) +{ + // If AsyncListen was already called (and set mListener), it's too late to set + // this. + if (NS_WARN_IF(mListener)) { + return NS_ERROR_IN_PROGRESS; + } + mServerCert = aCert; + return NS_OK; +} + +NS_IMETHODIMP +TLSServerSocket::SetSessionCache(bool aEnabled) +{ + // If AsyncListen was already called (and set mListener), it's too late to set + // this. + if (NS_WARN_IF(mListener)) { + return NS_ERROR_IN_PROGRESS; + } + SSL_OptionSet(mFD, SSL_NO_CACHE, !aEnabled); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerSocket::SetSessionTickets(bool aEnabled) +{ + // If AsyncListen was already called (and set mListener), it's too late to set + // this. + if (NS_WARN_IF(mListener)) { + return NS_ERROR_IN_PROGRESS; + } + SSL_OptionSet(mFD, SSL_ENABLE_SESSION_TICKETS, aEnabled); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerSocket::SetRequestClientCertificate(uint32_t aMode) +{ + // If AsyncListen was already called (and set mListener), it's too late to set + // this. + if (NS_WARN_IF(mListener)) { + return NS_ERROR_IN_PROGRESS; + } + SSL_OptionSet(mFD, SSL_REQUEST_CERTIFICATE, aMode != REQUEST_NEVER); + + switch (aMode) { + case REQUEST_ALWAYS: + SSL_OptionSet(mFD, SSL_REQUIRE_CERTIFICATE, SSL_REQUIRE_NO_ERROR); + break; + case REQUIRE_FIRST_HANDSHAKE: + SSL_OptionSet(mFD, SSL_REQUIRE_CERTIFICATE, SSL_REQUIRE_FIRST_HANDSHAKE); + break; + case REQUIRE_ALWAYS: + SSL_OptionSet(mFD, SSL_REQUIRE_CERTIFICATE, SSL_REQUIRE_ALWAYS); + break; + default: + SSL_OptionSet(mFD, SSL_REQUIRE_CERTIFICATE, SSL_REQUIRE_NEVER); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// TLSServerConnectionInfo +//----------------------------------------------------------------------------- + +namespace { + +class TLSServerSecurityObserverProxy MOZ_FINAL : public nsITLSServerSecurityObserver +{ + ~TLSServerSecurityObserverProxy() {} + +public: + explicit TLSServerSecurityObserverProxy(nsITLSServerSecurityObserver* aListener) + : mListener(new nsMainThreadPtrHolder(aListener)) + { } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITLSSERVERSECURITYOBSERVER + + class OnHandshakeDoneRunnable : public nsRunnable + { + public: + OnHandshakeDoneRunnable(const nsMainThreadPtrHandle& aListener, + nsITLSServerSocket* aServer, + nsITLSClientStatus* aStatus) + : mListener(aListener) + , mServer(aServer) + , mStatus(aStatus) + { } + + NS_DECL_NSIRUNNABLE + + private: + nsMainThreadPtrHandle mListener; + nsCOMPtr mServer; + nsCOMPtr mStatus; + }; + +private: + nsMainThreadPtrHandle mListener; +}; + +NS_IMPL_ISUPPORTS(TLSServerSecurityObserverProxy, + nsITLSServerSecurityObserver) + +NS_IMETHODIMP +TLSServerSecurityObserverProxy::OnHandshakeDone(nsITLSServerSocket* aServer, + nsITLSClientStatus* aStatus) +{ + nsRefPtr r = + new OnHandshakeDoneRunnable(mListener, aServer, aStatus); + return NS_DispatchToMainThread(r); +} + +NS_IMETHODIMP +TLSServerSecurityObserverProxy::OnHandshakeDoneRunnable::Run() +{ + mListener->OnHandshakeDone(mServer, mStatus); + return NS_OK; +} + +} // anonymous namespace + +NS_IMPL_ISUPPORTS(TLSServerConnectionInfo, + nsITLSServerConnectionInfo, + nsITLSClientStatus) + +TLSServerConnectionInfo::TLSServerConnectionInfo() + : mServerSocket(nullptr) + , mTransport(nullptr) + , mPeerCert(nullptr) + , mTlsVersionUsed(TLS_VERSION_UNKNOWN) + , mKeyLength(0) + , mMacLength(0) + , mLock("TLSServerConnectionInfo.mLock") + , mSecurityObserver(nullptr) +{ +} + +TLSServerConnectionInfo::~TLSServerConnectionInfo() +{ + if (!mSecurityObserver) { + return; + } + + nsITLSServerSecurityObserver* observer; + { + MutexAutoLock lock(mLock); + mSecurityObserver.forget(&observer); + } + + if (observer) { + nsCOMPtr mainThread; + NS_GetMainThread(getter_AddRefs(mainThread)); + NS_ProxyRelease(mainThread, observer); + } +} + +NS_IMETHODIMP +TLSServerConnectionInfo::SetSecurityObserver(nsITLSServerSecurityObserver* aObserver) +{ + { + MutexAutoLock lock(mLock); + mSecurityObserver = new TLSServerSecurityObserverProxy(aObserver); + } + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetServerSocket(nsITLSServerSocket** aSocket) +{ + if (NS_WARN_IF(!aSocket)) { + return NS_ERROR_INVALID_POINTER; + } + *aSocket = mServerSocket; + NS_IF_ADDREF(*aSocket); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetStatus(nsITLSClientStatus** aStatus) +{ + if (NS_WARN_IF(!aStatus)) { + return NS_ERROR_INVALID_POINTER; + } + *aStatus = this; + NS_IF_ADDREF(*aStatus); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetPeerCert(nsIX509Cert** aCert) +{ + if (NS_WARN_IF(!aCert)) { + return NS_ERROR_INVALID_POINTER; + } + *aCert = mPeerCert; + NS_IF_ADDREF(*aCert); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetTlsVersionUsed(int16_t* aTlsVersionUsed) +{ + if (NS_WARN_IF(!aTlsVersionUsed)) { + return NS_ERROR_INVALID_POINTER; + } + *aTlsVersionUsed = mTlsVersionUsed; + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetCipherName(nsACString& aCipherName) +{ + aCipherName.Assign(mCipherName); + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetKeyLength(uint32_t* aKeyLength) +{ + if (NS_WARN_IF(!aKeyLength)) { + return NS_ERROR_INVALID_POINTER; + } + *aKeyLength = mKeyLength; + return NS_OK; +} + +NS_IMETHODIMP +TLSServerConnectionInfo::GetMacLength(uint32_t* aMacLength) +{ + if (NS_WARN_IF(!aMacLength)) { + return NS_ERROR_INVALID_POINTER; + } + *aMacLength = mMacLength; + return NS_OK; +} + +// static +void +TLSServerConnectionInfo::HandshakeCallback(PRFileDesc* aFD, void* aArg) +{ + nsRefPtr info = + static_cast(aArg); + nsISocketTransport* transport = info->mTransport; + // No longer needed outside this function, so clear the weak ref + info->mTransport = nullptr; + nsresult rv = info->HandshakeCallback(aFD); + if (NS_WARN_IF(NS_FAILED(rv))) { + transport->Close(rv); + } +} + +nsresult +TLSServerConnectionInfo::HandshakeCallback(PRFileDesc* aFD) +{ + nsresult rv; + + ScopedCERTCertificate clientCert(SSL_PeerCertificate(aFD)); + if (clientCert) { + nsCOMPtr certDB = + do_GetService(NS_X509CERTDB_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr clientCertPSM; + rv = certDB->ConstructX509(reinterpret_cast(clientCert->derCert.data), + clientCert->derCert.len, + getter_AddRefs(clientCertPSM)); + if (NS_FAILED(rv)) { + return rv; + } + + mPeerCert = clientCertPSM; + } + + SSLChannelInfo channelInfo; + rv = MapSECStatus(SSL_GetChannelInfo(aFD, &channelInfo, sizeof(channelInfo))); + if (NS_FAILED(rv)) { + return rv; + } + mTlsVersionUsed = channelInfo.protocolVersion; + + SSLCipherSuiteInfo cipherInfo; + rv = MapSECStatus(SSL_GetCipherSuiteInfo(channelInfo.cipherSuite, &cipherInfo, + sizeof(cipherInfo))); + if (NS_FAILED(rv)) { + return rv; + } + mCipherName.Assign(cipherInfo.cipherSuiteName); + mKeyLength = cipherInfo.effectiveKeyBits; + mMacLength = cipherInfo.macBits; + + if (!mSecurityObserver) { + return NS_OK; + } + + // Notify consumer code that handshake is complete + nsCOMPtr observer; + { + MutexAutoLock lock(mLock); + mSecurityObserver.swap(observer); + } + nsCOMPtr serverSocket; + GetServerSocket(getter_AddRefs(serverSocket)); + observer->OnHandshakeDone(serverSocket, this); + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/base/src/TLSServerSocket.h b/netwerk/base/src/TLSServerSocket.h new file mode 100644 index 000000000000..7554a03c21f4 --- /dev/null +++ b/netwerk/base/src/TLSServerSocket.h @@ -0,0 +1,81 @@ +/* vim:set ts=2 sw=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_net_TLSServerSocket_h +#define mozilla_net_TLSServerSocket_h + +#include "nsAutoPtr.h" +#include "nsITLSServerSocket.h" +#include "nsServerSocket.h" +#include "nsString.h" +#include "mozilla/Mutex.h" +#include "seccomon.h" + +namespace mozilla { +namespace net { + +class TLSServerSocket : public nsServerSocket + , public nsITLSServerSocket +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSISERVERSOCKET(nsServerSocket::) + NS_DECL_NSITLSSERVERSOCKET + + // Override methods from nsServerSocket + virtual void CreateClientTransport(PRFileDesc* clientFD, + const NetAddr& clientAddr) MOZ_OVERRIDE; + virtual nsresult SetSocketDefaults() MOZ_OVERRIDE; + virtual nsresult OnSocketListen() MOZ_OVERRIDE; + + TLSServerSocket(); + +private: + virtual ~TLSServerSocket(); + + static SECStatus AuthCertificateHook(void* arg, PRFileDesc* fd, + PRBool checksig, PRBool isServer); + + nsCOMPtr mServerCert; +}; + +class TLSServerConnectionInfo : public nsITLSServerConnectionInfo + , public nsITLSClientStatus +{ + friend class TLSServerSocket; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITLSSERVERCONNECTIONINFO + NS_DECL_NSITLSCLIENTSTATUS + + TLSServerConnectionInfo(); + +private: + virtual ~TLSServerConnectionInfo(); + + static void HandshakeCallback(PRFileDesc* aFD, void* aArg); + nsresult HandshakeCallback(PRFileDesc* aFD); + + nsRefPtr mServerSocket; + // Weak ref to the transport, to avoid cycles since the transport holds a + // reference to the TLSServerConnectionInfo object. This is not handed out to + // anyone, and is only used in HandshakeCallback to close the transport in + // case of an error. After this, it's set to nullptr. + nsISocketTransport* mTransport; + nsCOMPtr mPeerCert; + int16_t mTlsVersionUsed; + nsCString mCipherName; + uint32_t mKeyLength; + uint32_t mMacLength; + // lock protects access to mSecurityObserver + mozilla::Mutex mLock; + nsCOMPtr mSecurityObserver; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_TLSServerSocket_h diff --git a/netwerk/base/src/moz.build b/netwerk/base/src/moz.build index cfeadf57beb7..0fb2fc3a97c1 100644 --- a/netwerk/base/src/moz.build +++ b/netwerk/base/src/moz.build @@ -77,6 +77,7 @@ UNIFIED_SOURCES += [ 'RedirectChannelRegistrar.cpp', 'StreamingProtocolService.cpp', 'Tickler.cpp', + 'TLSServerSocket.cpp', ] # These files cannot be built in unified mode because they force NSPR logging. diff --git a/netwerk/base/src/nsServerSocket.cpp b/netwerk/base/src/nsServerSocket.cpp index eb7317d3abd8..d3d09f7fd73d 100644 --- a/netwerk/base/src/nsServerSocket.cpp +++ b/netwerk/base/src/nsServerSocket.cpp @@ -45,8 +45,8 @@ PostEvent(nsServerSocket *s, nsServerSocketFunc func) //----------------------------------------------------------------------------- nsServerSocket::nsServerSocket() - : mLock("nsServerSocket.mLock") - , mFD(nullptr) + : mFD(nullptr) + , mLock("nsServerSocket.mLock") , mAttached(false) , mKeepWhenOffline(false) { @@ -154,6 +154,25 @@ nsServerSocket::TryAttach() return NS_OK; } +void +nsServerSocket::CreateClientTransport(PRFileDesc* aClientFD, + const NetAddr& aClientAddr) +{ + nsRefPtr trans = new nsSocketTransport; + if (NS_WARN_IF(!trans)) { + mCondition = NS_ERROR_OUT_OF_MEMORY; + return; + } + + nsresult rv = trans->InitWithConnectedSocket(aClientFD, &aClientAddr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mCondition = rv; + return; + } + + mListener->OnSocketAccepted(this, trans); +} + //----------------------------------------------------------------------------- // nsServerSocket::nsASocketHandler //----------------------------------------------------------------------------- @@ -185,25 +204,14 @@ nsServerSocket::OnSocketReady(PRFileDesc *fd, int16_t outFlags) clientFD = PR_Accept(mFD, &prClientAddr, PR_INTERVAL_NO_WAIT); PRNetAddrToNetAddr(&prClientAddr, &clientAddr); - if (!clientFD) - { + if (!clientFD) { NS_WARNING("PR_Accept failed"); mCondition = NS_ERROR_UNEXPECTED; + return; } - else - { - nsRefPtr trans = new nsSocketTransport; - if (!trans) - mCondition = NS_ERROR_OUT_OF_MEMORY; - else - { - nsresult rv = trans->InitWithConnectedSocket(clientFD, &clientAddr); - if (NS_FAILED(rv)) - mCondition = rv; - else - mListener->OnSocketAccepted(this, trans); - } - } + + // Accept succeeded, create socket transport and notify consumer + CreateClientTransport(clientFD, clientAddr); } void @@ -328,6 +336,7 @@ NS_IMETHODIMP nsServerSocket::InitWithAddress(const PRNetAddr *aAddr, int32_t aBackLog) { NS_ENSURE_TRUE(mFD == nullptr, NS_ERROR_ALREADY_INITIALIZED); + nsresult rv; // // configure listening socket... @@ -373,12 +382,18 @@ nsServerSocket::InitWithAddress(const PRNetAddr *aAddr, int32_t aBackLog) goto fail; } + // Set any additional socket defaults needed by child classes + rv = SetSocketDefaults(); + if (NS_WARN_IF(NS_FAILED(rv))) { + goto fail; + } + // wait until AsyncListen is called before polling the socket for // client connections. return NS_OK; fail: - nsresult rv = ErrorAccordingToNSPR(PR_GetError()); + rv = ErrorAccordingToNSPR(PR_GetError()); Close(); return rv; } @@ -509,6 +524,13 @@ nsServerSocket::AsyncListen(nsIServerSocketListener *aListener) mListener = new ServerSocketListenerProxy(aListener); mListenerTarget = NS_GetCurrentThread(); } + + // Child classes may need to do additional setup just before listening begins + nsresult rv = OnSocketListen(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return PostEvent(this, &nsServerSocket::OnMsgAttach); } diff --git a/netwerk/base/src/nsServerSocket.h b/netwerk/base/src/nsServerSocket.h index 6f7d25bc6398..05a2568ad816 100644 --- a/netwerk/base/src/nsServerSocket.h +++ b/netwerk/base/src/nsServerSocket.h @@ -6,12 +6,18 @@ #ifndef nsServerSocket_h__ #define nsServerSocket_h__ +#include "prio.h" #include "nsASocketHandler.h" #include "nsIServerSocket.h" #include "mozilla/Mutex.h" //----------------------------------------------------------------------------- +class nsIEventTarget; +namespace mozilla { namespace net { +union NetAddr; +}} // namespace mozilla::net + class nsServerSocket : public nsASocketHandler , public nsIServerSocket { @@ -29,20 +35,26 @@ public: virtual uint64_t ByteCountReceived() { return 0; } nsServerSocket(); -private: - virtual ~nsServerSocket(); + virtual void CreateClientTransport(PRFileDesc* clientFD, + const mozilla::net::NetAddr& clientAddr); + virtual nsresult SetSocketDefaults() { return NS_OK; } + virtual nsresult OnSocketListen() { return NS_OK; } +protected: + virtual ~nsServerSocket(); + PRFileDesc* mFD; + nsCOMPtr mListener; + +private: void OnMsgClose(); void OnMsgAttach(); - + // try attaching our socket (mFD) to the STS's poll list. nsresult TryAttach(); // lock protects access to mListener; so it is not cleared while being used. mozilla::Mutex mLock; - PRFileDesc *mFD; PRNetAddr mAddr; - nsCOMPtr mListener; nsCOMPtr mListenerTarget; bool mAttached; bool mKeepWhenOffline; diff --git a/netwerk/base/src/nsSocketTransport2.cpp b/netwerk/base/src/nsSocketTransport2.cpp index d3261d02014a..eee15bef7339 100644 --- a/netwerk/base/src/nsSocketTransport2.cpp +++ b/netwerk/base/src/nsSocketTransport2.cpp @@ -942,6 +942,15 @@ nsSocketTransport::InitWithConnectedSocket(PRFileDesc *fd, const NetAddr *addr) return PostEvent(MSG_RETRY_INIT_SOCKET); } +nsresult +nsSocketTransport::InitWithConnectedSocket(PRFileDesc* aFD, + const NetAddr* aAddr, + nsISupports* aSecInfo) +{ + mSecInfo = aSecInfo; + return InitWithConnectedSocket(aFD, aAddr); +} + nsresult nsSocketTransport::PostEvent(uint32_t type, nsresult status, nsISupports *param) { diff --git a/netwerk/base/src/nsSocketTransport2.h b/netwerk/base/src/nsSocketTransport2.h index d043a1ee8f0c..6bd3d5d85fde 100644 --- a/netwerk/base/src/nsSocketTransport2.h +++ b/netwerk/base/src/nsSocketTransport2.h @@ -131,6 +131,12 @@ public: nsresult InitWithConnectedSocket(PRFileDesc *socketFD, const mozilla::net::NetAddr *addr); + // this method instructs the socket transport to use an already connected + // socket with the given address, and additionally supplies security info. + nsresult InitWithConnectedSocket(PRFileDesc* aSocketFD, + const mozilla::net::NetAddr* aAddr, + nsISupports* aSecInfo); + // This method instructs the socket transport to open a socket // connected to the given Unix domain address. We can only create // unlayered, simple, stream sockets. diff --git a/netwerk/build/nsNetCID.h b/netwerk/build/nsNetCID.h index 130170d9b782..4117a99a3561 100644 --- a/netwerk/build/nsNetCID.h +++ b/netwerk/build/nsNetCID.h @@ -335,6 +335,17 @@ {0xab, 0x1d, 0x5e, 0x68, 0xa9, 0xf4, 0x5f, 0x08} \ } +// component implementing nsITLSServerSocket +#define NS_TLSSERVERSOCKET_CONTRACTID \ + "@mozilla.org/network/tls-server-socket;1" +#define NS_TLSSERVERSOCKET_CID \ +{ /* 1813cbb4-c98e-4622-8c7d-839167f3f272 */ \ + 0x1813cbb4, \ + 0xc98e, \ + 0x4622, \ + {0x8c, 0x7d, 0x83, 0x91, 0x67, 0xf3, 0xf2, 0x72} \ +} + // component implementing nsIUDPSocket #define NS_UDPSOCKET_CONTRACTID \ "@mozilla.org/network/udp-socket;1" diff --git a/netwerk/build/nsNetModule.cpp b/netwerk/build/nsNetModule.cpp index 34ee5852e62e..51e20fb53d4b 100644 --- a/netwerk/build/nsNetModule.cpp +++ b/netwerk/build/nsNetModule.cpp @@ -75,6 +75,10 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsSocketTransportService, Init) #include "nsServerSocket.h" NS_GENERIC_FACTORY_CONSTRUCTOR(nsServerSocket) +#include "TLSServerSocket.h" +typedef mozilla::net::TLSServerSocket TLSServerSocket; +NS_GENERIC_FACTORY_CONSTRUCTOR(TLSServerSocket) + #include "nsUDPSocket.h" NS_GENERIC_FACTORY_CONSTRUCTOR(nsUDPSocket) @@ -665,6 +669,7 @@ NS_DEFINE_NAMED_CID(NS_IOSERVICE_CID); NS_DEFINE_NAMED_CID(NS_STREAMTRANSPORTSERVICE_CID); NS_DEFINE_NAMED_CID(NS_SOCKETTRANSPORTSERVICE_CID); NS_DEFINE_NAMED_CID(NS_SERVERSOCKET_CID); +NS_DEFINE_NAMED_CID(NS_TLSSERVERSOCKET_CID); NS_DEFINE_NAMED_CID(NS_UDPSOCKET_CID); NS_DEFINE_NAMED_CID(NS_SOCKETPROVIDERSERVICE_CID); NS_DEFINE_NAMED_CID(NS_DNSSERVICE_CID); @@ -804,6 +809,7 @@ static const mozilla::Module::CIDEntry kNeckoCIDs[] = { { &kNS_STREAMTRANSPORTSERVICE_CID, false, nullptr, nsStreamTransportServiceConstructor }, { &kNS_SOCKETTRANSPORTSERVICE_CID, false, nullptr, nsSocketTransportServiceConstructor }, { &kNS_SERVERSOCKET_CID, false, nullptr, nsServerSocketConstructor }, + { &kNS_TLSSERVERSOCKET_CID, false, nullptr, TLSServerSocketConstructor }, { &kNS_UDPSOCKET_CID, false, nullptr, nsUDPSocketConstructor }, { &kNS_SOCKETPROVIDERSERVICE_CID, false, nullptr, nsSocketProviderService::Create }, { &kNS_DNSSERVICE_CID, false, nullptr, nsIDNSServiceConstructor }, @@ -950,6 +956,7 @@ static const mozilla::Module::ContractIDEntry kNeckoContracts[] = { { NS_STREAMTRANSPORTSERVICE_CONTRACTID, &kNS_STREAMTRANSPORTSERVICE_CID }, { NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &kNS_SOCKETTRANSPORTSERVICE_CID }, { NS_SERVERSOCKET_CONTRACTID, &kNS_SERVERSOCKET_CID }, + { NS_TLSSERVERSOCKET_CONTRACTID, &kNS_TLSSERVERSOCKET_CID }, { NS_UDPSOCKET_CONTRACTID, &kNS_UDPSOCKET_CID }, { NS_SOCKETPROVIDERSERVICE_CONTRACTID, &kNS_SOCKETPROVIDERSERVICE_CID }, { NS_DNSSERVICE_CONTRACTID, &kNS_DNSSERVICE_CID }, diff --git a/netwerk/test/unit/test_tls_server.js b/netwerk/test/unit/test_tls_server.js new file mode 100644 index 000000000000..5b595ea08a63 --- /dev/null +++ b/netwerk/test/unit/test_tls_server.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Need profile dir to store the key / cert +do_get_profile(); +// Ensure PSM is initialized +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Promise: promise } = + Cu.import("resource://gre/modules/Promise.jsm", {}); +const certService = Cc["@mozilla.org/security/local-cert-service;1"] + .getService(Ci.nsILocalCertService); +const certOverrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); +const socketTransportService = + Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + +function run_test() { + run_next_test(); +} + +function getCert() { + let deferred = promise.defer(); + certService.getOrCreateCert("tls-test", { + handleCert: function(c, rv) { + if (rv) { + deferred.reject(rv); + return; + } + deferred.resolve(c); + } + }); + return deferred.promise; +} + +function startServer(cert) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"] + .createInstance(Ci.nsITLSServerSocket); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + + let input, output; + + let listener = { + onSocketAccepted: function(socket, transport) { + do_print("Accept TLS client connection"); + let connectionInfo = transport.securityInfo + .QueryInterface(Ci.nsITLSServerConnectionInfo); + connectionInfo.setSecurityObserver(listener); + input = transport.openInputStream(0, 0, 0); + output = transport.openOutputStream(0, 0, 0); + }, + onHandshakeDone: function(socket, status) { + do_print("TLS handshake done"); + ok(!!status.peerCert, "Has peer cert"); + ok(status.peerCert.equals(cert), "Peer cert matches expected cert"); + + equal(status.tlsVersionUsed, Ci.nsITLSClientStatus.TLS_VERSION_1_2, + "Using TLS 1.2"); + equal(status.cipherName, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "Using expected cipher"); + equal(status.keyLength, 128, "Using 128-bit key"); + equal(status.macLength, 128, "Using 128-bit MAC"); + + input.asyncWait({ + onInputStreamReady: function(input) { + NetUtil.asyncCopy(input, output); + } + }, 0, 0, Services.tm.currentThread); + }, + onStopListening: function() {} + }; + + tlsServer.setSessionCache(false); + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUIRE_ALWAYS); + + tlsServer.asyncListen(listener); + + return tlsServer.port; +} + +function storeCertOverride(port, cert) { + let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH; + certOverrideService.rememberValidityOverride("127.0.0.1", port, cert, + overrideBits, true); +} + +function startClient(port, cert) { + let transport = + socketTransportService.createTransport(["ssl"], 1, "127.0.0.1", port, null); + let input; + let output; + + let inputDeferred = promise.defer(); + let outputDeferred = promise.defer(); + + let handler = { + + onTransportStatus: function(transport, status) { + if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + output.asyncWait(handler, 0, 0, Services.tm.currentThread); + } + }, + + onInputStreamReady: function(input) { + try { + let data = NetUtil.readInputStreamToString(input, input.available()); + equal(data, "HELLO", "Echoed data received"); + input.close(); + output.close(); + inputDeferred.resolve(); + } catch (e) { + let SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; + let SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13; + let errorCode = -1 * (e.result & 0xFFFF); + if (errorCode == SEC_ERROR_UNKNOWN_ISSUER) { + do_print("Client doesn't like server cert"); + } + inputDeferred.reject(e); + } + }, + + onOutputStreamReady: function(output) { + try { + // Set the cert we want to avoid any cert UI prompts + let clientSecInfo = transport.securityInfo; + let tlsControl = clientSecInfo.QueryInterface(Ci.nsISSLSocketControl); + tlsControl.clientCert = cert; + + output.write("HELLO", 5); + do_print("Output to server written"); + outputDeferred.resolve(); + input = transport.openInputStream(0, 0, 0); + input.asyncWait(handler, 0, 0, Services.tm.currentThread); + } catch (e) { + let SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE; + let SSL_ERROR_BAD_CERT_ALERT = SSL_ERROR_BASE + 17; + let errorCode = -1 * (e.result & 0xFFFF); + if (errorCode == SSL_ERROR_BAD_CERT_ALERT) { + do_print("Server doesn't like client cert"); + } + outputDeferred.reject(e); + } + } + + }; + + transport.setEventSink(handler, Services.tm.currentThread); + output = transport.openOutputStream(0, 0, 0); + + return promise.all([inputDeferred.promise, outputDeferred.promise]); +} + +add_task(function*() { + let cert = yield getCert(); + ok(!!cert, "Got self-signed cert"); + let port = startServer(cert); + storeCertOverride(port, cert); + yield startClient(port, cert); +}); diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index 821e3686f475..dfc2e5560dcc 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -299,3 +299,6 @@ run-if = os == "win" [test_redirect_history.js] [test_reply_without_content_type.js] [test_websocket_offline.js] +[test_tls_server.js] +# The local cert service used by this test is not currently shipped on Android +skip-if = os == "android"