/* 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 "seccomon.h" #include "nss.h" #include "key.h" #include "cert.h" #include "pk11func.h" #include "secmod.h" #include "cmmf.h" #include "crmf.h" #include "base64.h" #include "secasn1.h" #include "cryptohi.h" #include #include #include #define DEFAULT_ALLOC_SIZE 200 #define DEFAULT_CGI_VARS 20 typedef struct CGIVariableStr { char *name; char *value; } CGIVariable; typedef struct CGIVarTableStr { CGIVariable **variables; int numVars; int numAlloc; } CGIVarTable; typedef struct CertResponseInfoStr { CERTCertificate *cert; long certReqID; } CertResponseInfo; typedef struct ChallengeCreationInfoStr { long random; SECKEYPublicKey *pubKey; } ChallengeCreationInfo; char *missingVar = NULL; /* * Error values. */ typedef enum { NO_ERROR = 0, NSS_INIT_FAILED, AUTH_FAILED, REQ_CGI_VAR_NOT_PRESENT, CRMF_REQ_NOT_PRESENT, BAD_ASCII_FOR_REQ, CGI_VAR_MISSING, COULD_NOT_FIND_CA, COULD_NOT_DECODE_REQS, OUT_OF_MEMORY, ERROR_RETRIEVING_REQUEST_MSG, ERROR_RETRIEVING_CERT_REQUEST, ERROR_RETRIEVING_SUBJECT_FROM_REQ, ERROR_RETRIEVING_PUBLIC_KEY_FROM_REQ, ERROR_CREATING_NEW_CERTIFICATE, COULD_NOT_START_EXTENSIONS, ERROR_RETRIEVING_EXT_FROM_REQ, ERROR_ADDING_EXT_TO_CERT, ERROR_ENDING_EXTENSIONS, COULD_NOT_FIND_ISSUER_PRIVATE_KEY, UNSUPPORTED_SIGN_OPERATION_FOR_ISSUER, ERROR_SETTING_SIGN_ALG, ERROR_ENCODING_NEW_CERT, ERROR_SIGNING_NEW_CERT, ERROR_CREATING_CERT_REP_CONTENT, ERROR_CREATING_SINGLE_CERT_RESPONSE, ERROR_SETTING_CERT_RESPONSES, ERROR_CREATING_CA_LIST, ERROR_ADDING_ISSUER_TO_CA_LIST, ERROR_ENCODING_CERT_REP_CONTENT, NO_POP_FOR_REQUEST, UNSUPPORTED_POP, ERROR_RETRIEVING_POP_SIGN_KEY, ERROR_RETRIEVING_ALG_ID_FROM_SIGN_KEY, ERROR_RETRIEVING_SIGNATURE_FROM_POP_SIGN_KEY, DO_CHALLENGE_RESPONSE, ERROR_RETRIEVING_PUB_KEY_FROM_NEW_CERT, ERROR_ENCODING_CERT_REQ_FOR_POP, ERROR_VERIFYING_SIGNATURE_POP, ERROR_RETRIEVING_PUB_KEY_FOR_CHALL, ERROR_CREATING_EMPTY_CHAL_CONTENT, ERROR_EXTRACTING_GEN_NAME_FROM_ISSUER, ERROR_SETTING_CHALLENGE, ERROR_ENCODING_CHALL, ERROR_CONVERTING_CHALL_TO_BASE64, ERROR_CONVERTING_RESP_FROM_CHALL_TO_BIN, ERROR_CREATING_KEY_RESP_FROM_DER, ERROR_RETRIEVING_CLIENT_RESPONSE_TO_CHALLENGE, ERROR_RETURNED_CHALL_NOT_VALUE_EXPECTED, ERROR_GETTING_KEY_ENCIPHERMENT, ERROR_NO_POP_FOR_PRIVKEY, ERROR_UNSUPPORTED_POPOPRIVKEY_TYPE } ErrorCode; const char * CGITableFindValue(CGIVarTable *varTable, const char *key); void spitOutHeaders(void) { printf("Content-type: text/html\n\n"); } void dumpRequest(CGIVarTable *varTable) { int i; CGIVariable *var; printf ("\n"); printf ("" "\n"); for (i=0; inumVars; i++) { var = varTable->variables[i]; printf ("\n", var->name, var->value); } printf("
Variable Name
Value
%s
%s
\n"); } void echo_request(CGIVarTable *varTable) { spitOutHeaders(); printf("CGI Echo Page\n" "

Got the following request

\n"); dumpRequest(varTable); printf(""); } void processVariable(CGIVariable *var) { char *plusSign, *percentSign; /*First look for all of the '+' and convert them to spaces */ plusSign = var->value; while ((plusSign=strchr(plusSign, '+')) != NULL) { *plusSign = ' '; } percentSign = var->value; while ((percentSign=strchr(percentSign, '%')) != NULL) { char string[3]; int value; string[0] = percentSign[1]; string[1] = percentSign[2]; string[2] = '\0'; sscanf(string,"%x", &value); *percentSign = (char)value; memmove(&percentSign[1], &percentSign[3], 1+strlen(&percentSign[3])); } } char * parseNextVariable(CGIVarTable *varTable, char *form_output) { char *ampersand, *equal; CGIVariable *var; if (varTable->numVars == varTable->numAlloc) { CGIVariable **newArr = realloc(varTable->variables, (varTable->numAlloc + DEFAULT_CGI_VARS)*sizeof(CGIVariable*)); if (newArr == NULL) { return NULL; } varTable->variables = newArr; varTable->numAlloc += DEFAULT_CGI_VARS; } equal = strchr(form_output, '='); if (equal == NULL) { return NULL; } ampersand = strchr(equal, '&'); if (ampersand == NULL) { return NULL; } equal[0] = '\0'; if (ampersand != NULL) { ampersand[0] = '\0'; } var = malloc(sizeof(CGIVariable)); var->name = form_output; var->value = &equal[1]; varTable->variables[varTable->numVars] = var; varTable->numVars++; processVariable(var); return (ampersand != NULL) ? &ersand[1] : NULL; } void ParseInputVariables(CGIVarTable *varTable, char *form_output) { varTable->variables = malloc(sizeof(CGIVariable*)*DEFAULT_CGI_VARS); varTable->numVars = 0; varTable->numAlloc = DEFAULT_CGI_VARS; while (form_output && form_output[0] != '\0') { form_output = parseNextVariable(varTable, form_output); } } const char * CGITableFindValue(CGIVarTable *varTable, const char *key) { const char *retVal = NULL; int i; for (i=0; inumVars; i++) { if (strcmp(varTable->variables[i]->name, key) == 0) { retVal = varTable->variables[i]->value; break; } } return retVal; } char* passwordCallback(PK11SlotInfo *slot, PRBool retry, void *arg) { const char *passwd; if (retry) { return NULL; } passwd = CGITableFindValue((CGIVarTable*)arg, "dbPassword"); if (passwd == NULL) { return NULL; } return PORT_Strdup(passwd); } ErrorCode initNSS(CGIVarTable *varTable) { const char *nssDir; PK11SlotInfo *keySlot; SECStatus rv; nssDir = CGITableFindValue(varTable,"NSSDirectory"); if (nssDir == NULL) { missingVar = "NSSDirectory"; return REQ_CGI_VAR_NOT_PRESENT; } rv = NSS_Init(nssDir); if (rv != SECSuccess) { return NSS_INIT_FAILED; } PK11_SetPasswordFunc(passwordCallback); keySlot = PK11_GetInternalKeySlot(); rv = PK11_Authenticate(keySlot, PR_FALSE, varTable); PK11_FreeSlot(keySlot); if (rv != SECSuccess) { return AUTH_FAILED; } return NO_ERROR; } void dumpErrorMessage(ErrorCode errNum) { spitOutHeaders(); printf("Error

Error processing " "data

Received the error %d

", errNum); if (errNum == REQ_CGI_VAR_NOT_PRESENT) { printf ("The missing variable is %s.", missingVar); } printf ("More useful information here in the future."); } ErrorCode initOldCertReq(CERTCertificateRequest *oldCertReq, CERTName *subject, CERTSubjectPublicKeyInfo *spki) { PLArenaPool *poolp; poolp = oldCertReq->arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE); SEC_ASN1EncodeInteger(poolp, &oldCertReq->version, SEC_CERTIFICATE_VERSION_3); CERT_CopyName(poolp, &oldCertReq->subject, subject); SECKEY_CopySubjectPublicKeyInfo(poolp, &oldCertReq->subjectPublicKeyInfo, spki); oldCertReq->attributes = NULL; return NO_ERROR; } ErrorCode addExtensions(CERTCertificate *newCert, CRMFCertRequest *certReq) { int numExtensions, i; void *extHandle; ErrorCode rv = NO_ERROR; CRMFCertExtension *ext; SECStatus srv; numExtensions = CRMF_CertRequestGetNumberOfExtensions(certReq); if (numExtensions == 0) { /* No extensions to add */ return NO_ERROR; } extHandle = CERT_StartCertExtensions(newCert); if (extHandle == NULL) { rv = COULD_NOT_START_EXTENSIONS; goto loser; } for (i=0; idata, der->len); PR_Close(outfile); } ErrorCode createNewCert(CERTCertificate**issuedCert,CERTCertificateRequest *oldCertReq, CRMFCertReqMsg *currReq, CRMFCertRequest *certReq, CERTCertificate *issuerCert, CGIVarTable *varTable) { CERTCertificate *newCert = NULL; CERTValidity *validity; PRExplodedTime printableTime; PRTime now, after; ErrorCode rv=NO_ERROR; SECKEYPrivateKey *issuerPrivKey; SECItem derCert = { 0 }; SECOidTag signTag; SECStatus srv; long version; now = PR_Now(); PR_ExplodeTime(now, PR_GMTParameters, &printableTime); printableTime.tm_month += 9; after = PR_ImplodeTime(&printableTime); validity = CERT_CreateValidity(now, after); newCert = *issuedCert = CERT_CreateCertificate(rand(), &(issuerCert->subject), validity, oldCertReq); if (newCert == NULL) { rv = ERROR_CREATING_NEW_CERTIFICATE; goto loser; } rv = addExtensions(newCert, certReq); if (rv != NO_ERROR) { goto loser; } issuerPrivKey = PK11_FindKeyByAnyCert(issuerCert, varTable); if (issuerPrivKey == NULL) { rv = COULD_NOT_FIND_ISSUER_PRIVATE_KEY; } signTag = SEC_GetSignatureAlgorithmOidTag(issuerPrivatekey->keytype, SEC_OID_UNKNOWN); if (signTag == SEC_OID_UNKNOWN) { rv = UNSUPPORTED_SIGN_OPERATION_FOR_ISSUER; goto loser; } srv = SECOID_SetAlgorithmID(newCert->arena, &newCert->signature, signTag, 0); if (srv != SECSuccess) { rv = ERROR_SETTING_SIGN_ALG; goto loser; } srv = CRMF_CertRequestGetCertTemplateVersion(certReq, &version); if (srv != SECSuccess) { /* No version included in the request */ *(newCert->version.data) = SEC_CERTIFICATE_VERSION_3; } else { SECITEM_FreeItem(&newCert->version, PR_FALSE); SEC_ASN1EncodeInteger(newCert->arena, &newCert->version, version); } SEC_ASN1EncodeItem(newCert->arena, &derCert, newCert, CERT_CertificateTemplate); if (derCert.data == NULL) { rv = ERROR_ENCODING_NEW_CERT; goto loser; } srv = SEC_DerSignData(newCert->arena, &(newCert->derCert), derCert.data, derCert.len, issuerPrivKey, signTag); if (srv != SECSuccess) { rv = ERROR_SIGNING_NEW_CERT; goto loser; } #ifdef WRITE_OUT_RESPONSE writeOutItem("newcert.der", &newCert->derCert); #endif return NO_ERROR; loser: *issuedCert = NULL; if (newCert) { CERT_DestroyCertificate(newCert); } return rv; } void formatCMMFResponse(char *nickname, char *base64Response) { char *currLine, *nextLine; printf("var retVal = crypto.importUserCertificates(\"%s\",\n", nickname); currLine = base64Response; while (1) { nextLine = strchr(currLine, '\n'); if (nextLine == NULL) { /* print out the last line here. */ printf ("\"%s\",\n", currLine); break; } nextLine[0] = '\0'; printf("\"%s\\n\"+\n", currLine); currLine = nextLine+1; } printf("true);\n" "if(retVal == '') {\n" "\tdocument.write(\"

New Certificate Successfully Imported.

\");\n" "} else {\n" "\tdocument.write(\"

Unable to import New Certificate

\");\n" "\tdocument.write(\"crypto.importUserCertificates returned \");\n" "\tdocument.write(retVal);\n" "\tdocument.write(\"\");\n" "}\n"); } void spitOutCMMFResponse(char *nickname, char *base64Response) { spitOutHeaders(); printf("\n\nCMMF Resonse Page\n\n\n" "

CMMF Response Page

\n" "\n\n"); } char* getNickname(CERTCertificate *cert) { char *nickname; if (cert->nickname != NULL) { return cert->nickname; } nickname = CERT_GetCommonName(&cert->subject); if (nickname != NULL) { return nickname; } return CERT_NameToAscii(&cert->subject); } ErrorCode createCMMFResponse(CertResponseInfo *issuedCerts, int numCerts, CERTCertificate *issuerCert, char **base64der) { CMMFCertRepContent *certRepContent=NULL; ErrorCode rv = NO_ERROR; CMMFCertResponse **responses, *currResponse; CERTCertList *caList; int i; SECStatus srv; PLArenaPool *poolp; SECItem *der; certRepContent = CMMF_CreateCertRepContent(); if (certRepContent == NULL) { rv = ERROR_CREATING_CERT_REP_CONTENT; goto loser; } responses = PORT_NewArray(CMMFCertResponse*, numCerts); if (responses == NULL) { rv = OUT_OF_MEMORY; goto loser; } for (i=0; idata, der->len); return NO_ERROR; loser: return rv; } ErrorCode issueCerts(CertResponseInfo *issuedCerts, int numCerts, CERTCertificate *issuerCert) { ErrorCode rv; char *base64Response; rv = createCMMFResponse(issuedCerts, numCerts, issuerCert, &base64Response); if (rv != NO_ERROR) { goto loser; } spitOutCMMFResponse(getNickname(issuedCerts[0].cert),base64Response); return NO_ERROR; loser: return rv; } ErrorCode verifySignature(CGIVarTable *varTable, CRMFCertReqMsg *currReq, CRMFCertRequest *certReq, CERTCertificate *newCert) { SECStatus srv; ErrorCode rv = NO_ERROR; CRMFPOPOSigningKey *signKey = NULL; SECAlgorithmID *algID = NULL; SECItem *signature = NULL; SECKEYPublicKey *pubKey = NULL; SECItem *reqDER = NULL; srv = CRMF_CertReqMsgGetPOPOSigningKey(currReq, &signKey); if (srv != SECSuccess || signKey == NULL) { rv = ERROR_RETRIEVING_POP_SIGN_KEY; goto loser; } algID = CRMF_POPOSigningKeyGetAlgID(signKey); if (algID == NULL) { rv = ERROR_RETRIEVING_ALG_ID_FROM_SIGN_KEY; goto loser; } signature = CRMF_POPOSigningKeyGetSignature(signKey); if (signature == NULL) { rv = ERROR_RETRIEVING_SIGNATURE_FROM_POP_SIGN_KEY; goto loser; } /* Make the length the number of bytes instead of bits */ signature->len = (signature->len+7)/8; pubKey = CERT_ExtractPublicKey(newCert); if (pubKey == NULL) { rv = ERROR_RETRIEVING_PUB_KEY_FROM_NEW_CERT; goto loser; } reqDER = SEC_ASN1EncodeItem(NULL, NULL, certReq, CRMFCertRequestTemplate); if (reqDER == NULL) { rv = ERROR_ENCODING_CERT_REQ_FOR_POP; goto loser; } srv = VFY_VerifyDataWithAlgorithmID(reqDER->data, reqDER->len, pubKey, signature, &algID->algorithm, NULL, varTable); if (srv != SECSuccess) { rv = ERROR_VERIFYING_SIGNATURE_POP; goto loser; } /* Fall thru in successfull case. */ loser: if (pubKey != NULL) { SECKEY_DestroyPublicKey(pubKey); } if (reqDER != NULL) { SECITEM_FreeItem(reqDER, PR_TRUE); } if (signature != NULL) { SECITEM_FreeItem(signature, PR_TRUE); } if (algID != NULL) { SECOID_DestroyAlgorithmID(algID, PR_TRUE); } if (signKey != NULL) { CRMF_DestroyPOPOSigningKey(signKey); } return rv; } ErrorCode doChallengeResponse(CGIVarTable *varTable, CRMFCertReqMsg *currReq, CRMFCertRequest *certReq, CERTCertificate *newCert, ChallengeCreationInfo *challs, int *numChall) { CRMFPOPOPrivKey *privKey = NULL; CRMFPOPOPrivKeyChoice privKeyChoice; SECStatus srv; ErrorCode rv = NO_ERROR; srv = CRMF_CertReqMsgGetPOPKeyEncipherment(currReq, &privKey); if (srv != SECSuccess || privKey == NULL) { rv = ERROR_GETTING_KEY_ENCIPHERMENT; goto loser; } privKeyChoice = CRMF_POPOPrivKeyGetChoice(privKey); CRMF_DestroyPOPOPrivKey(privKey); switch (privKeyChoice) { case crmfSubsequentMessage: challs = &challs[*numChall]; challs->random = rand(); challs->pubKey = CERT_ExtractPublicKey(newCert); if (challs->pubKey == NULL) { rv = ERROR_RETRIEVING_PUB_KEY_FOR_CHALL; goto loser; } (*numChall)++; rv = DO_CHALLENGE_RESPONSE; break; case crmfThisMessage: /* There'd better be a PKIArchiveControl in this message */ if (!CRMF_CertRequestIsControlPresent(certReq, crmfPKIArchiveOptionsControl)) { rv = ERROR_NO_POP_FOR_PRIVKEY; goto loser; } break; default: rv = ERROR_UNSUPPORTED_POPOPRIVKEY_TYPE; goto loser; } loser: return rv; } ErrorCode doProofOfPossession(CGIVarTable *varTable, CRMFCertReqMsg *currReq, CRMFCertRequest *certReq, CERTCertificate *newCert, ChallengeCreationInfo *challs, int *numChall) { CRMFPOPChoice popChoice; ErrorCode rv = NO_ERROR; popChoice = CRMF_CertReqMsgGetPOPType(currReq); if (popChoice == crmfNoPOPChoice) { rv = NO_POP_FOR_REQUEST; goto loser; } switch (popChoice) { case crmfSignature: rv = verifySignature(varTable, currReq, certReq, newCert); break; case crmfKeyEncipherment: rv = doChallengeResponse(varTable, currReq, certReq, newCert, challs, numChall); break; case crmfRAVerified: case crmfKeyAgreement: default: rv = UNSUPPORTED_POP; goto loser; } loser: return rv; } void convertB64ToJS(char *base64) { int i; for (i=0; base64[i] != '\0'; i++) { if (base64[i] == '\n') { printf ("\\n"); }else { printf ("%c", base64[i]); } } } void formatChallenge(char *chall64, char *certRepContentDER, ChallengeCreationInfo *challInfo, int numChalls) { printf ("function respondToChallenge() {\n" " var chalForm = document.chalForm;\n\n" " chalForm.CertRepContent.value = '"); convertB64ToJS(certRepContentDER); printf ("';\n" " chalForm.ChallResponse.value = crypto.popChallengeResponse('"); convertB64ToJS(chall64); printf("');\n" " chalForm.submit();\n" "}\n"); } void spitOutChallenge(char *chall64, char *certRepContentDER, ChallengeCreationInfo *challInfo, int numChalls, char *nickname) { int i; spitOutHeaders(); printf("\n" "\n" "Challenge Page\n" "\n" "\n" "\n" "

Cartman is now responding to the Challenge " "presented by the CGI

\n" "
\n" "\n" "\n"); for (i=0;i\n", i+1, challInfo[i].random); } printf("\n", nickname); printf("\n\n"); } ErrorCode issueChallenge(CertResponseInfo *issuedCerts, int numCerts, ChallengeCreationInfo *challInfo, int numChalls, CERTCertificate *issuer, CGIVarTable *varTable) { ErrorCode rv = NO_ERROR; CMMFPOPODecKeyChallContent *chalContent = NULL; int i; SECStatus srv; PLArenaPool *poolp; CERTGeneralName *genName; SECItem *challDER = NULL; char *chall64, *certRepContentDER; rv = createCMMFResponse(issuedCerts, numCerts, issuer, &certRepContentDER); if (rv != NO_ERROR) { goto loser; } chalContent = CMMF_CreatePOPODecKeyChallContent(); if (chalContent == NULL) { rv = ERROR_CREATING_EMPTY_CHAL_CONTENT; goto loser; } poolp = PORT_NewArena(1024); if (poolp == NULL) { rv = OUT_OF_MEMORY; goto loser; } genName = CERT_GetCertificateNames(issuer, poolp); if (genName == NULL) { rv = ERROR_EXTRACTING_GEN_NAME_FROM_ISSUER; goto loser; } for (i=0;idata, challDER->len); SECITEM_FreeItem(challDER, PR_TRUE); if (chall64 == NULL) { rv = ERROR_CONVERTING_CHALL_TO_BASE64; goto loser; } spitOutChallenge(chall64, certRepContentDER, challInfo, numChalls, getNickname(issuedCerts[0].cert)); loser: return rv; } ErrorCode processRequest(CGIVarTable *varTable) { CERTCertDBHandle *certdb; SECKEYKeyDBHandle *keydb; CRMFCertReqMessages *certReqs = NULL; const char *crmfReq; const char *caNickname; CERTCertificate *caCert = NULL; CertResponseInfo *issuedCerts = NULL; CERTSubjectPublicKeyInfo spki = { 0 }; ErrorCode rv=NO_ERROR; PRBool doChallengeResponse = PR_FALSE; SECItem der = { 0 }; SECStatus srv; CERTCertificateRequest oldCertReq = { 0 }; CRMFCertReqMsg **reqMsgs = NULL,*currReq = NULL; CRMFCertRequest **reqs = NULL, *certReq = NULL; CERTName subject = { 0 }; int numReqs,i; ChallengeCreationInfo *challInfo=NULL; int numChalls = 0; certdb = CERT_GetDefaultCertDB(); keydb = SECKEY_GetDefaultKeyDB(); crmfReq = CGITableFindValue(varTable, "CRMFRequest"); if (crmfReq == NULL) { rv = CGI_VAR_MISSING; missingVar = "CRMFRequest"; goto loser; } caNickname = CGITableFindValue(varTable, "CANickname"); if (caNickname == NULL) { rv = CGI_VAR_MISSING; missingVar = "CANickname"; goto loser; } caCert = CERT_FindCertByNickname(certdb, caNickname); if (caCert == NULL) { rv = COULD_NOT_FIND_CA; goto loser; } srv = ATOB_ConvertAsciiToItem(&der, crmfReq); if (srv != SECSuccess) { rv = BAD_ASCII_FOR_REQ; goto loser; } certReqs = CRMF_CreateCertReqMessagesFromDER(der.data, der.len); SECITEM_FreeItem(&der, PR_FALSE); if (certReqs == NULL) { rv = COULD_NOT_DECODE_REQS; goto loser; } numReqs = CRMF_CertReqMessagesGetNumMessages(certReqs); issuedCerts = PORT_ZNewArray(CertResponseInfo, numReqs); challInfo = PORT_ZNewArray(ChallengeCreationInfo, numReqs); if (issuedCerts == NULL || challInfo == NULL) { rv = OUT_OF_MEMORY; goto loser; } reqMsgs = PORT_ZNewArray(CRMFCertReqMsg*, numReqs); reqs = PORT_ZNewArray(CRMFCertRequest*, numReqs); if (reqMsgs == NULL || reqs == NULL) { rv = OUT_OF_MEMORY; goto loser; } for (i=0; i= form_output_len) { form_output_len += DEFAULT_ALLOC_SIZE; form_output = PORT_Realloc(form_output, form_output_len+1); } form_output_used += fread(&form_output[form_output_used], sizeof(char), DEFAULT_ALLOC_SIZE, stdin); } ParseInputVariables(&varTable, form_output); certRepContent = CGITableFindValue(&varTable, "CertRepContent"); if (certRepContent == NULL) { errNum = initNSS(&varTable); if (errNum != 0) { goto loser; } errNum = processRequest(&varTable); } else { errNum = processChallengeResponse(&varTable, certRepContent); } if (errNum != NO_ERROR) { goto loser; } goto done; loser: dumpErrorMessage(errNum); done: free (form_output); return 0; }