Oops, I forgot some details - we are using NSS-3.11.4 and NSPR-4.6.4
on a RedHat Linux with Kernel 2.4, using gcc 3.4.5.
On Jul 10, 5:36 pm, Yahel Zamir <[EMAIL PROTECTED]> wrote:
> Hi,
>
> I have encountered a problem in using SSL sockets in blocking mode. My
> application is multi threaded, with one thread waiting to read and
> another that is waiting to write. Upon some external input, a third
> thread tries to shutdown the connection and then close the socket.
> However, calling PR_Shutdown or PR_Close never returns from
> SSL_LOCK_READER(ss).
>
> Following are a simple client and server that demonstrate the problem
> (search the client for 999 and the server for "delayed_close").
>
> Thanks,
> Yahel Zamir,
> SW Engineer at CWNT
>
> ===
> M_DIST = /usr/home/yahel/programs/nss/mozilla/dist
>
> M_DIST_LINUX = ${M_DIST}/Linux2.4_x86_glibc_PTH_DBG.OBJ
>
> M_INC = -I${M_DIST_LINUX}/include
> M_INC += -I${M_DIST}/public/nss
> M_INC += -I${M_DIST}/private/nss # for error strings
>
> M_FLAGS = -g -Wall -pthread
>
> M_LIBS = -L${M_DIST_LINUX}/lib
> M_LIBS += -lssl3 -lnss3 -lnspr4 -lplds4 -lplc4 -lsoftokn3
> M_LIBS += -lfreebl3 -lsectool -lpthread -ldl -lc
>
> all: server client
> @echo done
>
> clean:
> rm -f client server client.o server.o
>
> server.o: server.c
> gcc server.c -c ${M_FLAGS} ${M_INC} -o server.o
>
> client.o: client.c
> gcc client.c -c ${M_FLAGS} ${M_INC} -o client.o
>
> server: server.o
> gcc server.o ${M_FLAGS} ${M_LIBS} -o server
>
> client: client.o
> gcc client.o ${M_FLAGS} ${M_LIBS} -o client
>
> ===
>
> // client.c
> // --------
>
> #include <unistd.h>
> #include <plgetopt.h>
> #include <nspr.h>
> #include <nss.h>
> #include <pk11pub.h>
> #include <ssl.h>
> #include <sslproto.h>
> #include <key.h>
> #include <secutil.h> // a private API needed for SECU_Strerror
>
> static char *s_progName = NULL;
> static char *s_password = NULL;
> static char *s_hostName = NULL;
>
> /*
> * errWarn()
> *
> * Print a warning message for NSS and NSPR errors.
> * More detailed explanations for the error can be found at:
> *http://www.mozilla.org/projects/security/pki/nss/ref/ssl/sslerr.html
> */
> static void
> errWarn(char * funcString)
> {
> PRErrorCode perr = PR_GetError();
> const char * errString = SECU_Strerror(perr);
> fprintf(stderr, "%s: %s returned error %d (%s)\n", s_progName,
> funcString, perr, errString);
> return;
>
> }
>
> static void
> errExit(char * funcString)
> {
> errWarn(funcString);
> exit(3);
>
> }
>
> static void
> Usage(const char *progName)
> {
> fprintf(stderr, "Usage: %s hostname -n cert_name -p port -d cert_dir -
> w password \n", progName);
> exit(1);
>
> }
>
> /* fakePasswd()
> *
> * This function is our custom password handler that is called by
> * SSL when retreiving private certs and keys from the database.
> Returns a
> * pointer to a string that with a password for the database. Password
> pointer
> * should point to dynamically allocated memory that will be freed
> later.
> * We set "arg" to give the correct password, using
> SSL_SetPKCS11PinArg().
> */
> char *
> fakePasswd(PK11SlotInfo *info, PRBool retry, void *arg)
> {
> char * passwd = NULL;
>
> if ((!retry) && (arg != NULL)) {
> passwd = PL_strdup((char *)arg);
> }
>
> return passwd;
>
> }
>
> /* Function: setupSSLSocket()
> *
> * Purpose: Configure a socket for SSL.
> */
> PRFileDesc *
> setupSSLSocket()
> {
> PRFileDesc *sslSocket;
> PRFileDesc *tcpSocket;
> SECStatus secStatus;
> PRSocketOptionData socketOption;
> PRStatus prStatus;
>
> tcpSocket = PR_NewTCPSocket();
> if (tcpSocket == NULL) {
> errWarn("PR_NewTCPSocket");
> return NULL;
> }
>
> // Ensure the socket is blocking.
> socketOption.option = PR_SockOpt_Nonblocking;
> socketOption.value.non_blocking = PR_FALSE;
>
> prStatus = PR_SetSocketOption(tcpSocket, &socketOption);
> if (prStatus != PR_SUCCESS) {
> errWarn("PR_SetSocketOption");
> PR_Close(tcpSocket);
> return NULL;
> }
>
> sslSocket = SSL_ImportFD(NULL, tcpSocket);
> if (sslSocket == NULL) {
> errWarn("SSL_ImportFD");
> PR_Close(tcpSocket);
> return NULL;
> }
>
> // ensure original socket is not used
> tcpSocket = NULL;
>
> do {
>
> // handshake as client
> secStatus = SSL_OptionSet(sslSocket, SSL_HANDSHAKE_AS_CLIENT,
> PR_TRUE);
> if (secStatus != SECSuccess) {
> errWarn("SSL_OptionSet:SSL_HANDSHAKE_AS_CLIENT");
> break;
> }
>
> // enable full duplex
> secStatus = SSL_OptionSet(sslSocket, SSL_ENABLE_FDX, PR_TRUE);
> if (secStatus != SECSuccess) {
> errWarn("SSL_OptionSet(SSL_ENABLE_FDX)");
> break;
> }
>
> // allow communication without encryption (YZ - must be only
> upon client request)
> if (SECSuccess != SSL_CipherPrefSet(sslSocket,
> SSL_RSA_WITH_NULL_SHA, PR_TRUE)
> || SECSuccess != SSL_CipherPrefSet(sslSocket,
> SSL_RSA_WITH_NULL_MD5, PR_TRUE)) {
> errWarn("SSL_CipherPrefSet - Null Cipher");
> break;
> }
>
> secStatus = SSL_SetPKCS11PinArg(sslSocket, s_password);
> if (secStatus != SECSuccess) {
> errWarn("SSL_SetPKCS11PinArg");
> break;
> }
>
> secStatus = SSL_SetURL(sslSocket, s_hostName);
> if (secStatus != SECSuccess) {
> errWarn("SSL_SetURL");
> break;
> }
>
> // Success
> return sslSocket;
>
> } while (0);
>
> // Failure
> PR_Close(sslSocket);
> return NULL;
>
> }
>
> /* Function: clientMain()
> *
> * Purpose: Setup an SSL socket and connect a server.
> * Send a small message and expect a reply message.
> */
> void
> clientMain(unsigned short port)
> {
> SECStatus secStatus;
> PRStatus prStatus;
> PRInt32 rv;
> PRNetAddr addr;
> PRHostEnt hostEntry;
> PRFileDesc *sslSocket;
> char buffer[256];
>
> sslSocket = setupSSLSocket();
> if (sslSocket == NULL) {
> errExit("setupSSLSocket");
> }
>
> prStatus = PR_GetHostByName(s_hostName, buffer, sizeof(buffer),
> &hostEntry);
> if (prStatus != PR_SUCCESS) {
> errExit("PR_GetHostByName");
> }
>
> rv = PR_EnumerateHostEnt(0, &hostEntry, port, &addr);
> if (rv < 0) {
> errExit("PR_EnumerateHostEnt");
> }
>
> prStatus = PR_Connect(sslSocket, &addr, PR_INTERVAL_NO_TIMEOUT);
> if (prStatus != PR_SUCCESS) {
> errExit("PR_Connect");
> }
>
> secStatus = SSL_ResetHandshake(sslSocket, /* asServer */
> PR_FALSE);
> if (secStatus != SECSuccess) {
> errExit("SSL_ResetHandshake");
> }
>
> // single write, then single read
>
> sprintf(buffer, "hello from client");
> int msgSize = strlen(buffer);
>
> sleep(999);
>
> rv = PR_Write(sslSocket, buffer, msgSize);
> if (rv != msgSize) {
> errWarn("PR_Write");
> PR_Close(sslSocket);
> return;
> }
>
> printf("client sent: %s \n", buffer);
>
> rv = PR_Read(sslSocket, buffer, sizeof(buffer));
> // (rv == 0) is EOF
> if (rv <= 0) {
> errWarn("PR_Read");
> PR_Close(sslSocket);
> return;
> }
>
> buffer[rv] = 0;
> printf("client received: %s \n", buffer);
>
> prStatus = PR_Close(sslSocket);
> if (prStatus != PR_SUCCESS) {
> errExit("PR_Close");
> }
>
> return;
>
> }
>
> int
> main(int argc, char **argv)
> {
> char * certName = NULL;
> char * certDir = ".";
> unsigned short port = 0;
> PLOptState * optstate;
> PLOptStatus status;
> SECStatus secStatus;
>
> s_progName = argv[0];
>
> optstate = PL_CreateOptState(argc, argv, "d:p:n:w:");
> while ((status = PL_GetNextOpt(optstate)) == PL_OPT_OK) {
> switch(optstate->option) {
> case 0: s_hostName = PL_strdup(optstate->value); break;
> case 'd': certDir = PL_strdup(optstate->value); break;
> case 'n': certName = PL_strdup(optstate->value); break;
> case 'p': port = atoi(optstate->value); break;
> case 'w': s_password = PL_strdup(optstate->value); break;
> case '?': Usage(s_progName); break;
> default: Usage(s_progName);
> break;
> }
> }
>
> if (certName == NULL || s_hostName == NULL || s_password == NULL ||
> port == 0) {
> Usage(s_progName);
> }
>
> // Client set up
> // -------------
>
> PR_Init( PR_SYSTEM_THREAD, PR_PRIORITY_NORMAL, 1);
>
> PK11_SetPasswordFunc(fakePasswd);
>
> secStatus = NSS_Init(certDir);
> if (secStatus != SECSuccess) {
> errExit("NSS_Init");
> }
>
> secStatus = NSS_SetExportPolicy();
> if (secStatus != SECSuccess) {
> errExit("NSS_SetExportPolicy");
> }
>
> // optional - clear client cache
> SSL_ClearSessionCache();
>
> // Client main function
> // --------------------
>
> clientMain(port);
>
> // Client shutdown
> // ---------------
>
> SSL_ClearSessionCache(); // otherwise, NSS_Shutdown fails.
> if (NSS_Shutdown() != SECSuccess) {
> errExit("NSS_Shutdown");
> }
>
> PR_Cleanup();
>
> if (certName) {
> free(certName);
> }
>
> if (certDir) {
> free(certDir);
> }
>
> if (s_hostName) {
> free(s_hostName);
> }
>
> printf("%s: normal termination\n", s_progName);
> return 0;
>
> }
>
> ====
>
> // server.c
> // --------
>
> #include <unistd.h>
> #include <pthread.h>
> #include <plgetopt.h>
> #include <nspr.h>
> #include <nss.h>
> #include <pk11pub.h>
> #include <ssl.h>
> #include <sslproto.h>
> #include <key.h>
> #include <secutil.h> // a private API needed for SECU_Strerror
>
> static char *s_progName = NULL;
>
> /*
> * errWarn()
> *
> * Print a warning message for NSS and NSPR errors.
> * More detailed explanations for the error can be found at:
> *http://www.mozilla.org/projects/security/pki/nss/ref/ssl/sslerr.html
> */
> static void
> errWarn(char * funcString)
> {
> PRErrorCode perr = PR_GetError();
> const char * errString = SECU_Strerror(perr);
> fprintf(stderr, "%s: %s returned error %d (%s)\n", s_progName,
> funcString, perr, errString);
> return;
>
> }
>
> static void
> errExit(char * funcString)
> {
> errWarn(funcString);
> exit(3);
>
> }
>
> static void
> Usage(const char *progName)
> {
> fprintf(stderr, "Usage: %s -n cert_name -p port -w password [-d
> cert_dir] \n", progName);
> exit(1);
>
> }
>
> /* fakePasswd()
> *
> * This function is our custom password handler that is called by
> * SSL when retreiving private certs and keys from the database.
> Returns a
> * pointer to a string that with a password for the database. Password
> pointer
> * should point to dynamically allocated memory that will be freed
> later.
> * We set "arg" to give the correct password, using
> SSL_SetPKCS11PinArg().
> */
> char *
> fakePasswd(PK11SlotInfo *info, PRBool retry, void *arg)
> {
> char * passwd = NULL;
>
> if ((!retry) && (arg != NULL)) {
> passwd = PL_strdup((char *)arg);
> }
>
> return passwd;
>
> }
>
> /* Function: setupSSLSocket()
> *
> * Purpose: Configure a socket for SSL.
> * NSS has 3 methods for configuring a TCP socket for SSL:
> * 1. Do the configuration step by step.
> * 2. Inherit a model socket using SSL_ImportFD.
> * 3. Do the configuration on the listen socket. This way,
> * sockets created by PR_Accept inherit the configuration.
> *
> * - this application uses method 3, so we configure the listen
> socket.
> */
> PRFileDesc *
> setupSSLSocket(PRFileDesc *tcpSocket,
> CERTCertificate *cert,
> SECKEYPrivateKey *privKey,
> SSLKEAType sslKEA,
> char* password)
> {
> PRFileDesc *sslSocket;
> SECStatus secStatus;
>
> sslSocket = SSL_ImportFD(NULL, tcpSocket);
> if (sslSocket == NULL) {
> errWarn("SSL_ImportFD");
> PR_Close(tcpSocket);
> return NULL;
> }
>
> // ensure original socket is not used
> tcpSocket = NULL;
>
> do {
>
> // handshake as server
> secStatus = SSL_OptionSet(sslSocket, SSL_HANDSHAKE_AS_SERVER,
> PR_TRUE);
> if (secStatus != SECSuccess) {
> errWarn("SSL_OptionSet:SSL_HANDSHAKE_AS_SERVER");
> break;
> }
>
> // enable full duplex
> secStatus = SSL_OptionSet(sslSocket, SSL_ENABLE_FDX, PR_TRUE);
> if (secStatus != SECSuccess) {
> errWarn("SSL_OptionSet(SSL_ENABLE_FDX)");
> break;
> }
>
> // allow communication without encryption (YZ - must be only
> upon client request)
> if (SECSuccess != SSL_CipherPrefSet(sslSocket,
> SSL_RSA_WITH_NULL_SHA, PR_TRUE)
> || SECSuccess != SSL_CipherPrefSet(sslSocket,
> SSL_RSA_WITH_NULL_MD5, PR_TRUE)) {
> errWarn("SSL_CipherPrefSet - Null Cipher");
> break;
> }
>
> /* // optional, see SSL
> reference. */
> /* secStatus = SSL_AuthCertificateHook(sslSocket,
> myAuthCertificate, CERT_GetDefaultCertDB()); */
> /* if (secStatus != SECSuccess)
> { */
> /*
> errWarn("SSL_AuthCertificateHook");
> */
> /*
> break;
> */
> /
> * }
> */
>
> /* // optional, see SSL
> reference. */
> /* secStatus =
> SSL_BadCertHook(sslSocket, */
> /*
> (SSLBadCertHandler)myBadCertHandler, &certErr); */
> /* if (secStatus != SECSuccess)
> { */
> /*
> errWarn("SSL_BadCertHook");
> */
> /*
> break;
> */
> /
> * }
> */
>
> /* // optional, see SSL
> reference. */
> /* secStatus =
> SSL_HandshakeCallback(sslSocket, */
> /*
> (SSLHandshakeCallback)myHandshakeCallback, */
> /*
> NULL); */
> /* if (secStatus != SECSuccess)
> { */
> /*
> errWarn("SSL_HandshakeCallback");
> */
> /*
> break;
> */
> /
> * }
> */
>
> secStatus = SSL_SetPKCS11PinArg(sslSocket, password);
> if (secStatus != SECSuccess) {
> errWarn("SSL_HandshakeCallback");
> break;
> }
>
> secStatus = SSL_ConfigSecureServer(sslSocket, cert, privKey,
> sslKEA);
> if (secStatus != SECSuccess) {
> errWarn("SSL_ConfigSecureServer");
> break;
> }
>
> // Success
> return sslSocket;
>
> } while (0);
>
> // Failure
> PR_Close(sslSocket);
> return NULL;
>
> }
>
> void* delayed_close(void *pSocket)
> {
> PRFileDesc *socket = (PRFileDesc *) pSocket;
> sleep(1);
> PR_Shutdown(socket, PR_SHUTDOWN_BOTH);
> PR_Close(socket);
> printf("\n" "socket closed .\n");
> return NULL;
>
> }
>
> /* Function: handleConnection()
> *
> * Purpose: handle a single SSL connection.
> *
> */
> SECStatus
> handleConnection(PRFileDesc *pSocket)
> {
> PRFileDesc *sslSocket = (PRFileDesc *) pSocket;
> PRStatus prStatus;
> PRSocketOptionData socketOption;
> char buffer[256];
> PRInt32 rv;
>
> // ensure the socket is blocking. this should be the default.
> socketOption.option = PR_SockOpt_Nonblocking;
> socketOption.value.non_blocking = PR_FALSE;
> PR_SetSocketOption(sslSocket, &socketOption);
>
> /* // handshake as server - required in addition to the SSL
> option, */
> /* // in case the listen socket was not an SSL
> socket. */
> /* secStatus = SSL_ResetHandshake(sslSocket,
> PR_TRUE ); */
> /* if (secStatus != SECSuccess)
> { */
> /*
> errWarn("SSL_ResetHandshake"); */
> /* return
> secStatus; */
> /
> * }
> */
>
> pthread_t pth;
> pthread_create(&pth, NULL, delayed_close, sslSocket);
>
> // use socket: single read, then single write
>
> rv = PR_Read(sslSocket, buffer, sizeof(buffer));
> // (rv == 0) is EOF
> if (rv <= 0) {
> errWarn("PR_Read");
> PR_Close(sslSocket);
> return SECFailure;
> }
>
> buffer[rv] = 0;
> printf("server received: %s \n", buffer);
>
> sprintf(buffer, "hello from server");
> int msgSize = strlen(buffer);
>
> rv = PR_Write(sslSocket, buffer, msgSize);
> if (rv != msgSize) {
> errWarn("PR_Write");
> PR_Close(sslSocket);
> return SECFailure;
> }
>
> printf("server sent: %s \n", buffer);
>
> // Finally
> // -------
>
> printf("\n" "Closing client connection.\n");
>
> prStatus = PR_Close(sslSocket);
> if (prStatus != PR_SUCCESS) {
> errWarn("PR_Close");
> return SECFailure;
> }
>
> return SECSuccess;
>
> }
>
> /* Function: startListening()
> *
> * Purpose: Create a new socket and starts listening.
> */
>
> PRFileDesc *
> startListening(unsigned short port)
> {
> PRFileDesc * listen_sock;
> PRStatus prStatus;
> PRNetAddr addr;
> PRSocketOptionData opt;
>
> addr.inet.family = PR_AF_INET;
> addr.inet.ip = PR_INADDR_ANY;
> addr.inet.port = PR_htons(port);
>
> listen_sock = PR_NewTCPSocket();
> if (listen_sock == NULL) {
> errExit("PR_NewTCPSocket");
> }
>
> // YZ set blocking mode. this should be the default anyway.
> opt.option = PR_SockOpt_Nonblocking;
> opt.value.non_blocking = PR_FALSE;
> prStatus = PR_SetSocketOption(listen_sock, &opt);
> if (prStatus < 0) {
> errExit("PR_SetSocketOption(PR_SockOpt_Nonblocking =
> PR_FALSE)");
> }
>
> opt.option=PR_SockOpt_Reuseaddr;
> opt.value.reuse_addr = PR_TRUE;
> prStatus = PR_SetSocketOption(listen_sock, &opt);
> if (prStatus < 0) {
> errExit("PR_SetSocketOption(PR_SockOpt_Reuseaddr = PR_TRUE)");
> }
>
> prStatus = PR_Bind(listen_sock, &addr);
> if (prStatus < 0) {
> errExit("PR_Bind");
> }
>
> prStatus = PR_Listen(listen_sock, 5);
> if (prStatus < 0) {
> errExit("PR_Listen");
> }
>
> return listen_sock;
>
> }
>
> /* Function: serverMain()
> *
> * Purpose:
> * Loop to accept connections. For every connection,
> * receive a message and then send a reply message.
> * Since listenSocket is an SSL socket, tcpSocket is an SSL socket
> too.
> */
> void
> serverMain(PRFileDesc *listenSocket)
> {
> PRNetAddr addr;
> PRStatus prStatus;
>
> while (1) {
> PRFileDesc *tcpSocket;
>
> printf("\n" "Waiting for new connection.\n");
>
> /* Accept a connection */
> tcpSocket = PR_Accept(listenSocket, &addr,
> PR_INTERVAL_NO_TIMEOUT);
> if (tcpSocket == NULL) {
> errWarn("PR_Accept");
> break;
> }
>
> // Handle one connection at a time.
> // Can be replaced with a new thread
> pthread_t pth;
> pthread_create(&pth, NULL, handleConnection, tcpSocket);
> }
>
> printf("\n" "Closing listen socket.\n");
>
> prStatus = PR_Close(listenSocket);
> if (prStatus != PR_SUCCESS) {
> errWarn("PR_Close");
> }
>
> return;
>
> }
>
> int
> main(int argc, char **argv)
> {
> char * password = NULL;
> char * certName = NULL;
> char * certDir = ".";
> char * cipherString = NULL;
> unsigned short port = 0;
> PLOptState * optstate;
> PLOptStatus status;
> SECStatus secStatus;
> CERTCertificate *cert = NULL;
> SSLKEAType sslKEA;
> SECKEYPrivateKey *privKey = NULL;
> PRFileDesc * listenSocket;
> PRFileDesc * sslSocket;
>
> s_progName = argv[0];
>
> optstate = PL_CreateOptState(argc, argv, "c:d:p:n:w:");
> while ((status = PL_GetNextOpt(optstate)) == PL_OPT_OK) {
> switch(optstate->option) {
> case 'c': cipherString = PL_strdup(optstate->value); break;
> case 'd': certDir = PL_strdup(optstate->value); break;
> case 'n': certName = PL_strdup(optstate->value); break;
> case 'p': port = atoi(optstate->value); break;
> case 'w': password = PL_strdup(optstate->value); break;
> case 0: break;
> case '?': Usage(s_progName); break;
> default: Usage(s_progName);
> break;
> }
> }
>
> if (certName == NULL || password == NULL || port == 0) {
> Usage(s_progName);
> }
>
> // Server set up
> // -------------
>
> PR_Init( PR_SYSTEM_THREAD, PR_PRIORITY_NORMAL, 1);
>
> PK11_SetPasswordFunc(fakePasswd);
>
> secStatus = NSS_Init(certDir);
> if (secStatus != SECSuccess) {
> errExit("NSS_Init");
> }
>
> // Using export policy.
> secStatus = NSS_SetExportPolicy();
> if (secStatus != SECSuccess) {
> errExit("NSS_SetExportPolicy");
> }
>
> /* Get own certificate and private key. */
> cert = PK11_FindCertFromNickname(certName, password);
> if (cert == NULL) {
> errExit("PK11_FindCertFromNickname");
> }
>
> sslKEA = NSS_FindCertKEAType(cert);
> if (sslKEA == kt_null) {
> errExit("NSS_FindCertKEAType");
> }
>
> privKey = PK11_FindKeyByAnyCert(cert, password);
> if (privKey == NULL) {
> errExit("PK11_FindKeyByAnyCert");
> }
> // Set a session cache for single-process server.
> secStatus = SSL_ConfigServerSessionIDCache(100, 0, 0, 0);
> if (secStatus != SECSuccess) {
> errExit("SSL_ConfigServerSessionIDCache");
> }
>
> listenSocket = startListening(port);
> if (listenSocket == NULL) {
> errExit("startListening");
> }
>
> sslSocket = setupSSLSocket(listenSocket, cert, privKey, sslKEA,
> password);
> if (sslSocket == NULL) {
> errExit("setupSSLSocket");
> }
>
> // Main server loop
> // ----------------
>
> serverMain(sslSocket);
>
> // Server shutdown
> // ---------------
>
> if (cert) {
> CERT_DestroyCertificate(cert);
> }
>
> if (privKey) {
> SECKEY_DestroyPrivateKey(privKey);
> }
>
> SSL_ShutdownServerSessionIDCache();
>
> if (NSS_Shutdown() != SECSuccess) {
> errExit("NSS_Shutdown");
> }
>
> PR_Cleanup();
>
> if (certName) {
> free(certName);
> }
>
> if (certDir) {
> free(certDir);
> }
>
> if (password) {
> free(password);
> }
>
> printf("%s: normal termination\n", s_progName);
> return 0;
>
> }
_______________________________________________
dev-tech-crypto mailing list
[email protected]
https://lists.mozilla.org/listinfo/dev-tech-crypto