Hi Wan-Teh Chang,
Please see some replies below.
Wan-Teh Chang wrote:
Yahel Zamir wrote:
1.
When writing an application that uses NSS, I would expect all Platform
Dependent configuration to reside in some configuration file, so that
the build process of the whole application will not need to be aware
of these details. However, the platform name and other properties
appear again and again in the Include path, the Library search path
and in command line defines, as you can see below - the command is
copied from running "make" in SSLsample, and contains many references
to Linux, Unix, x86 etc. Am I missing something here?
... -DLINUX1_2 -Di386 -D_XOPEN_SOURCE -DLINUX2_1 -ansi -Wall -pipe
-DHAVE_STRERROR -DLINUX -Dlinux -D_POSIX_SOURCE -D_BSD_SOURCE-DXP_UNIX
-DDEBUG -UNDEBUG -DDEBUG_yahel -D_REENTRANT
-I../../../../dist/Linux2.4_x86_glibc_PTH_DBG.OBJ/include
-I../../../../dist/public/nss -I../../../../dist/private/nss
Linux2.4_x86_glibc_PTH_DBG.OBJ/server.o
Linux2.4_x86_glibc_PTH_DBG.OBJ/sslsample.o
../../../../dist/Linux2.4_x86_glibc_PTH_DBG.OBJ/lib/libsectool.a
-Wl,-rpath,'$ORIGIN/../lib'
-Wl,-rpath-link,../../../../dist/Linux2.4_x86_glibc_PTH_DBG.OBJ/lib
-L../../../../dist/Linux2.4_x86_glibc_PTH_DBG.OBJ/lib -lssl3 -lsmime3
-lnss3 -L../../../../dist/Linux2.4_x86_glibc_PTH_DBG.OBJ/lib -lplc4
-lplds4 -lnspr4 -lpthread -ldl -lc
We didn't write good stand-alone makefiles for SSLsample.
Instead, SSLsample's makefiles include common makefiles that
we use to build NSS itself. This is why you see a lot of
configuration macros on the compiler command line, that are
not really necessary for an application that uses NSS. Sorry
about the confusion.
To compile NSS, you just need to use the platform-dependent
flags for compiling a multithreaded application. This flag
is usually called -pthread or -pthreads on Unix platforms.
This requirement is actually for the NSPR library, which NSS
depends on.
As a reference, here are the flags we use to compile and link
an application that uses NSS on Linux:
$ nss-config --cflags
-I/usr/include/nss3
$ nss-config --libs
-Wl,-rpath-link,/usr/lib -L/usr/lib -lssl3 -lsmime3 -lnss3
You should replace /usr/include/nss3 and /usr/lib by the directories
where you installed the NSS headers and libraries.
Similarly the flags for an application that uses NSPR on Linux:
$ nspr-config --cflags
-I/usr/include/nspr4
$ nspr-config --libs
-L/usr/lib -lplds4 -lplc4 -lnspr4 -lpthread -ldl
Yes, it seems that an application does not need any specific library or
-D flag, except -pthread. I used just that, and my applications had no
trouble yet.
2.
How can I build using only static libraries?
This is no longer possible. You can get very close, by
imitating how we build the commands in mozilla/security/nss/cmd/xxx
where we set USE_STATIC_LIBS=1 in the manifest.mn file. But even
in that case, you still need the shared library libfreebl3.so, so
it's not 100% static libraries.
(On some Unix platforms we have multiple libfreebl_xxx_3.so's, one
for each version of the processor.)
Note that we discourage the use of NSS static libraries and don't
officially support it.
OK.
3.
How should an application generate error strings? The examples at
SSLsample do a very strange trick. On the other hand, selfserv on my
machine simply finds no error string (for example, when using a bad
certificate database, error "-8174").
I'm afraid that right now each application needs to build its own
NSS error string table. You can use the error string table for the
NSS command-line tools as an example:
http://lxr.mozilla.org/security/source/security/nss/cmd/lib/secerror.c
I see. Actually, this is what SSLsample does.
4.
It is explained that we can either configure a listen socket for SSL
using SSL_ConfigSecureServer(), or listen in the regular manner and
then configure enable SSL on the socket created by PR_Accept(). Is
SSL_ConfigSecureServer() just an elegant way to enable SSL on all
sockets created from some listen socket, and nothing else? Does it add
any security?
I'll let Nelson answer this question.
OK
5.
Is there a simple example of an NSS client and server? I understand
that the SSLsample example are not considered good enough. However,
selfserv and strsclnt are rather complicated, since they handle
numerous configuration options.
Unfortunately no.
Anyone have a simple NSS client and server?
I have written a simplistic client and server,
based on SSLsample and selfserv/strsclnt.
You are welcome to use it if you find it useful.
6.
bug report - in function setupSSLSocket() of SSLsample/client.c, any
failure leads to closing tcpSocket, even after SSL_ImportFD(), which
replaces tcpSocket by sslSocket.
Thank you for the bug report. Would you have time to file it in
bugzilla.mozilla.org? If not, I can do it for you.
Wan-Teh
I am not sure there is a bug here, since Nelson disagrees (see his message).
Yahel.
#include <plgetopt.h>
#include <nspr.h>
#include <nss.h>
#include <pk11pub.h>
#include <ssl.h>
#include <sslproto.h>
#include <key.h>
static char *s_progName = NULL;
static void
errWarn(char * funcString)
{
PRErrorCode perr = PR_GetError();
fprintf(stderr, "%s: %s returned error %d\n", s_progName, funcString, perr);
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;
}
/* Function: handleConnection()
*
* Purpose: handle a single SSL connection.
*
*/
SECStatus
handleConnection(PRFileDesc *sslSocket)
{
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; */
/* } */
// 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
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;
}
M_ROOT=/usr/home/yahel/programs/nss/mozilla
M_DIST=${M_ROOT}/dist/Linux2.4_x86_glibc_PTH_DBG.OBJ
M_INC=-I${M_DIST}/include -I${M_ROOT}/dist/public/nss
M_FLAGS = -g -Wall
M_LIBS = -L${M_DIST}/lib -lssl3 -lnss3 -lnspr4
all: server client
@echo done
clean:
rm -f client server client.o server.o
server.o: server.c
gcc -c ${M_FLAGS} ${M_INC} server.c -o server.o
client.o: client.c
gcc -c ${M_FLAGS} ${M_INC} client.c -o client.o
server: server.o
gcc ${M_FLAGS} ${M_INC} ${M_LIBS} server.o -o server
client: client.o
gcc ${M_FLAGS} ${M_INC} ${M_LIBS} client.o -o client
#include <plgetopt.h>
#include <nspr.h>
#include <nss.h>
#include <pk11pub.h>
#include <ssl.h>
#include <sslproto.h>
#include <key.h>
static char *s_progName = NULL;
static char *s_password = NULL;
static char *s_hostName = NULL;
static void
errWarn(char * funcString)
{
PRErrorCode prError = PR_GetError();
fprintf(stderr, "%s: %s returned %d\n", s_progName, funcString, prError);
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);
}
/* myPasswd()
*
* 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);
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;
}
_______________________________________________
dev-tech-crypto mailing list
dev-tech-crypto@lists.mozilla.org
https://lists.mozilla.org/listinfo/dev-tech-crypto