/* 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 "signtool.h" #include "zip.h" #include "prmem.h" #include "blapi.h" #include "sechash.h" /* for HASH_GetHashObject() */ static int create_pk7(char *dir, char *keyName, int *keyType); static int jar_find_key_type(CERTCertificate *cert); static int manifesto(char *dirname, char *install_script, PRBool recurse); static int manifesto_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg); static int manifesto_xpi_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg); static int sign_all_arc_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg); static int add_meta(FILE *fp, char *name); static int SignFile(FILE *outFile, FILE *inFile, CERTCertificate *cert); static int generate_SF_file(char *manifile, char *who); static int calculate_MD5_range(FILE *fp, long r1, long r2, JAR_Digest *dig); static void SignOut(void *arg, const char *buf, unsigned long len); static char *metafile = NULL; static int optimize = 0; static FILE *mf; static ZIPfile *zipfile = NULL; /* * S i g n A r c h i v e * * Sign an individual archive tree. A directory * called META-INF is created underneath this. * */ int SignArchive(char *tree, char *keyName, char *zip_file, int javascript, char *meta_file, char *install_script, int _optimize, PRBool recurse) { int status; char tempfn[FNSIZE], fullfn[FNSIZE]; int keyType = rsaKey; int count; metafile = meta_file; optimize = _optimize; /* To create XPI compatible Archive manifesto() must be run before * the zipfile is opened. This is so the signed files are not added * the archive before the crucial rsa/dsa file*/ if (xpi_arc) { manifesto(tree, install_script, recurse); } if (zip_file) { zipfile = JzipOpen(zip_file, NULL /*no comment*/); } /*Sign and add files to the archive normally with manifesto()*/ if (!xpi_arc) { manifesto(tree, install_script, recurse); } if (keyName) { status = create_pk7(tree, keyName, &keyType); if (status < 0) { PR_fprintf(errorFD, "the tree \"%s\" was NOT SUCCESSFULLY SIGNED\n", tree); errorCount++; exit(ERRX); } } /* Add the rsa/dsa file as the first file in the archive. This is crucial * for a XPInstall compatible archive */ if (xpi_arc) { if (verbosity >= 0) { PR_fprintf(outputFD, "%s \n", XPI_TEXT); } /* rsa/dsa to zip */ count = snprintf(tempfn, sizeof(tempfn), "META-INF/%s.%s", base, (keyType == dsaKey ? "dsa" : "rsa")); if (count >= sizeof(tempfn)) { PR_fprintf(errorFD, "unable to write key metadata\n"); errorCount++; exit(ERRX); } count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); if (count >= sizeof(fullfn)) { PR_fprintf(errorFD, "unable to write key metadata\n"); errorCount++; exit(ERRX); } JzipAdd(fullfn, tempfn, zipfile, compression_level); /* Loop through all files & subdirectories, add to archive */ foreach (tree, "", manifesto_xpi_fn, recurse, PR_FALSE /*include dirs */, (void *)NULL) ; } /* mf to zip */ strcpy(tempfn, "META-INF/manifest.mf"); count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); if (count >= sizeof(fullfn)) { PR_fprintf(errorFD, "unable to write manifest\n"); errorCount++; exit(ERRX); } JzipAdd(fullfn, tempfn, zipfile, compression_level); /* sf to zip */ count = snprintf(tempfn, sizeof(tempfn), "META-INF/%s.sf", base); if (count >= sizeof(tempfn)) { PR_fprintf(errorFD, "unable to write sf metadata\n"); errorCount++; exit(ERRX); } count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); if (count >= sizeof(fullfn)) { PR_fprintf(errorFD, "unable to write sf metadata\n"); errorCount++; exit(ERRX); } JzipAdd(fullfn, tempfn, zipfile, compression_level); /* Add the rsa/dsa file to the zip archive normally */ if (!xpi_arc) { /* rsa/dsa to zip */ count = snprintf(tempfn, sizeof(tempfn), "META-INF/%s.%s", base, (keyType == dsaKey ? "dsa" : "rsa")); if (count >= sizeof(tempfn)) { PR_fprintf(errorFD, "unable to write key metadata\n"); errorCount++; exit(ERRX); } count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); if (count >= sizeof(fullfn)) { PR_fprintf(errorFD, "unable to write key metadata\n"); errorCount++; exit(ERRX); } JzipAdd(fullfn, tempfn, zipfile, compression_level); } JzipClose(zipfile); if (verbosity >= 0) { if (javascript) { PR_fprintf(outputFD, "jarfile \"%s\" signed successfully\n", zip_file); } else { PR_fprintf(outputFD, "tree \"%s\" signed successfully\n", tree); } } return 0; } typedef struct { char *keyName; int javascript; char *metafile; char *install_script; int optimize; } SignArcInfo; /* * S i g n A l l A r c * * Javascript may generate multiple .arc directories, one * for each jar archive needed. Sign them all. * */ int SignAllArc(char *jartree, char *keyName, int javascript, char *metafilename, char *install_script, int optimize_level, PRBool recurse) { SignArcInfo info; info.keyName = keyName; info.javascript = javascript; info.metafile = metafilename; info.install_script = install_script; info.optimize = optimize_level; return foreach (jartree, "", sign_all_arc_fn, recurse, PR_TRUE /*include dirs*/, (void *)&info); } static int sign_all_arc_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg) { char *zipfilename = NULL; char *arc = NULL, *archive = NULL; int retval = 0; SignArcInfo *infop = (SignArcInfo *)arg; /* Make sure there is one and only one ".arc" in the relative path, * and that it is at the end of the path (don't sign .arcs within .arcs) */ if ((PL_strcaserstr(relpath, ".arc") == relpath + strlen(relpath) - 4) && (PL_strcasestr(relpath, ".arc") == relpath + strlen(relpath) - 4)) { if (!infop) { PR_fprintf(errorFD, "%s: Internal failure\n", PROGRAM_NAME); errorCount++; retval = -1; goto finish; } archive = PR_smprintf("%s/%s", basedir, relpath); zipfilename = PL_strdup(archive); arc = PORT_Strrchr(zipfilename, '.'); if (arc == NULL) { PR_fprintf(errorFD, "%s: Internal failure\n", PROGRAM_NAME); errorCount++; retval = -1; goto finish; } PL_strcpy(arc, ".jar"); if (verbosity >= 0) { PR_fprintf(outputFD, "\nsigning: %s\n", zipfilename); } retval = SignArchive(archive, infop->keyName, zipfilename, infop->javascript, infop->metafile, infop->install_script, infop->optimize, PR_TRUE /* recurse */); } finish: if (archive) PR_Free(archive); if (zipfilename) PR_Free(zipfilename); return retval; } /********************************************************************* * * c r e a t e _ p k 7 */ static int create_pk7(char *dir, char *keyName, int *keyType) { int status = 0; char *file_ext; CERTCertificate *cert; CERTCertDBHandle *db; FILE *in, *out; char sf_file[FNSIZE]; char pk7_file[FNSIZE]; /* open cert database */ db = CERT_GetDefaultCertDB(); if (db == NULL) return -1; /* find cert */ /*cert = CERT_FindCertByNicknameOrEmailAddr(db, keyName);*/ cert = PK11_FindCertFromNickname(keyName, &pwdata); if (cert == NULL) { SECU_PrintError(PROGRAM_NAME, "Cannot find the cert \"%s\"", keyName); return -1; } /* determine the key type, which sets the extension for pkcs7 object */ *keyType = jar_find_key_type(cert); file_ext = (*keyType == dsaKey) ? "dsa" : "rsa"; snprintf(sf_file, sizeof(sf_file), "%s/META-INF/%s.sf", dir, base); snprintf(pk7_file, sizeof(pk7_file), "%s/META-INF/%s.%s", dir, base, file_ext); if ((in = fopen(sf_file, "rb")) == NULL) { PR_fprintf(errorFD, "%s: Can't open %s for reading\n", PROGRAM_NAME, sf_file); errorCount++; exit(ERRX); } if ((out = fopen(pk7_file, "wb")) == NULL) { PR_fprintf(errorFD, "%s: Can't open %s for writing\n", PROGRAM_NAME, sf_file); errorCount++; exit(ERRX); } status = SignFile(out, in, cert); CERT_DestroyCertificate(cert); fclose(in); fclose(out); if (status) { PR_fprintf(errorFD, "%s: PROBLEM signing data (%s)\n", PROGRAM_NAME, SECU_Strerror(PORT_GetError())); errorCount++; return -1; } return 0; } /* * j a r _ f i n d _ k e y _ t y p e * * Determine the key type for a given cert, which * should be rsaKey or dsaKey. Any error return 0. * */ static int jar_find_key_type(CERTCertificate *cert) { SECKEYPrivateKey *privk = NULL; KeyType keyType; /* determine its type */ privk = PK11_FindKeyByAnyCert(cert, &pwdata); if (privk == NULL) { PR_fprintf(errorFD, "warning - can't find private key for this cert\n"); warningCount++; return 0; } keyType = privk->keyType; SECKEY_DestroyPrivateKey(privk); return keyType; } /* * m a n i f e s t o * * Run once for every subdirectory in which a * manifest is to be created -- usually exactly once. * */ static int manifesto(char *dirname, char *install_script, PRBool recurse) { char metadir[FNSIZE], sfname[FNSIZE]; /* Create the META-INF directory to hold signing info */ if (PR_Access(dirname, PR_ACCESS_READ_OK)) { PR_fprintf(errorFD, "%s: unable to read your directory: %s\n", PROGRAM_NAME, dirname); errorCount++; perror(dirname); exit(ERRX); } if (PR_Access(dirname, PR_ACCESS_WRITE_OK)) { PR_fprintf(errorFD, "%s: unable to write to your directory: %s\n", PROGRAM_NAME, dirname); errorCount++; perror(dirname); exit(ERRX); } snprintf(metadir, sizeof(metadir), "%s/META-INF", dirname); strcpy(sfname, metadir); PR_MkDir(metadir, 0777); strcat(metadir, "/"); strcat(metadir, MANIFEST); if ((mf = fopen(metadir, "wb")) == NULL) { perror(MANIFEST); PR_fprintf(errorFD, "%s: Probably, the directory you are trying to" " sign has\n", PROGRAM_NAME); PR_fprintf(errorFD, "%s: permissions problems or may not exist.\n", PROGRAM_NAME); errorCount++; exit(ERRX); } if (verbosity >= 0) { PR_fprintf(outputFD, "Generating %s file..\n", metadir); } fprintf(mf, "Manifest-Version: 1.0\n"); fprintf(mf, "Created-By: %s\n", CREATOR); fprintf(mf, "Comments: %s\n", BREAKAGE); if (scriptdir) { fprintf(mf, "Comments: --\n"); fprintf(mf, "Comments: --\n"); fprintf(mf, "Comments: -- This archive signs Javascripts which may not necessarily\n"); fprintf(mf, "Comments: -- be included in the physical jar file.\n"); fprintf(mf, "Comments: --\n"); fprintf(mf, "Comments: --\n"); } if (install_script) fprintf(mf, "Install-Script: %s\n", install_script); if (metafile) add_meta(mf, "+"); /* Loop through all files & subdirectories */ foreach (dirname, "", manifesto_fn, recurse, PR_FALSE /*include dirs */, (void *)NULL) ; fclose(mf); strcat(sfname, "/"); strcat(sfname, base); strcat(sfname, ".sf"); if (verbosity >= 0) { PR_fprintf(outputFD, "Generating %s.sf file..\n", base); } generate_SF_file(metadir, sfname); return 0; } /* * m a n i f e s t o _ x p i _ f n * * Called by pointer from SignArchive(), once for * each file within the directory. This function * is only used for adding to XPI compatible archive * */ static int manifesto_xpi_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg) { char fullname[FNSIZE]; int count; if (verbosity >= 0) { PR_fprintf(outputFD, "--> %s\n", relpath); } /* extension matching */ if (extensionsGiven) { char *ext = PL_strrchr(relpath, '.'); if (!ext) return 0; if (!PL_HashTableLookup(extensions, ext)) return 0; } count = snprintf(fullname, sizeof(fullname), "%s/%s", basedir, relpath); if (count >= sizeof(fullname)) { return 1; } JzipAdd(fullname, relpath, zipfile, compression_level); return 0; } /* * m a n i f e s t o _ f n * * Called by pointer from manifesto(), once for * each file within the directory. * */ static int manifesto_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg) { int use_js; char *md5, *sha1; JAR_Digest dig; char fullname[FNSIZE]; if (verbosity >= 0) { PR_fprintf(outputFD, "--> %s\n", relpath); } /* extension matching */ if (extensionsGiven) { char *ext = PL_strrchr(relpath, '.'); if (!ext) return 0; if (!PL_HashTableLookup(extensions, ext)) return 0; } snprintf(fullname, sizeof(fullname), "%s/%s", basedir, relpath); fprintf(mf, "\n"); use_js = 0; if (scriptdir && !PORT_Strcmp(scriptdir, reldir)) use_js++; /* sign non-.js files inside .arc directories using the javascript magic */ if ((PL_strcaserstr(filename, ".js") != filename + strlen(filename) - 3) && (PL_strcaserstr(reldir, ".arc") == reldir + strlen(filename) - 4)) use_js++; if (use_js) { fprintf(mf, "Name: %s\n", filename); fprintf(mf, "Magic: javascript\n"); if (optimize == 0) fprintf(mf, "javascript.id: %s\n", filename); if (metafile) add_meta(mf, filename); } else { fprintf(mf, "Name: %s\n", relpath); if (metafile) add_meta(mf, relpath); } JAR_digest_file(fullname, &dig); if (optimize == 0) { fprintf(mf, "Digest-Algorithms: MD5 SHA1\n"); md5 = BTOA_DataToAscii(dig.md5, MD5_LENGTH); fprintf(mf, "MD5-Digest: %s\n", md5); PORT_Free(md5); } sha1 = BTOA_DataToAscii(dig.sha1, SHA1_LENGTH); fprintf(mf, "SHA1-Digest: %s\n", sha1); PORT_Free(sha1); if (!use_js) { JzipAdd(fullname, relpath, zipfile, compression_level); } return 0; } /* * a d d _ m e t a * * Parse the metainfo file, and add any details * necessary to the manifest file. In most cases you * should be using the -i option (ie, for SmartUpdate). * */ static int add_meta(FILE *fp, char *name) { FILE *met; char buf[BUFSIZ]; int place; char *pattern, *meta; int num = 0; if ((met = fopen(metafile, "r")) != NULL) { while (fgets(buf, BUFSIZ, met)) { char *s; for (s = buf; *s && *s != '\n' && *s != '\r'; s++) ; *s = 0; if (*buf == 0) continue; pattern = buf; /* skip to whitespace */ for (s = buf; *s && *s != ' ' && *s != '\t'; s++) ; /* terminate pattern */ if (*s == ' ' || *s == '\t') *s++ = 0; /* eat through whitespace */ while (*s == ' ' || *s == '\t') s++; meta = s; /* this will eventually be regexp matching */ place = 0; if (!PORT_Strcmp(pattern, name)) place = 1; if (place) { num++; if (verbosity >= 0) { PR_fprintf(outputFD, "[%s] %s\n", name, meta); } fprintf(fp, "%s\n", meta); } } fclose(met); } else { PR_fprintf(errorFD, "%s: can't open metafile: %s\n", PROGRAM_NAME, metafile); errorCount++; exit(ERRX); } return num; } /********************************************************************** * * S i g n F i l e */ static int SignFile(FILE *outFile, FILE *inFile, CERTCertificate *cert) { int nb; char ibuf[4096], digestdata[32]; const SECHashObject *hashObj; void *hashcx; unsigned int len; SECItem digest; SEC_PKCS7ContentInfo *cinfo; SECStatus rv; if (outFile == NULL || inFile == NULL || cert == NULL) return -1; /* XXX probably want to extend interface to allow other hash algorithms */ hashObj = HASH_GetHashObject(HASH_AlgSHA1); hashcx = (*hashObj->create)(); if (hashcx == NULL) return -1; (*hashObj->begin)(hashcx); for (;;) { if (feof(inFile)) break; nb = fread(ibuf, 1, sizeof(ibuf), inFile); if (nb == 0) { if (ferror(inFile)) { PORT_SetError(SEC_ERROR_IO); (*hashObj->destroy)(hashcx, PR_TRUE); return -1; } /* eof */ break; } (*hashObj->update)(hashcx, (unsigned char *)ibuf, nb); } (*hashObj->end)(hashcx, (unsigned char *)digestdata, &len, 32); (*hashObj->destroy)(hashcx, PR_TRUE); digest.data = (unsigned char *)digestdata; digest.len = len; cinfo = SEC_PKCS7CreateSignedData(cert, certUsageObjectSigner, NULL, SEC_OID_SHA1, &digest, NULL, NULL); if (cinfo == NULL) return -1; rv = SEC_PKCS7IncludeCertChain(cinfo, NULL); if (rv != SECSuccess) { SEC_PKCS7DestroyContentInfo(cinfo); return -1; } if (no_time == 0) { rv = SEC_PKCS7AddSigningTime(cinfo); if (rv != SECSuccess) { /* don't check error */ } } rv = SEC_PKCS7Encode(cinfo, SignOut, outFile, NULL, NULL, &pwdata); SEC_PKCS7DestroyContentInfo(cinfo); if (rv != SECSuccess) return -1; return 0; } /* * g e n e r a t e _ S F _ f i l e * * From the supplied manifest file, calculates * digests on the various sections, creating a .SF * file in the process. * */ static int generate_SF_file(char *manifile, char *who) { FILE *sfFile; FILE *mfFile; long r1, r2, r3; char whofile[FNSIZE]; char *buf, *name = NULL; char *md5, *sha1; JAR_Digest dig; int line = 0; strcpy(whofile, who); if ((mfFile = fopen(manifile, "rb")) == NULL) { perror(manifile); exit(ERRX); } if ((sfFile = fopen(whofile, "wb")) == NULL) { perror(who); exit(ERRX); } buf = (char *)PORT_ZAlloc(BUFSIZ); if (buf) name = (char *)PORT_ZAlloc(BUFSIZ); if (buf == NULL || name == NULL) out_of_memory(); fprintf(sfFile, "Signature-Version: 1.0\n"); fprintf(sfFile, "Created-By: %s\n", CREATOR); fprintf(sfFile, "Comments: %s\n", BREAKAGE); if (fgets(buf, BUFSIZ, mfFile) == NULL) { PR_fprintf(errorFD, "%s: empty manifest file!\n", PROGRAM_NAME); errorCount++; exit(ERRX); } if (strncmp(buf, "Manifest-Version:", 17)) { PR_fprintf(errorFD, "%s: not a manifest file!\n", PROGRAM_NAME); errorCount++; exit(ERRX); } fseek(mfFile, 0L, SEEK_SET); /* Process blocks of headers, and calculate their hashen */ while (1) { /* Beginning range */ r1 = ftell(mfFile); if (fgets(name, BUFSIZ, mfFile) == NULL) break; line++; if (r1 != 0 && strncmp(name, "Name:", 5)) { PR_fprintf(errorFD, "warning: unexpected input in manifest file \"%s\" at line %d:\n", manifile, line); PR_fprintf(errorFD, "%s\n", name); warningCount++; } r2 = r1; while (fgets(buf, BUFSIZ, mfFile)) { if (*buf == 0 || *buf == '\n' || *buf == '\r') break; line++; /* Ending range for hashing */ r2 = ftell(mfFile); } r3 = ftell(mfFile); if (r1) { fprintf(sfFile, "\n"); fprintf(sfFile, "%s", name); } calculate_MD5_range(mfFile, r1, r2, &dig); if (optimize == 0) { fprintf(sfFile, "Digest-Algorithms: MD5 SHA1\n"); md5 = BTOA_DataToAscii(dig.md5, MD5_LENGTH); fprintf(sfFile, "MD5-Digest: %s\n", md5); PORT_Free(md5); } sha1 = BTOA_DataToAscii(dig.sha1, SHA1_LENGTH); fprintf(sfFile, "SHA1-Digest: %s\n", sha1); PORT_Free(sha1); /* restore normalcy after changing offset position */ fseek(mfFile, r3, SEEK_SET); } PORT_Free(buf); PORT_Free(name); fclose(sfFile); fclose(mfFile); return 0; } /* * c a l c u l a t e _ M D 5 _ r a n g e * * Calculate the MD5 digest on a range of bytes in * the specified fopen'd file. Returns base64. * */ static int calculate_MD5_range(FILE *fp, long r1, long r2, JAR_Digest *dig) { int num; int range; unsigned char *buf; SECStatus rv; range = r2 - r1; /* position to the beginning of range */ fseek(fp, r1, SEEK_SET); buf = (unsigned char *)PORT_ZAlloc(range); if (buf == NULL) out_of_memory(); if ((num = fread(buf, 1, range, fp)) != range) { PR_fprintf(errorFD, "%s: expected %d bytes, got %d\n", PROGRAM_NAME, range, num); errorCount++; exit(ERRX); } rv = PK11_HashBuf(SEC_OID_MD5, dig->md5, buf, range); if (rv == SECSuccess) { rv = PK11_HashBuf(SEC_OID_SHA1, dig->sha1, buf, range); } if (rv != SECSuccess) { PR_fprintf(errorFD, "%s: can't generate digest context\n", PROGRAM_NAME); errorCount++; exit(ERRX); } PORT_Free(buf); return 0; } static void SignOut(void *arg, const char *buf, unsigned long len) { fwrite(buf, len, 1, (FILE *)arg); }