diff options
Diffstat (limited to 'security/manager/ssl/tests/unit/tlsserver/lib/TLSServer.cpp')
-rw-r--r-- | security/manager/ssl/tests/unit/tlsserver/lib/TLSServer.cpp | 694 |
1 files changed, 694 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/unit/tlsserver/lib/TLSServer.cpp b/security/manager/ssl/tests/unit/tlsserver/lib/TLSServer.cpp new file mode 100644 index 0000000000..e4aeda0e82 --- /dev/null +++ b/security/manager/ssl/tests/unit/tlsserver/lib/TLSServer.cpp @@ -0,0 +1,694 @@ +/* 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 "TLSServer.h" + +#include <stdio.h> +#include <string> +#include <thread> +#include <vector> +#include <fstream> +#include <iostream> +#ifdef XP_WIN +# include <windows.h> +#else +# include <unistd.h> +#endif + +#include <utility> + +#include "base64.h" +#include "mozilla/Sprintf.h" +#include "nspr.h" +#include "nss.h" +#include "plarenas.h" +#include "prenv.h" +#include "prerror.h" +#include "prnetdb.h" +#include "prtime.h" +#include "ssl.h" +#include "sslexp.h" +#include "sslproto.h" + +namespace mozilla { +namespace test { + +static const uint16_t LISTEN_PORT = 8443; + +SSLAntiReplayContext* antiReplay = nullptr; + +DebugLevel gDebugLevel = DEBUG_ERRORS; +uint16_t gCallbackPort = 0; + +const std::string kPEMBegin = "-----BEGIN "; +const std::string kPEMEnd = "-----END "; +const char DEFAULT_CERT_NICKNAME[] = "default-ee"; + +struct Connection { + PRFileDesc* mSocket; + char mByte; + + explicit Connection(PRFileDesc* aSocket); + ~Connection(); +}; + +Connection::Connection(PRFileDesc* aSocket) : mSocket(aSocket), mByte(0) {} + +Connection::~Connection() { + if (mSocket) { + PR_Close(mSocket); + } +} + +void PrintPRError(const char* aPrefix) { + const char* err = PR_ErrorToName(PR_GetError()); + if (err) { + if (gDebugLevel >= DEBUG_ERRORS) { + fprintf(stderr, "%s: %s\n", aPrefix, err); + } + } else { + if (gDebugLevel >= DEBUG_ERRORS) { + fprintf(stderr, "%s\n", aPrefix); + } + } +} + +// This decodes a PEM file into `item`. The line endings need to be +// UNIX-style, or there will be cross-platform issues. +static bool DecodePEMFile(const std::string& filename, SECItem* item) { + std::ifstream in(filename); + if (in.bad()) { + return false; + } + + char buf[1024]; + in.getline(buf, sizeof(buf)); + if (in.bad()) { + return false; + } + + if (strncmp(buf, kPEMBegin.c_str(), kPEMBegin.size()) != 0) { + return false; + } + + std::string value; + for (;;) { + in.getline(buf, sizeof(buf)); + if (in.bad()) { + return false; + } + + if (strncmp(buf, kPEMEnd.c_str(), kPEMEnd.size()) == 0) { + break; + } + + value += buf; + } + + unsigned int binLength; + UniquePORTString bin(BitwiseCast<char*, unsigned char*>( + ATOB_AsciiToData(value.c_str(), &binLength))); + if (!bin || binLength == 0) { + PrintPRError("ATOB_AsciiToData failed"); + return false; + } + + if (SECITEM_AllocItem(nullptr, item, binLength) == nullptr) { + return false; + } + + PORT_Memcpy(item->data, bin.get(), binLength); + return true; +} + +static SECStatus AddKeyFromFile(const std::string& path, + const std::string& filename) { + ScopedAutoSECItem item; + + std::string file = path + "/" + filename; + if (!DecodePEMFile(file, &item)) { + return SECFailure; + } + + UniquePK11SlotInfo slot(PK11_GetInternalKeySlot()); + if (!slot) { + PrintPRError("PK11_GetInternalKeySlot failed"); + return SECFailure; + } + + if (PK11_NeedUserInit(slot.get())) { + if (PK11_InitPin(slot.get(), nullptr, nullptr) != SECSuccess) { + PrintPRError("PK11_InitPin failed"); + return SECFailure; + } + } + + SECKEYPrivateKey* privateKey = nullptr; + SECItem nick = {siBuffer, + BitwiseCast<unsigned char*, const char*>(filename.data()), + static_cast<unsigned int>(filename.size())}; + if (PK11_ImportDERPrivateKeyInfoAndReturnKey( + slot.get(), &item, &nick, nullptr, true, false, KU_ALL, &privateKey, + nullptr) != SECSuccess) { + PrintPRError("PK11_ImportDERPrivateKeyInfoAndReturnKey failed"); + return SECFailure; + } + + SECKEY_DestroyPrivateKey(privateKey); + return SECSuccess; +} + +static SECStatus AddCertificateFromFile(const std::string& path, + const std::string& filename) { + ScopedAutoSECItem item; + + std::string file = path + "/" + filename; + if (!DecodePEMFile(file, &item)) { + return SECFailure; + } + + UniqueCERTCertificate cert(CERT_NewTempCertificate( + CERT_GetDefaultCertDB(), &item, nullptr, false, true)); + if (!cert) { + PrintPRError("CERT_NewTempCertificate failed"); + return SECFailure; + } + + UniquePK11SlotInfo slot(PK11_GetInternalKeySlot()); + if (!slot) { + PrintPRError("PK11_GetInternalKeySlot failed"); + return SECFailure; + } + // The nickname is the filename without '.pem'. + std::string nickname = filename.substr(0, filename.length() - 4); + SECStatus rv = PK11_ImportCert(slot.get(), cert.get(), CK_INVALID_HANDLE, + nickname.c_str(), false); + if (rv != SECSuccess) { + PrintPRError("PK11_ImportCert failed"); + return rv; + } + + return SECSuccess; +} + +SECStatus LoadCertificatesAndKeys(const char* basePath) { + // The NSS cert DB path could have been specified as "sql:path". Trim off + // the leading "sql:" if so. + if (strncmp(basePath, "sql:", 4) == 0) { + basePath = basePath + 4; + } + + UniquePRDir fdDir(PR_OpenDir(basePath)); + if (!fdDir) { + PrintPRError("PR_OpenDir failed"); + return SECFailure; + } + // On the B2G ICS emulator, operations taken in AddCertificateFromFile + // appear to interact poorly with readdir (more specifically, something is + // causing readdir to never return null - it indefinitely loops through every + // file in the directory, which causes timeouts). Rather than waste more time + // chasing this down, loading certificates and keys happens in two phases: + // filename collection and then loading. (This is probably a good + // idea anyway because readdir isn't reentrant. Something could change later + // such that it gets called as a result of calling AddCertificateFromFile or + // AddKeyFromFile.) + std::vector<std::string> certificates; + std::vector<std::string> keys; + for (PRDirEntry* dirEntry = PR_ReadDir(fdDir.get(), PR_SKIP_BOTH); dirEntry; + dirEntry = PR_ReadDir(fdDir.get(), PR_SKIP_BOTH)) { + size_t nameLength = strlen(dirEntry->name); + if (nameLength > 4) { + if (strncmp(dirEntry->name + nameLength - 4, ".pem", 4) == 0) { + certificates.push_back(dirEntry->name); + } else if (strncmp(dirEntry->name + nameLength - 4, ".key", 4) == 0) { + keys.push_back(dirEntry->name); + } + } + } + SECStatus rv; + for (std::string& certificate : certificates) { + rv = AddCertificateFromFile(basePath, certificate.c_str()); + if (rv != SECSuccess) { + return rv; + } + } + for (std::string& key : keys) { + rv = AddKeyFromFile(basePath, key.c_str()); + if (rv != SECSuccess) { + return rv; + } + } + return SECSuccess; +} + +SECStatus InitializeNSS(const char* nssCertDBDir) { + // Try initializing an existing DB. + if (NSS_Init(nssCertDBDir) == SECSuccess) { + return SECSuccess; + } + + // Create a new DB if there is none... + SECStatus rv = NSS_Initialize(nssCertDBDir, nullptr, nullptr, nullptr, 0); + if (rv != SECSuccess) { + return rv; + } + + // ...and load all certificates into it. + return LoadCertificatesAndKeys(nssCertDBDir); +} + +nsresult SendAll(PRFileDesc* aSocket, const char* aData, size_t aDataLen) { + if (gDebugLevel >= DEBUG_VERBOSE) { + fprintf(stderr, "sending '%s'\n", aData); + } + + while (aDataLen > 0) { + int32_t bytesSent = + PR_Send(aSocket, aData, aDataLen, 0, PR_INTERVAL_NO_TIMEOUT); + if (bytesSent == -1) { + PrintPRError("PR_Send failed"); + return NS_ERROR_FAILURE; + } + + aDataLen -= bytesSent; + aData += bytesSent; + } + + return NS_OK; +} + +nsresult ReplyToRequest(Connection* aConn) { + // For debugging purposes, SendAll can print out what it's sending. + // So, any strings we give to it to send need to be null-terminated. + char buf[2] = {aConn->mByte, 0}; + return SendAll(aConn->mSocket, buf, 1); +} + +nsresult SetupTLS(Connection* aConn, PRFileDesc* aModelSocket) { + PRFileDesc* sslSocket = SSL_ImportFD(aModelSocket, aConn->mSocket); + if (!sslSocket) { + PrintPRError("SSL_ImportFD failed"); + return NS_ERROR_FAILURE; + } + aConn->mSocket = sslSocket; + + /* anti-replay must be configured to accept 0RTT */ + if (antiReplay) { + SECStatus rv = SSL_SetAntiReplayContext(sslSocket, antiReplay); + if (rv != SECSuccess) { + PrintPRError("error configuring anti-replay "); + return NS_ERROR_FAILURE; + } + } + + SSL_OptionSet(sslSocket, SSL_SECURITY, true); + SSL_OptionSet(sslSocket, SSL_HANDSHAKE_AS_CLIENT, false); + SSL_OptionSet(sslSocket, SSL_HANDSHAKE_AS_SERVER, true); + // Unconditionally enabling 0RTT makes test_session_resumption.js fail + SSL_OptionSet(sslSocket, SSL_ENABLE_0RTT_DATA, + !!PR_GetEnv("MOZ_TLS_SERVER_0RTT")); + + SSL_ResetHandshake(sslSocket, /* asServer */ 1); + + return NS_OK; +} + +nsresult ReadRequest(Connection* aConn) { + int32_t bytesRead = + PR_Recv(aConn->mSocket, &aConn->mByte, 1, 0, PR_INTERVAL_NO_TIMEOUT); + if (bytesRead < 0) { + PrintPRError("PR_Recv failed"); + return NS_ERROR_FAILURE; + } else if (bytesRead == 0) { + PR_SetError(PR_IO_ERROR, 0); + PrintPRError("PR_Recv EOF in ReadRequest"); + return NS_ERROR_FAILURE; + } else { + if (gDebugLevel >= DEBUG_VERBOSE) { + fprintf(stderr, "read '0x%hhx'\n", aConn->mByte); + } + } + return NS_OK; +} + +void HandleConnection(PRFileDesc* aSocket, + const UniquePRFileDesc& aModelSocket) { + Connection conn(aSocket); + nsresult rv = SetupTLS(&conn, aModelSocket.get()); + if (NS_FAILED(rv)) { + PR_SetError(PR_INVALID_STATE_ERROR, 0); + PrintPRError("PR_Recv failed"); + exit(1); + } + + // TODO: On tests that are expected to fail (e.g. due to a revoked + // certificate), the client will close the connection wtihout sending us the + // request byte. In those cases, we should keep going. But, in the cases + // where the connection is supposed to suceed, we should verify that we + // successfully receive the request and send the response. + rv = ReadRequest(&conn); + if (NS_SUCCEEDED(rv)) { + rv = ReplyToRequest(&conn); + } +} + +// returns 0 on success, non-zero on error +int DoCallback() { + UniquePRFileDesc socket(PR_NewTCPSocket()); + if (!socket) { + PrintPRError("PR_NewTCPSocket failed"); + return 1; + } + + PRNetAddr addr; + PR_InitializeNetAddr(PR_IpAddrLoopback, gCallbackPort, &addr); + if (PR_Connect(socket.get(), &addr, PR_INTERVAL_NO_TIMEOUT) != PR_SUCCESS) { + PrintPRError("PR_Connect failed"); + return 1; + } + + const char* request = "GET / HTTP/1.0\r\n\r\n"; + SendAll(socket.get(), request, strlen(request)); + char buf[4096]; + memset(buf, 0, sizeof(buf)); + int32_t bytesRead = + PR_Recv(socket.get(), buf, sizeof(buf) - 1, 0, PR_INTERVAL_NO_TIMEOUT); + if (bytesRead < 0) { + PrintPRError("PR_Recv failed 1"); + return 1; + } + if (bytesRead == 0) { + fprintf(stderr, "PR_Recv eof 1\n"); + return 1; + } + fprintf(stderr, "%s\n", buf); + return 0; +} + +SECStatus ConfigSecureServerWithNamedCert( + PRFileDesc* fd, const char* certName, + /*optional*/ UniqueCERTCertificate* certOut, + /*optional*/ SSLKEAType* keaOut, + /*optional*/ SSLExtraServerCertData* extraData) { + UniqueCERTCertificate cert(PK11_FindCertFromNickname(certName, nullptr)); + if (!cert) { + PrintPRError("PK11_FindCertFromNickname failed"); + return SECFailure; + } + // If an intermediate certificate issued the server certificate (rather than + // directly by a trust anchor), we want to send it along in the handshake so + // we don't encounter unknown issuer errors when that's not what we're + // testing. + UniqueCERTCertificateList certList; + UniqueCERTCertificate issuerCert( + CERT_FindCertByName(CERT_GetDefaultCertDB(), &cert->derIssuer)); + // If we can't find the issuer cert, continue without it. + if (issuerCert) { + // Sadly, CERTCertificateList does not have a CERT_NewCertificateList + // utility function, so we must create it ourselves. This consists + // of creating an arena, allocating space for the CERTCertificateList, + // and then transferring ownership of the arena to that list. + UniquePLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); + if (!arena) { + PrintPRError("PORT_NewArena failed"); + return SECFailure; + } + certList.reset(static_cast<CERTCertificateList*>( + PORT_ArenaAlloc(arena.get(), sizeof(CERTCertificateList)))); + if (!certList) { + PrintPRError("PORT_ArenaAlloc failed"); + return SECFailure; + } + certList->arena = arena.release(); + // We also have to manually copy the certificates we care about to the + // list, because there aren't any utility functions for that either. + certList->certs = static_cast<SECItem*>( + PORT_ArenaAlloc(certList->arena, 2 * sizeof(SECItem))); + if (SECITEM_CopyItem(certList->arena, certList->certs, &cert->derCert) != + SECSuccess) { + PrintPRError("SECITEM_CopyItem failed"); + return SECFailure; + } + if (SECITEM_CopyItem(certList->arena, certList->certs + 1, + &issuerCert->derCert) != SECSuccess) { + PrintPRError("SECITEM_CopyItem failed"); + return SECFailure; + } + certList->len = 2; + } + + UniquePK11SlotInfo slot(PK11_GetInternalKeySlot()); + if (!slot) { + PrintPRError("PK11_GetInternalKeySlot failed"); + return SECFailure; + } + UniqueSECKEYPrivateKey key( + PK11_FindKeyByDERCert(slot.get(), cert.get(), nullptr)); + if (!key) { + PrintPRError("PK11_FindKeyByDERCert failed"); + return SECFailure; + } + + if (extraData) { + SSLExtraServerCertData dataCopy = {ssl_auth_null, nullptr, nullptr, + nullptr, nullptr, nullptr}; + memcpy(&dataCopy, extraData, sizeof(dataCopy)); + dataCopy.certChain = certList.get(); + + if (SSL_ConfigServerCert(fd, cert.get(), key.get(), &dataCopy, + sizeof(dataCopy)) != SECSuccess) { + PrintPRError("SSL_ConfigServerCert failed"); + return SECFailure; + } + + } else { + // This is the deprecated setup mechanism, to be cleaned up in Bug 1569222 + SSLKEAType certKEA = NSS_FindCertKEAType(cert.get()); + if (SSL_ConfigSecureServerWithCertChain(fd, cert.get(), certList.get(), + key.get(), certKEA) != SECSuccess) { + PrintPRError("SSL_ConfigSecureServer failed"); + return SECFailure; + } + + if (keaOut) { + *keaOut = certKEA; + } + } + + if (certOut) { + *certOut = std::move(cert); + } + + SSL_OptionSet(fd, SSL_NO_CACHE, false); + SSL_OptionSet(fd, SSL_ENABLE_SESSION_TICKETS, true); + // Unconditionally enabling 0RTT makes test_session_resumption.js fail + SSL_OptionSet(fd, SSL_ENABLE_0RTT_DATA, !!PR_GetEnv("MOZ_TLS_SERVER_0RTT")); + + return SECSuccess; +} + +#ifdef XP_WIN +using PidType = DWORD; +constexpr bool IsValidPid(long long pid) { + // Excluding `(DWORD)-1` because it is not a valid process ID. + // See https://devblogs.microsoft.com/oldnewthing/20040223-00/?p=40503 + return pid > 0 && pid < std::numeric_limits<PidType>::max(); +} +#else +using PidType = pid_t; +constexpr bool IsValidPid(long long pid) { + return pid > 0 && pid <= std::numeric_limits<PidType>::max(); +} +#endif + +PidType ConvertPid(const char* pidStr) { + long long pid = strtoll(pidStr, nullptr, 10); + if (!IsValidPid(pid)) { + return 0; + } + return static_cast<PidType>(pid); +} + +int StartServer(int argc, char* argv[], SSLSNISocketConfig sniSocketConfig, + void* sniSocketConfigArg, ServerConfigFunc configFunc) { + if (argc != 3) { + fprintf(stderr, "usage: %s <NSS DB directory> <ppid>\n", argv[0]); + return 1; + } + const char* nssCertDBDir = argv[1]; + PidType ppid = ConvertPid(argv[2]); + + const char* debugLevel = PR_GetEnv("MOZ_TLS_SERVER_DEBUG_LEVEL"); + if (debugLevel) { + int level = atoi(debugLevel); + switch (level) { + case DEBUG_ERRORS: + gDebugLevel = DEBUG_ERRORS; + break; + case DEBUG_WARNINGS: + gDebugLevel = DEBUG_WARNINGS; + break; + case DEBUG_VERBOSE: + gDebugLevel = DEBUG_VERBOSE; + break; + default: + PrintPRError("invalid MOZ_TLS_SERVER_DEBUG_LEVEL"); + return 1; + } + } + + const char* callbackPort = PR_GetEnv("MOZ_TLS_SERVER_CALLBACK_PORT"); + if (callbackPort) { + gCallbackPort = atoi(callbackPort); + } + + if (InitializeNSS(nssCertDBDir) != SECSuccess) { + PR_fprintf(PR_STDERR, "InitializeNSS failed"); + return 1; + } + + if (NSS_SetDomesticPolicy() != SECSuccess) { + PrintPRError("NSS_SetDomesticPolicy failed"); + return 1; + } + + NSS_SetAlgorithmPolicy(SEC_OID_XYBER768D00, NSS_USE_ALG_IN_SSL_KX, 0); + + if (SSL_ConfigServerSessionIDCache(0, 0, 0, nullptr) != SECSuccess) { + PrintPRError("SSL_ConfigServerSessionIDCache failed"); + return 1; + } + + UniquePRFileDesc serverSocket(PR_NewTCPSocket()); + if (!serverSocket) { + PrintPRError("PR_NewTCPSocket failed"); + return 1; + } + + PRSocketOptionData socketOption; + socketOption.option = PR_SockOpt_Reuseaddr; + socketOption.value.reuse_addr = true; + PR_SetSocketOption(serverSocket.get(), &socketOption); + + PRNetAddr serverAddr; + PR_InitializeNetAddr(PR_IpAddrLoopback, LISTEN_PORT, &serverAddr); + if (PR_Bind(serverSocket.get(), &serverAddr) != PR_SUCCESS) { + PrintPRError("PR_Bind failed"); + return 1; + } + + if (PR_Listen(serverSocket.get(), 1) != PR_SUCCESS) { + PrintPRError("PR_Listen failed"); + return 1; + } + + UniquePRFileDesc rawModelSocket(PR_NewTCPSocket()); + if (!rawModelSocket) { + PrintPRError("PR_NewTCPSocket failed for rawModelSocket"); + return 1; + } + + UniquePRFileDesc modelSocket(SSL_ImportFD(nullptr, rawModelSocket.release())); + if (!modelSocket) { + PrintPRError("SSL_ImportFD of rawModelSocket failed"); + return 1; + } + + SSLVersionRange range = {0, 0}; + if (SSL_VersionRangeGet(modelSocket.get(), &range) != SECSuccess) { + PrintPRError("SSL_VersionRangeGet failed"); + return 1; + } + + if (range.max < SSL_LIBRARY_VERSION_TLS_1_3) { + range.max = SSL_LIBRARY_VERSION_TLS_1_3; + if (SSL_VersionRangeSet(modelSocket.get(), &range) != SECSuccess) { + PrintPRError("SSL_VersionRangeSet failed"); + return 1; + } + } + + if (PR_GetEnv("MOZ_TLS_SERVER_0RTT")) { + if (SSL_CreateAntiReplayContext(PR_Now(), 1L * PR_USEC_PER_SEC, 7, 14, + &antiReplay) != SECSuccess) { + PrintPRError("Unable to create anti-replay context for 0-RTT."); + return 1; + } + } + + if (SSL_SNISocketConfigHook(modelSocket.get(), sniSocketConfig, + sniSocketConfigArg) != SECSuccess) { + PrintPRError("SSL_SNISocketConfigHook failed"); + return 1; + } + + // We have to configure the server with a certificate, but it's not one + // we're actually going to end up using. In the SNI callback, we pick + // the right certificate for the connection. + // + // Provide an empty |extra_data| to force config via SSL_ConfigServerCert. + // This is a temporary mechanism to work around inconsistent setting of + // |authType| in the deprecated API (preventing the default cert from + // being removed in favor of the SNI-selected cert). This may be removed + // after Bug 1569222 removes the deprecated mechanism. + SSLExtraServerCertData extra_data = {ssl_auth_null, nullptr, nullptr, + nullptr, nullptr, nullptr}; + if (ConfigSecureServerWithNamedCert(modelSocket.get(), DEFAULT_CERT_NICKNAME, + nullptr, nullptr, + &extra_data) != SECSuccess) { + return 1; + } + + // Call back to implementation-defined configuration func, if provided. + if (configFunc) { + if (((configFunc)(modelSocket.get())) != SECSuccess) { + PrintPRError("configFunc failed"); + return 1; + } + } + + if (gCallbackPort != 0) { + if (DoCallback()) { + return 1; + } + } + + std::thread([ppid] { + if (!ppid) { + if (gDebugLevel >= DEBUG_ERRORS) { + fprintf(stderr, "invalid ppid\n"); + } + return; + } +#ifdef XP_WIN + HANDLE parent = OpenProcess(SYNCHRONIZE, false, ppid); + if (!parent) { + if (gDebugLevel >= DEBUG_ERRORS) { + fprintf(stderr, "OpenProcess failed\n"); + } + return; + } + WaitForSingleObject(parent, INFINITE); + CloseHandle(parent); +#else + while (getppid() == ppid) { + sleep(1); + } +#endif + if (gDebugLevel >= DEBUG_ERRORS) { + fprintf(stderr, "Parent process crashed\n"); + } + exit(1); + }).detach(); + + while (true) { + PRNetAddr clientAddr; + PRFileDesc* clientSocket = + PR_Accept(serverSocket.get(), &clientAddr, PR_INTERVAL_NO_TIMEOUT); + HandleConnection(clientSocket, modelSocket); + } +} + +} // namespace test +} // namespace mozilla |