diff --git a/js/xpconnect/src/xpc.msg b/js/xpconnect/src/xpc.msg index 264274d37662..522ba89d9a57 100644 --- a/js/xpconnect/src/xpc.msg +++ b/js/xpconnect/src/xpc.msg @@ -188,3 +188,13 @@ XPC_MSG_DEF(NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED, "Clearing site data by tim /* character converter related codes (from nsIUnicodeDecoder.h) */ XPC_MSG_DEF(NS_ERROR_ILLEGAL_INPUT , "The input characters have illegal sequences") + +/* Codes related to signd jars */ +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_NOT_SIGNED , "The JAR is not signed.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY , "An entry in the JAR has been modified after the JAR was signed.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY , "An entry in the JAR has not been signed.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_MISSING , "An entry is missing from the JAR file.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE , "The JAR's signature is wrong.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE , "An entry in the JAR is too large.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_INVALID , "An entry in the JAR is invalid.") +XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID , "The JAR's manifest or signature file is invalid.") diff --git a/security/manager/ssl/public/nsIX509CertDB.idl b/security/manager/ssl/public/nsIX509CertDB.idl index 801afd8b58bf..cedbb2644473 100644 --- a/security/manager/ssl/public/nsIX509CertDB.idl +++ b/security/manager/ssl/public/nsIX509CertDB.idl @@ -8,18 +8,28 @@ interface nsIArray; interface nsIX509Cert; +interface nsIX509Cert3; interface nsIFile; interface nsIInterfaceRequestor; +interface nsIZipReader; %{C++ #define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1" %} +[scriptable, function, uuid(48411e2d-85a9-4b16-bec8-e30cde801f9e)] +interface nsIOpenSignedJARFileCallback : nsISupports +{ + void openSignedJARFileFinished(in nsresult rv, + in nsIZipReader aZipReader, + in nsIX509Cert3 aSignerCert); +}; + /** * This represents a service to access and manipulate * X.509 certificates stored in a database. */ -[scriptable, uuid(eb426311-69cd-4a74-a7db-a4a215854c78)] +[scriptable, uuid(735d0363-e219-4387-b5c6-72e800c3ea0b)] interface nsIX509CertDB : nsISupports { /** @@ -253,5 +263,33 @@ interface nsIX509CertDB : nsISupports { * @return The new certificate object. */ nsIX509Cert constructX509FromBase64(in string base64); -}; + /** + * Verifies the signature on the given JAR file to verify that it has a + * valid signature. To be considered valid, there must be exactly one + * signature on the JAR file and that signature must have signed every + * entry. Further, the signature must come from a certificate that + * is trusted for code signing. + * + * On success, NS_OK, a nsIZipReader, and the trusted certificate that + * signed the JAR are returned. + * + * On failure, an error code is returned. + * + * This method returns a nsIZipReader, instead of taking an nsIZipReader + * as input, to encourage users of the API to verify the signature as the + * first step in opening the JAR. + */ + void openSignedJARFileAsync(in nsIFile aJarFile, + in nsIOpenSignedJARFileCallback callback); + + /* + * Add a cert to a cert DB from a binary string. + * + * @param certDER The raw DER encoding of a certificate. + * @param aTrust decoded by CERT_DecodeTrustString. 3 comma separated characters, + * indicating SSL, Email, and Obj signing trust + * @param aName name of the cert for display purposes. + */ + void addCert(in ACString certDER, in string aTrust, in string aName); +}; diff --git a/security/manager/ssl/src/JARSignatureVerification.cpp b/security/manager/ssl/src/JARSignatureVerification.cpp new file mode 100644 index 000000000000..d0a8b98ff787 --- /dev/null +++ b/security/manager/ssl/src/JARSignatureVerification.cpp @@ -0,0 +1,767 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifdef MOZ_LOGGING +#define FORCE_PR_LOG 1 +#endif + +#include "nsNSSCertificateDB.h" + +#include "mozilla/RefPtr.h" +#include "CryptoTask.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIStringEnumerator.h" +#include "nsIZipReader.h" +#include "nsNSSCertificate.h" +#include "nsString.h" +#include "nsTHashtable.h" +#include "ScopedNSSTypes.h" + +#include "base64.h" +#include "secmime.h" +#include "plstr.h" +#include "prlog.h" + +using namespace mozilla; + +#ifdef MOZ_LOGGING +extern PRLogModuleInfo* gPIPNSSLog; +#endif + +namespace { + +// Finds exactly one (signature metadata) entry that matches the given +// search pattern, and then load it. Fails if there are no matches or if +// there is more than one match. If bugDigest is not null then on success +// bufDigest will contain the SHA-1 digeset of the entry. +nsresult +FindAndLoadOneEntry(nsIZipReader * zip, + const nsACString & searchPattern, + /*out*/ nsACString & filename, + /*out*/ SECItem & buf, + /*optional, out*/ Digest * bufDigest) +{ + nsCOMPtr files; + nsresult rv = zip->FindEntries(searchPattern, getter_AddRefs(files)); + if (NS_FAILED(rv) || !files) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + bool more; + rv = files->HasMore(&more); + NS_ENSURE_SUCCESS(rv, rv); + if (!more) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + rv = files->GetNext(filename); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if there is more than one match, if so then error! + rv = files->HasMore(&more); + NS_ENSURE_SUCCESS(rv, rv); + if (more) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + nsCOMPtr stream; + rv = zip->GetInputStream(filename, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + // The size returned by Available() might be inaccurate so we need to check + // that Available() matches up with the actual length of the file. + uint64_t len64; + rv = stream->Available(&len64); + NS_ENSURE_SUCCESS(rv, rv); + + + // Cap the maximum accepted size of signature-related files at 1MB (which is + // still crazily huge) to avoid OOM. The uncompressed length of an entry can be + // hundreds of times larger than the compressed version, especially if + // someone has speifically crafted the entry to cause OOM or to consume + // massive amounts of disk space. + // + // Also, keep in mind bug 164695 and that we must leave room for + // null-terminating the buffer. + static const uint32_t MAX_LENGTH = 1024 * 1024; + MOZ_STATIC_ASSERT(MAX_LENGTH < UINT32_MAX, "MAX_LENGTH < UINT32_MAX"); + NS_ENSURE_TRUE(len64 < MAX_LENGTH, NS_ERROR_FILE_CORRUPTED); + NS_ENSURE_TRUE(len64 < UINT32_MAX, NS_ERROR_FILE_CORRUPTED); // bug 164695 + SECITEM_AllocItem(buf, static_cast(len64 + 1)); + + // buf.len == len64 + 1. We attempt to read len64 + 1 bytes instead of len64, + // so that we can check whether the metadata in the ZIP for the entry is + // incorrect. + uint32_t bytesRead; + rv = stream->Read(char_ptr_cast(buf.data), buf.len, &bytesRead); + NS_ENSURE_SUCCESS(rv, rv); + if (bytesRead != len64) { + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + + buf.data[buf.len - 1] = 0; // null-terminate + + if (bufDigest) { + rv = bufDigest->DigestBuf(SEC_OID_SHA1, buf.data, buf.len - 1); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// Verify the digest of an entry. We avoid loading the entire entry into memory +// at once, which would require memory in proportion to the size of the largest +// entry. Instead, we require only a small, fixed amount of memory. +// +// @param digestFromManifest The digest that we're supposed to check the file's +// contents against, from the manifest +// @param buf A scratch buffer that we use for doing the I/O, which must have +// already been allocated. The size of this buffer is the unit +// size of our I/O. +nsresult +VerifyEntryContentDigest(nsIZipReader * zip, const nsACString & aFilename, + const SECItem & digestFromManifest, SECItem & buf) +{ + MOZ_ASSERT(buf.len > 0); + if (digestFromManifest.len != SHA1_LENGTH) + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + + nsresult rv; + + nsCOMPtr stream; + rv = zip->GetInputStream(aFilename, getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + uint64_t len64; + rv = stream->Available(&len64); + NS_ENSURE_SUCCESS(rv, rv); + if (len64 > UINT32_MAX) { + return NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE; + } + + ScopedPK11Context digestContext(PK11_CreateDigestContext(SEC_OID_SHA1)); + if (!digestContext) { + return PRErrorCode_to_nsresult(PR_GetError()); + } + + rv = MapSECStatus(PK11_DigestBegin(digestContext)); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t totalBytesRead = 0; + for (;;) { + uint32_t bytesRead; + rv = stream->Read(char_ptr_cast(buf.data), buf.len, &bytesRead); + NS_ENSURE_SUCCESS(rv, rv); + + if (bytesRead == 0) { + break; // EOF + } + + totalBytesRead += bytesRead; + if (totalBytesRead >= UINT32_MAX) { + return NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE; + } + + rv = MapSECStatus(PK11_DigestOp(digestContext, buf.data, bytesRead)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (totalBytesRead != len64) { + // The metadata we used for Available() doesn't match the actual size of + // the entry. + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + + // Verify that the digests match. + Digest digest; + rv = digest.End(SEC_OID_SHA1, digestContext); + NS_ENSURE_SUCCESS(rv, rv); + + if (SECITEM_CompareItem(&digestFromManifest, &digest.get()) != SECEqual) { + return NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY; + } + + return NS_OK; +} + +// On input, nextLineStart is the start of the current line. On output, +// nextLineStart is the start of the next line. +nsresult +ReadLine(/*in/out*/ const char* & nextLineStart, /*out*/ nsCString & line, + bool allowContinuations = true) +{ + line.Truncate(); + for (;;) { + const char* eol = PL_strpbrk(nextLineStart, "\r\n"); + + if (!eol) { // Reached end of file before newline + eol = nextLineStart + PL_strlen(nextLineStart); + } + + line.Append(nextLineStart, eol - nextLineStart); + + if (*eol == '\r') { + ++eol; + } + if (*eol == '\n') { + ++eol; + } + + nextLineStart = eol; + + if (*eol != ' ') { + // not a continuation + return NS_OK; + } + + // continuation + if (!allowContinuations) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + ++nextLineStart; // skip space and keep appending + } +} + +// The header strings are defined in the JAR specification. +#define JAR_MF_SEARCH_STRING "(M|/M)ETA-INF/(M|m)(ANIFEST|anifest).(MF|mf)$" +#define JAR_SF_SEARCH_STRING "(M|/M)ETA-INF/*.(SF|sf)$" +#define JAR_RSA_SEARCH_STRING "(M|/M)ETA-INF/*.(RSA|rsa)$" +#define JAR_MF_HEADER (const char*)"Manifest-Version: 1.0" +#define JAR_SF_HEADER (const char*)"Signature-Version: 1.0" + +nsresult +ParseAttribute(const nsAutoCString & curLine, + /*out*/ nsAutoCString & attrName, + /*out*/ nsAutoCString & attrValue) +{ + nsAutoCString::size_type len = curLine.Length(); + if (len > 72) { + // The spec says "No line may be longer than 72 bytes (not characters)" + // in its UTF8-encoded form. This check also ensures that len < INT32_MAX, + // which is required below. + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Find the colon that separates the name from the value. + int32_t colonPos = curLine.FindChar(':'); + if (colonPos == kNotFound) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // set attrName to the name, skipping spaces between the name and colon + int32_t nameEnd = colonPos; + for (;;) { + if (nameEnd == 0) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; // colon with no name + } + if (curLine[nameEnd - 1] != ' ') + break; + --nameEnd; + } + curLine.Left(attrName, nameEnd); + + // Set attrValue to the value, skipping spaces between the colon and the + // value. The value may be empty. + int32_t valueStart = colonPos + 1; + int32_t curLineLength = curLine.Length(); + while (valueStart != curLineLength && curLine[valueStart] == ' ') { + ++valueStart; + } + curLine.Right(attrValue, curLineLength - valueStart); + + return NS_OK; +} + +// Parses the version line of the MF or SF header. +nsresult +CheckManifestVersion(const char* & nextLineStart, + const nsACString & expectedHeader) +{ + // The JAR spec says: "Manifest-Version and Signature-Version must be first, + // and in exactly that case (so that they can be recognized easily as magic + // strings)." + nsAutoCString curLine; + nsresult rv = ReadLine(nextLineStart, curLine, false); + if (NS_FAILED(rv)) { + return rv; + } + if (!curLine.Equals(expectedHeader)) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + return NS_OK; +} + +// Parses a signature file (SF) as defined in the JDK 8 JAR Specification. +// +// The SF file *must* contain exactly one SHA1-Digest-Manifest attribute in +// the main section. All other sections are ignored. This means that this will +// NOT parse old-style signature files that have separate digests per entry. +// The JDK8 x-Digest-Manifest variant is better because: +// +// (1) It allows us to follow the principle that we should minimize the +// processing of data that we do before we verify its signature. In +// particular, with the x-Digest-Manifest style, we can verify the digest +// of MANIFEST.MF before we parse it, which prevents malicious JARs +// exploiting our MANIFEST.MF parser. +// (2) It is more time-efficient and space-efficient to have one +// x-Digest-Manifest instead of multiple x-Digest values. +// +// In order to get benefit (1), we do NOT implement the fallback to the older +// mechanism as the spec requires/suggests. Also, for simplity's sake, we only +// support exactly one SHA1-Digest-Manifest attribute, and no other +// algorithms. +// +// filebuf must be null-terminated. On output, mfDigest will contain the +// decoded value of SHA1-Digest-Manifest. +nsresult +ParseSF(const char* filebuf, /*out*/ SECItem & mfDigest) +{ + nsresult rv; + + const char* nextLineStart = filebuf; + rv = CheckManifestVersion(nextLineStart, nsLiteralCString(JAR_SF_HEADER)); + if (NS_FAILED(rv)) + return rv; + + // Find SHA1-Digest-Manifest + for (;;) { + nsAutoCString curLine; + rv = ReadLine(nextLineStart, curLine); + if (NS_FAILED(rv)) { + return rv; + } + + if (curLine.Length() == 0) { + // End of main section (blank line or end-of-file), and no + // SHA1-Digest-Manifest found. + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + nsAutoCString attrName; + nsAutoCString attrValue; + rv = ParseAttribute(curLine, attrName, attrValue); + if (NS_FAILED(rv)) { + return rv; + } + + if (attrName.LowerCaseEqualsLiteral("sha1-digest-manifest")) { + rv = MapSECStatus(ATOB_ConvertAsciiToItem(&mfDigest, attrValue.get())); + if (NS_FAILED(rv)) { + return rv; + } + + // There could be multiple SHA1-Digest-Manifest attributes, which + // would be an error, but it's better to just skip any erroneous + // duplicate entries rather than trying to detect them, because: + // + // (1) It's simpler, and simpler generally means more secure + // (2) An attacker can't make us accept a JAR we would otherwise + // reject just by adding additional SHA1-Digest-Manifest + // attributes. + break; + } + + // ignore unrecognized attributes + } + + return NS_OK; +} + +// Parses MANIFEST.MF. The filenames of all entries will be returned in +// mfItems. buf must be a pre-allocated scratch buffer that is used for doing +// I/O. +nsresult +ParseMF(const char* filebuf, nsIZipReader * zip, + /*out*/ nsTHashtable & mfItems, + ScopedAutoSECItem & buf) +{ + nsresult rv; + + const char* nextLineStart = filebuf; + + rv = CheckManifestVersion(nextLineStart, nsLiteralCString(JAR_MF_HEADER)); + if (NS_FAILED(rv)) { + return rv; + } + + // Skip the rest of the header section, which ends with a blank line. + { + nsAutoCString line; + do { + rv = ReadLine(nextLineStart, line); + if (NS_FAILED(rv)) { + return rv; + } + } while (line.Length() > 0); + + // Manifest containing no file entries is OK, though useless. + if (*nextLineStart == '\0') { + return NS_OK; + } + } + + nsAutoCString curItemName; + ScopedAutoSECItem digest; + + for (;;) { + nsAutoCString curLine; + rv = ReadLine(nextLineStart, curLine); + NS_ENSURE_SUCCESS(rv, rv); + + if (curLine.Length() == 0) { + // end of section (blank line or end-of-file) + + if (curItemName.Length() == 0) { + // '...Each section must start with an attribute with the name as + // "Name",...', so every section must have a Name attribute. + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + if (digest.len == 0) { + // We require every entry to have a digest, since we require every + // entry to be signed and we don't allow duplicate entries. + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + if (mfItems.Contains(curItemName)) { + // Duplicate entry + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Verify that the entry's content digest matches the digest from this + // MF section. + rv = VerifyEntryContentDigest(zip, curItemName, digest, buf); + if (NS_FAILED(rv)) + return rv; + + mfItems.PutEntry(curItemName); + + if (*nextLineStart == '\0') // end-of-file + break; + + // reset so we know we haven't encountered either of these for the next + // item yet. + curItemName.Truncate(); + digest.reset(); + + continue; // skip the rest of the loop below + } + + nsAutoCString attrName; + nsAutoCString attrValue; + rv = ParseAttribute(curLine, attrName, attrValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Lines to look for: + + // (1) Digest: + if (attrName.LowerCaseEqualsLiteral("sha1-digest")) + { + if (digest.len > 0) // multiple SHA1 digests in section + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + + rv = MapSECStatus(ATOB_ConvertAsciiToItem(&digest, attrValue.get())); + if (NS_FAILED(rv)) + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + + continue; + } + + // (2) Name: associates this manifest section with a file in the jar. + if (attrName.LowerCaseEqualsLiteral("name")) + { + if (MOZ_UNLIKELY(curItemName.Length() > 0)) // multiple names in section + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + + if (MOZ_UNLIKELY(attrValue.Length() == 0)) + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + + curItemName = attrValue; + + continue; + } + + // (3) Magic: the only other must-understand attribute + if (attrName.LowerCaseEqualsLiteral("magic")) { + // We don't understand any magic, so we can't verify an entry that + // requires magic. Since we require every entry to have a valid + // signature, we have no choice but to reject the entry. + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // unrecognized attributes must be ignored + } + + return NS_OK; +} + +// Callback functions for decoder. For now, use empty/default functions. +void +ContentCallback(void *arg, const char *buf, unsigned long len) +{ +} +PK11SymKey * +GetDecryptKeyCallback(void *, SECAlgorithmID *) +{ + return nullptr; +} +PRBool +DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *bulkkey) +{ + return false; +} +void * +GetPasswordKeyCallback(void *arg, void *handle) +{ + return nullptr; +} + +NS_IMETHODIMP +OpenSignedJARFile(nsIFile * aJarFile, + /*out, optional */ nsIZipReader ** aZipReader, + /*out, optional */ nsIX509Cert3 ** aSignerCert) +{ + NS_ENSURE_ARG_POINTER(aJarFile); + + if (aZipReader) { + *aZipReader = nullptr; + } + + if (aSignerCert) { + *aSignerCert = nullptr; + } + + nsresult rv; + + static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); + nsCOMPtr zip = do_CreateInstance(kZipReaderCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = zip->Open(aJarFile); + NS_ENSURE_SUCCESS(rv, rv); + + // Signature (RSA) file + nsAutoCString sigFilename; + ScopedAutoSECItem sigBuffer; + rv = FindAndLoadOneEntry(zip, nsLiteralCString(JAR_RSA_SEARCH_STRING), + sigFilename, sigBuffer, nullptr); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_NOT_SIGNED; + } + + sigBuffer.type = siBuffer; + ScopedSEC_PKCS7ContentInfo p7_info(SEC_PKCS7DecodeItem(&sigBuffer, + ContentCallback, nullptr, + GetPasswordKeyCallback, nullptr, + GetDecryptKeyCallback, nullptr, + DecryptionAllowedCallback)); + if (!p7_info) { + PRErrorCode error = PR_GetError(); + const char * errorName = PR_ErrorToName(error); + PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("Failed to decode PKCS#7 item: %s", + errorName)); + return PRErrorCode_to_nsresult(error); + } + + // Signature (SF) file + nsAutoCString sfFilename; + ScopedAutoSECItem sfBuffer; + Digest sfCalculatedDigest; + rv = FindAndLoadOneEntry(zip, NS_LITERAL_CSTRING(JAR_SF_SEARCH_STRING), + sfFilename, sfBuffer, &sfCalculatedDigest); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Verify that the signature file is a valid signature of the SF file + if (!SEC_PKCS7VerifyDetachedSignature(p7_info, certUsageObjectSigner, + &sfCalculatedDigest.get(), HASH_AlgSHA1, + false)) { + PRErrorCode error = PR_GetError(); + const char * errorName = PR_ErrorToName(error); + PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("Failed to verify detached signature: %s", + errorName)); + rv = PRErrorCode_to_nsresult(error); + return rv; + } + + ScopedAutoSECItem mfDigest; + rv = ParseSF(char_ptr_cast(sfBuffer.data), mfDigest); + if (NS_FAILED(rv)) { + return rv; + } + + // Manifest (MF) file + nsAutoCString mfFilename; + ScopedAutoSECItem manifestBuffer; + Digest mfCalculatedDigest; + rv = FindAndLoadOneEntry(zip, NS_LITERAL_CSTRING(JAR_MF_SEARCH_STRING), + mfFilename, manifestBuffer, &mfCalculatedDigest); + if (NS_FAILED(rv)) { + return rv; + } + + if (SECITEM_CompareItem(&mfDigest, &mfCalculatedDigest.get()) != SECEqual) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Allocate the I/O buffer only once per JAR, instead of once per entry, in + // order to minimize malloc/free calls and in order to avoid fragmenting + // memory. + ScopedAutoSECItem buf(128 * 1024); + + nsTHashtable items; + items.Init(); + + rv = ParseMF(char_ptr_cast(manifestBuffer.data), zip, items, buf); + if (NS_FAILED(rv)) { + return rv; + } + + // Verify every entry in the file. + nsCOMPtr entries; + rv = zip->FindEntries(NS_LITERAL_CSTRING(""), getter_AddRefs(entries)); + if (NS_SUCCEEDED(rv) && !entries) { + rv = NS_ERROR_UNEXPECTED; + } + if (NS_FAILED(rv)) { + return rv; + } + + for (;;) { + bool hasMore; + rv = entries->HasMore(&hasMore); + NS_ENSURE_SUCCESS(rv, rv); + + if (!hasMore) { + break; + } + + nsAutoCString entryFilename; + rv = entries->GetNext(entryFilename); + NS_ENSURE_SUCCESS(rv, rv); + + PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("Verifying digests for %s", + entryFilename.get())); + + // The files that comprise the signature mechanism are not covered by the + // signature. + // + // XXX: This is OK for a single signature, but doesn't work for + // multiple signatures, because the metadata for the other signatures + // is not signed either. + if (entryFilename == mfFilename || + entryFilename == sfFilename || + entryFilename == sigFilename) { + continue; + } + + if (entryFilename.Length() == 0) { + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + + // Entries with names that end in "/" are directory entries, which are not + // signed. + // + // XXX: As long as we don't unpack the JAR into the filesystem, the "/" + // entries are harmless. But, it is not clear what the security + // implications of directory entries are if/when we were to unpackage the + // JAR into the filesystem. + if (entryFilename[entryFilename.Length() - 1] == '/') { + continue; + } + + nsCStringHashKey * item = items.GetEntry(entryFilename); + if (!item) { + return NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY; + } + + // Remove the item so we can check for leftover items later + items.RemoveEntry(entryFilename); + } + + // We verified that every entry that we require to be signed is signed. But, + // were there any missing entries--that is, entries that are mentioned in the + // manifest but missing from the archive? + if (items.Count() != 0) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + // Return the reader to the caller if they want it + if (aZipReader) { + zip.forget(aZipReader); + } + + // Return the signer's certificate to the reader if they want it. + // XXX: We should return an nsIX509CertList with the whole validated chain, + // but we can't do that until we switch to libpkix. + if (aSignerCert) { + CERTCertificate *rawSignerCert + = p7_info->content.signedData->signerInfos[0]->cert; + NS_ENSURE_TRUE(rawSignerCert, NS_ERROR_UNEXPECTED); + + nsCOMPtr signerCert = nsNSSCertificate::Create(rawSignerCert); + NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY); + + signerCert.forget(aSignerCert); + } + + return NS_OK; +} + +class OpenSignedJARFileTask MOZ_FINAL : public CryptoTask +{ +public: + OpenSignedJARFileTask(nsIFile * aJarFile, + nsIOpenSignedJARFileCallback * aCallback) + : mJarFile(aJarFile) + , mCallback(aCallback) + { + } + +private: + virtual nsresult CalculateResult() MOZ_OVERRIDE + { + return OpenSignedJARFile(mJarFile, getter_AddRefs(mZipReader), + getter_AddRefs(mSignerCert)); + } + + // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that + // needs to be released + virtual void ReleaseNSSResources() { } + + virtual void CallCallback(nsresult rv) + { + (void) mCallback->OpenSignedJARFileFinished(rv, mZipReader, mSignerCert); + } + + const nsCOMPtr mJarFile; + const nsCOMPtr mCallback; + nsCOMPtr mZipReader; // out + nsCOMPtr mSignerCert; // out +}; + +} // unnamed namespace + +NS_IMETHODIMP +nsNSSCertificateDB::OpenSignedJARFileAsync( + nsIFile * aJarFile, nsIOpenSignedJARFileCallback * aCallback) +{ + NS_ENSURE_ARG_POINTER(aJarFile); + NS_ENSURE_ARG_POINTER(aCallback); + RefPtr task(new OpenSignedJARFileTask(aJarFile, + aCallback)); + return task->Dispatch("SignedJAR"); +} diff --git a/security/manager/ssl/src/Makefile.in b/security/manager/ssl/src/Makefile.in index 8bc4a34c17fa..1ea14e33748b 100644 --- a/security/manager/ssl/src/Makefile.in +++ b/security/manager/ssl/src/Makefile.in @@ -21,6 +21,7 @@ LIBXUL_LIBRARY = 1 CPPSRCS = \ CryptoTask.cpp \ + JARSignatureVerification.cpp \ nsCERTValInParamWrapper.cpp \ nsNSSCleaner.cpp \ nsCertOverrideService.cpp \ diff --git a/security/manager/ssl/src/nsNSSCertificateDB.cpp b/security/manager/ssl/src/nsNSSCertificateDB.cpp index 5bfbfad07400..912ced9a8d68 100644 --- a/security/manager/ssl/src/nsNSSCertificateDB.cpp +++ b/security/manager/ssl/src/nsNSSCertificateDB.cpp @@ -9,6 +9,7 @@ #include "nsNSSComponent.h" #include "nsNSSCertificateDB.h" +#include "mozilla/Base64.h" #include "nsCOMPtr.h" #include "nsNSSCertificate.h" #include "nsNSSHelper.h" @@ -1616,6 +1617,16 @@ NS_IMETHODIMP nsNSSCertificateDB::AddCertFromBase64(const char *aBase64, const c return (srv == SECSuccess) ? NS_OK : NS_ERROR_FAILURE; } +NS_IMETHODIMP +nsNSSCertificateDB::AddCert(const nsACString & aCertDER, const char *aTrust, + const char *aName) +{ + nsCString base64; + nsresult rv = Base64Encode(aCertDER, base64); + NS_ENSURE_SUCCESS(rv, rv); + return AddCertFromBase64(base64.get(), aTrust, aName); +} + NS_IMETHODIMP nsNSSCertificateDB::GetCerts(nsIX509CertList **_retval) { diff --git a/security/manager/ssl/src/nsNSSComponent.cpp b/security/manager/ssl/src/nsNSSComponent.cpp index 3a93e8c482a6..486a3e1f907b 100644 --- a/security/manager/ssl/src/nsNSSComponent.cpp +++ b/security/manager/ssl/src/nsNSSComponent.cpp @@ -4,6 +4,10 @@ * 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/. */ +#ifdef MOZ_LOGGING +#define FORCE_PR_LOG 1 +#endif + #include "nsNSSComponent.h" #include "nsNSSCallbacks.h" #include "nsNSSIOLayer.h" @@ -84,7 +88,7 @@ using namespace mozilla; using namespace mozilla::psm; -#ifdef PR_LOGGING +#ifdef MOZ_LOGGING PRLogModuleInfo* gPIPNSSLog = nullptr; #endif diff --git a/security/manager/ssl/tests/unit/test_signed_apps.js b/security/manager/ssl/tests/unit/test_signed_apps.js new file mode 100644 index 000000000000..3fb67071cb3e --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps.js @@ -0,0 +1,254 @@ +"use strict"; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +/* To regenerate the certificates and apps for this test: + + cd security/manager/ssl/tests/unit/test_signed_apps + PATH=$NSS/bin:$NSS/lib:$PATH ./generate.sh + cd ../../../../../.. + make -C $OBJDIR/security/manager/ssl/tests + + $NSS is the path to NSS binaries and libraries built for the host platform. + If you get error messages about "CertUtil" on Windows, then it means that + the Windows CertUtil.exe is ahead of the NSS certutil.exe in $PATH. + + Check in the generated files. These steps are not done as part of the build + because we do not want to add a build-time dependency on the OpenSSL or NSS + tools or libraries built for the host platform. +*/ + +// XXX from prio.h +const PR_RDWR = 0x04; +const PR_CREATE_FILE = 0x08; +const PR_TRUNCATE = 0x20; + +let tempScope = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", tempScope); +let NetUtil = tempScope.NetUtil; + +Cu.import("resource://gre/modules/FileUtils.jsm"); // XXX: tempScope? +Cu.import("resource://gre/modules/Services.jsm"); // XXX: tempScope? + +do_get_profile(); // must be called before getting nsIX509CertDB +const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB); + +// Creates a new app package based in the inFilePath package, with a set of +// modifications (including possibly deletions) applied to the existing entries, +// and/or a set of new entries to be included. +function tamper(inFilePath, outFilePath, modifications, newEntries) { + var writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + writer.open(outFilePath, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + try { + var reader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); + reader.open(inFilePath); + try { + var entries = reader.findEntries(""); + while (entries.hasMore()) { + var entryName = entries.getNext(); + var inEntry = reader.getEntry(entryName); + var entryInput = reader.getInputStream(entryName); + try { + var f = modifications[entryName]; + var outEntry, outEntryInput; + if (f) { + [outEntry, outEntryInput] = f(inEntry, entryInput); + delete modifications[entryName]; + } else { + [outEntry, outEntryInput] = [inEntry, entryInput]; + } + // if f does not want the input entry to be copied to the output entry + // at all (i.e. it wants it to be deleted), it will return null. + if (outEntryInput) { + try { + writer.addEntryStream(entryName, + outEntry.lastModifiedTime, + outEntry.compression, + outEntryInput, + false); + } finally { + if (entryInput != outEntryInput) + outEntryInput.close(); + } + } + } finally { + entryInput.close(); + } + } + } finally { + reader.close(); + } + + // Any leftover modification means that we were expecting to modify an entry + // in the input file that wasn't there. + for(var name in modifications) { + if (modifications.hasOwnProperty(name)) { + throw "input file was missing expected entries: " + name; + } + } + + // Now, append any new entries to the end + newEntries.forEach(function(newEntry) { + var sis = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + try { + sis.setData(newEntry.content, newEntry.content.length); + writer.addEntryStream(newEntry.name, + new Date(), + Ci.nsIZipWriter.COMPRESSION_BEST, + sis, + false); + } finally { + sis.close(); + } + }); + } finally { + writer.close(); + } +} + +function removeEntry(entry, entryInput) { return [null, null]; } + +function truncateEntry(entry, entryInput) { + if (entryInput.available() == 0) + throw "Truncating already-zero length entry will result in identical entry."; + + var content = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + content.data = ""; + + return [entry, content] +} + +function readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + return data; +} + +function run_test() { + var root_cert_der = + do_get_file("test_signed_apps/trusted_ca1.der", false); + var der = readFile(root_cert_der); + certdb.addCert(der, ",,CTu", "test-root"); + run_next_test(); +} + +function check_open_result(name, expectedRv) { + return function openSignedJARFileCallback(rv, aZipReader, aSignerCert) { + do_print("openSignedJARFileCallback called for " + name); + do_check_eq(rv, expectedRv); + do_check_eq(aZipReader != null, Components.isSuccessCode(expectedRv)); + do_check_eq(aSignerCert != null, Components.isSuccessCode(expectedRv)); + run_next_test(); + }; +} + +function original_app_path(test_name) { + return do_get_file("test_signed_apps/" + test_name + ".zip", false); +} + +function tampered_app_path(test_name) { + return FileUtils.getFile("TmpD", ["test_signed_app-" + test_name + ".zip"]); +} + +add_test(function () { + certdb.openSignedJARFileAsync(original_app_path("valid"), + check_open_result("valid", Cr.NS_OK)); +}); + +add_test(function () { + certdb.openSignedJARFileAsync(original_app_path("unsigned"), + check_open_result("unsigned", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)); +}); + +add_test(function () { + // XXX: NSS has many possible error codes for this, e.g. + // SEC_ERROR_UNTRUSTED_ISSUER and others are also reasonable. Future versions + // of NSS may return one of these alternate errors; in that case, we need to + // update this test. + // + // XXX (bug 812089): Cr.NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER is undefined. + // + // XXX: Cannot use operator| instead of operator+ to combine bits because + // bit 31 trigger's JavaScript's crazy interpretation of the numbers as + // two's complement negative integers. + const NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER = 0x80000000 /* unsigned (1 << 31) */ + + ( (0x45 + 21) << 16) + + (-(-0x2000 + 13) ); + certdb.openSignedJARFileAsync(original_app_path("unknown_issuer"), + check_open_result("unknown_issuer", + /*Cr.*/NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER)); +}); + +// Sanity check to ensure a no-op tampering gives a valid result +add_test(function () { + var tampered = tampered_app_path("identity_tampering"); + tamper(original_app_path("valid"), tampered, { }, []); + certdb.openSignedJARFileAsync(original_app_path("valid"), + check_open_result("identity_tampering", Cr.NS_OK)); +}); + +add_test(function () { + var tampered = tampered_app_path("missing_rsa"); + tamper(original_app_path("valid"), tampered, { "META-INF/A.RSA" : removeEntry }, []); + certdb.openSignedJARFileAsync(tampered, + check_open_result("missing_rsa", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)); +}); + +add_test(function () { + var tampered = tampered_app_path("missing_sf"); + tamper(original_app_path("valid"), tampered, { "META-INF/A.SF" : removeEntry }, []); + certdb.openSignedJARFileAsync(tampered, + check_open_result("missing_sf", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID)); +}); + +add_test(function () { + var tampered = tampered_app_path("missing_manifest_mf"); + tamper(original_app_path("valid"), tampered, { "META-INF/MANIFEST.MF" : removeEntry }, []); + certdb.openSignedJARFileAsync(tampered, + check_open_result("missing_manifest_mf", + Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID)); +}); + +add_test(function () { + var tampered = tampered_app_path("missing_entry"); + tamper(original_app_path("valid"), tampered, { "manifest.webapp" : removeEntry }, []); + certdb.openSignedJARFileAsync(tampered, + check_open_result("missing_entry", Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING)); +}); + +add_test(function () { + var tampered = tampered_app_path("truncated_entry"); + tamper(original_app_path("valid"), tampered, { "manifest.webapp" : truncateEntry }, []); + certdb.openSignedJARFileAsync(tampered, + check_open_result("truncated_entry", Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY)); +}); + +add_test(function () { + var tampered = tampered_app_path("unsigned_entry"); + tamper(original_app_path("valid"), tampered, {}, + [ { "name": "unsigned.txt", "content": "unsigned content!" } ]); + certdb.openSignedJARFileAsync(tampered, + check_open_result("unsigned_entry", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY)); +}); + +add_test(function () { + var tampered = tampered_app_path("unsigned_metainf_entry"); + tamper(original_app_path("valid"), tampered, {}, + [ { name: "META-INF/unsigned.txt", content: "unsigned content!" } ]); + certdb.openSignedJARFileAsync(tampered, + check_open_result("unsigned_metainf_entry", + Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY)); +}); + +// TODO: tampered MF, tampered SF +// TODO: too-large MF, too-large RSA, too-large SF +// TODO: MF and SF that end immediately after the last main header +// (no CR nor LF) +// TODO: broken headers to exercise the parser diff --git a/security/manager/ssl/tests/unit/test_signed_apps/generate.sh b/security/manager/ssl/tests/unit/test_signed_apps/generate.sh new file mode 100644 index 000000000000..03642d0f8aea --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps/generate.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Usage: +# export NSS_PREFIX= \ +# PATH=$NSS_PREFIX/bin:$NSS_PREFIX/lib:$PATH ./generate.sh + +set -e + +srcdir=$PWD +tmpdir=$TMP/test_signed_apps +noisefile=$tmpdir/noise +passwordfile=$tmpdir/passwordfile +ca_responses=$tmpdir/ca_responses +ee_responses=$tmpdir/ee_responses + +replace_zip() +{ + zip=$1 # must be an absolute path + new_contents_dir=$2 + + rm -f $zip + oldpwd=$PWD + cd $new_contents_dir && zip -9 -o -r $zip * + cd $oldpwd +} + +sign_app_with_new_cert() +{ + label=$1 + unsigned_zip=$2 + out_signed_zip=$3 + + # XXX: We cannot give the trusted and untrusted versions of the certs the same + # subject names because otherwise we'll run into + # SEC_ERROR_REUSED_ISSUER_AND_SERIAL. + org="O=Examplla Corporation,L=Mountain View,ST=CA,C=US" + ca_subj="CN=Examplla Root CA $label,OU=Examplla CA,$org" + ee_subj="CN=Examplla Marketplace App Signing $label,OU=Examplla Marketplace App Signing,$org" + + db=$tmpdir/$label + mkdir -p $db + certutil -d $db -N -f $passwordfile + make_cert="certutil -d $db -f $passwordfile -S -v 3 -g 2048 -Z SHA256 \ + -z $noisefile -y 3 -2 --extKeyUsage critical,codeSigning" + $make_cert -n ca1 -m 1 -s "$ca_subj" \ + --keyUsage critical,certSigning -t ",,CTu" -x < $ca_responses + $make_cert -n ee1 -c ca1 -m 2 -s "$ee_subj" \ + --keyUsage critical,digitalSignature -t ",,," < $ee_responses + + # In case we want to inspect the generated certs + certutil -d $db -L -n ca1 -r -o $db/ca1.der + certutil -d $db -L -n ee1 -r -o $db/ee1.der + + python sign_b2g_app.py -d $db -f $passwordfile -k ee1 -i $unsigned_zip -o $out_signed_zip +} + +rm -Rf $tmpdir +mkdir $tmpdir + +echo password1 > $passwordfile +head --bytes 32 /dev/urandom > $noisefile + +# XXX: certutil cannot generate basic constraints without interactive prompts, +# so we need to build response files to answer its questions +# XXX: certutil cannot generate AKI/SKI without interactive prompts so we just +# skip them. +echo y > $ca_responses # Is this a CA? +echo >> $ca_responses # Accept default path length constraint (no constraint) +echo y >> $ca_responses # Is this a critical constraint? +echo n > $ee_responses # Is this a CA? +echo >> $ee_responses # Accept default path length constraint (no constraint) +echo y >> $ee_responses # Is this a critical constraint? + +replace_zip $srcdir/unsigned.zip $srcdir/simple + +sign_app_with_new_cert trusted $srcdir/unsigned.zip $srcdir/valid.zip +sign_app_with_new_cert untrusted $srcdir/unsigned.zip $srcdir/unknown_issuer.zip +certutil -d $tmpdir/trusted -f $passwordfile -L -n ca1 -r -o $srcdir/trusted_ca1.der + +rm -Rf $tmpdir diff --git a/security/manager/ssl/tests/unit/test_signed_apps/nss_ctypes.py b/security/manager/ssl/tests/unit/test_signed_apps/nss_ctypes.py new file mode 100644 index 000000000000..ba70fbc24f72 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps/nss_ctypes.py @@ -0,0 +1,124 @@ +from ctypes import * +import os +import sys + +if sys.platform == 'darwin': + libprefix = "lib" + libsuffix = ".dylib" +elif os.name == 'posix': + libprefix = "lib" + libsuffix = ".so" +else: # assume windows + libprefix = "" + libsuffix = ".dll" + +nspr = cdll.LoadLibrary(libprefix + "nspr4" + libsuffix) +nss = cdll.LoadLibrary(libprefix + "nss3" + libsuffix) +smime = cdll.LoadLibrary(libprefix + "smime3" + libsuffix) + +nspr.PR_GetError.argtypes = [] +nspr.PR_GetError.restype = c_int32 +nspr.PR_ErrorToName.argtypes = [c_int32] +nspr.PR_ErrorToName.restype = c_char_p + +def raise_if_not_SECSuccess(rv): + SECSuccess = 0 + if (rv != SECSuccess): + raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError())) + +def raise_if_NULL(p): + if not p: + raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError())) + return p + +PRBool = c_int +SECStatus = c_int + +# from secoidt.h +SEC_OID_SHA1 = 4 + +# from certt.h +certUsageObjectSigner = 6 + +class SECItem(Structure): + _fields_ = [("type", c_int), + ("data", c_char_p), + ("len", c_uint)] + +nss.NSS_Init.argtypes = [c_char_p] +nss.NSS_Init.restype = SECStatus +def NSS_Init(db_dir): + nss.NSS_Init.argtypes = [c_char_p] + nss.NSS_Init.restype = SECStatus + raise_if_not_SECSuccess(nss.NSS_Init(db_dir)) + +nss.NSS_Shutdown.argtypes = [] +nss.NSS_Shutdown.restype = SECStatus +def NSS_Shutdown(): + raise_if_not_SECSuccess(nss.NSS_Shutdown()) + +PK11PasswordFunc = CFUNCTYPE(c_char_p, c_void_p, PRBool, c_char_p) + +# pass the result of this as the wincx parameter when a wincx is required +nss.PK11_SetPasswordFunc.argtypes = [PK11PasswordFunc] +nss.PK11_SetPasswordFunc.restype = None +def SetPasswordContext(password): + def callback(slot, retry, arg): + return password + wincx = PK11PasswordFunc(callback) + nss.PK11_SetPasswordFunc(wincx) + return wincx + +nss.CERT_GetDefaultCertDB.argtypes = [] +nss.CERT_GetDefaultCertDB.restype = c_void_p +def CERT_GetDefaultCertDB(): + return raise_if_NULL(nss.CERT_GetDefaultCertDB()) + +nss.PK11_FindCertFromNickname.argtypes = [c_char_p, c_void_p] +nss.PK11_FindCertFromNickname.restype = c_void_p +def PK11_FindCertFromNickname(nickname, wincx): + return raise_if_NULL(nss.PK11_FindCertFromNickname(nickname, wincx)) + +nss.CERT_DestroyCertificate.argtypes = [c_void_p] +nss.CERT_DestroyCertificate.restype = None +def CERT_DestroyCertificate(cert): + nss.CERT_DestroyCertificate(cert) + +smime.SEC_PKCS7CreateSignedData.argtypes = [c_void_p, c_int, c_void_p, + c_int, c_void_p, + c_void_p, c_void_p] +smime.SEC_PKCS7CreateSignedData.restype = c_void_p +def SEC_PKCS7CreateSignedData(cert, certusage, certdb, digestalg, digest, wincx): + item = SECItem(0, c_char_p(digest), len(digest)) + return raise_if_NULL(smime.SEC_PKCS7CreateSignedData(cert, certusage, certdb, + digestalg, + pointer(item), + None, wincx)) + +smime.SEC_PKCS7AddSigningTime.argtypes = [c_void_p] +smime.SEC_PKCS7AddSigningTime.restype = SECStatus +def SEC_PKCS7AddSigningTime(p7): + raise_if_not_SECSuccess(smime.SEC_PKCS7AddSigningTime(p7)) + +smime.SEC_PKCS7IncludeCertChain.argtypes = [c_void_p, c_void_p] +smime.SEC_PKCS7IncludeCertChain.restype = SECStatus +def SEC_PKCS7IncludeCertChain(p7, wincx): + raise_if_not_SECSuccess(smime.SEC_PKCS7IncludeCertChain(p7, wincx)) + +SEC_PKCS7EncoderOutputCallback = CFUNCTYPE(None, c_void_p, c_void_p, c_long) +smime.SEC_PKCS7Encode.argtypes = [c_void_p, SEC_PKCS7EncoderOutputCallback, + c_void_p, c_void_p, c_void_p, c_void_p] +smime.SEC_PKCS7Encode.restype = SECStatus +def SEC_PKCS7Encode(p7, bulkkey, wincx): + outputChunks = [] + def callback(chunks, data, len): + outputChunks.append(string_at(data, len)) + callbackWrapper = SEC_PKCS7EncoderOutputCallback(callback) + raise_if_not_SECSuccess(smime.SEC_PKCS7Encode(p7, callbackWrapper, + None, None, None, wincx)) + return "".join(outputChunks) + +smime.SEC_PKCS7DestroyContentInfo.argtypes = [c_void_p] +smime.SEC_PKCS7DestroyContentInfo.restype = None +def SEC_PKCS7DestroyContentInfo(p7): + smime.SEC_PKCS7DestroyContentInfo(p7) diff --git a/security/manager/ssl/tests/unit/test_signed_apps/sign_b2g_app.py b/security/manager/ssl/tests/unit/test_signed_apps/sign_b2g_app.py new file mode 100644 index 000000000000..5505a685bf1e --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps/sign_b2g_app.py @@ -0,0 +1,144 @@ +import argparse +from base64 import b64encode +from hashlib import sha1 +import sys +import zipfile +import ctypes + +import nss_ctypes + +def nss_load_cert(nss_db_dir, nss_password, cert_nickname): + nss_ctypes.NSS_Init(nss_db_dir) + try: + wincx = nss_ctypes.SetPasswordContext(nss_password) + cert = nss_ctypes.PK11_FindCertFromNickname(cert_nickname, wincx) + return (wincx, cert) + except: + nss_ctypes.NSS_Shutdown() + raise + +def nss_create_detached_signature(cert, dataToSign, wincx): + certdb = nss_ctypes.CERT_GetDefaultCertDB() + p7 = nss_ctypes.SEC_PKCS7CreateSignedData(cert, + nss_ctypes.certUsageObjectSigner, + certdb, + nss_ctypes.SEC_OID_SHA1, + sha1(dataToSign).digest(), + wincx ) + try: + nss_ctypes.SEC_PKCS7AddSigningTime(p7) + nss_ctypes.SEC_PKCS7IncludeCertChain(p7, wincx) + return nss_ctypes.SEC_PKCS7Encode(p7, None, wincx) + finally: + nss_ctypes.SEC_PKCS7DestroyContentInfo(p7) + +def sign_zip(in_zipfile_name, out_zipfile_name, cert, wincx): + mf_entries = [] + seen_entries = set() + + # Change the limits in JarSignatureVerification.cpp when you change the limits + # here. + max_entry_uncompressed_len = 100 * 1024 * 1024 + max_total_uncompressed_len = 500 * 1024 * 1024 + max_entry_count = 100 * 1000 + max_entry_filename_len = 1024 + max_mf_len = max_entry_count * 50 + max_sf_len = 1024 + + total_uncompressed_len = 0 + entry_count = 0 + with zipfile.ZipFile(out_zipfile_name, 'w') as out_zip: + with zipfile.ZipFile(in_zipfile_name, 'r') as in_zip: + for entry_info in in_zip.infolist(): + name = entry_info.filename + + # Check for reserved and/or insane (potentially malicious) names + if name.endswith("/"): + pass + # Do nothing; we don't copy directory entries since they are just a + # waste of space. + elif name.lower().startswith("meta-inf/"): + # META-INF/* is reserved for our use + raise ValueError("META-INF entries are not allowed: %s" % (name)) + elif len(name) > max_entry_filename_len: + raise ValueError("Entry's filename is too long: %s" % (name)) + # TODO: elif name has invalid characters... + elif name in seen_entries: + # It is possible for a zipfile to have duplicate entries (with the exact + # same filenames). Python's zipfile module accepts them, but our zip + # reader in Gecko cannot do anything useful with them, and there's no + # sane reason for duplicate entries to exist, so reject them. + raise ValueError("Duplicate entry in input file: %s" % (name)) + else: + entry_count += 1 + if entry_count > max_entry_count: + raise ValueError("Too many entries in input archive") + + seen_entries.add(name) + + # Read in the input entry, but be careful to avoid going over the + # various limits we have, to minimize the likelihood that we'll run + # out of memory. Note that we can't use the length from entry_info + # because that might not be accurate if the input zip file is + # maliciously crafted to contain misleading metadata. + with in_zip.open(name, 'r') as entry_file: + contents = entry_file.read(max_entry_uncompressed_len + 1) + if len(contents) > max_entry_uncompressed_len: + raise ValueError("Entry is too large: %s" % (name)) + total_uncompressed_len += len(contents) + if total_uncompressed_len > max_total_uncompressed_len: + raise ValueError("Input archive is too large") + + # Copy the entry, using the same compression as used in the input file + out_zip.writestr(entry_info, contents) + + # Add the entry to the manifest we're building + mf_entries.append('Name: %s\nSHA1-Digest: %s\n' + % (name, b64encode(sha1(contents).digest()))) + + mf_contents = 'Manifest-Version: 1.0\n\n' + '\n'.join(mf_entries) + if len(mf_contents) > max_mf_len: + raise ValueError("Generated MANIFEST.MF is too large: %d" % (len(mf_contents))) + + sf_contents = ('Signature-Version: 1.0\nSHA1-Digest-Manifest: %s\n' + % (b64encode(sha1(mf_contents).digest()))) + if len(sf_contents) > max_sf_len: + raise ValueError("Generated SIGNATURE.SF is too large: %d" + % (len(mf_contents))) + + p7 = nss_create_detached_signature(cert, sf_contents, wincx) + + # write the signature, SF, and MF + out_zip.writestr("META-INF/A.RSA", p7, zipfile.ZIP_DEFLATED) + out_zip.writestr("META-INF/A.SF", sf_contents, zipfile.ZIP_DEFLATED) + out_zip.writestr("META-INF/MANIFEST.MF", mf_contents, zipfile.ZIP_DEFLATED) + +def main(): + parser = argparse.ArgumentParser(description='Sign a B2G app.') + parser.add_argument('-d', action='store', + required=True, help='NSS database directory') + parser.add_argument('-f', action='store', + type=argparse.FileType('rb'), + required=True, help='password file') + parser.add_argument('-k', action='store', + required=True, help="nickname of signing cert.") + parser.add_argument('-i', action='store', type=argparse.FileType('rb'), + required=True, help="input JAR file (unsigned)") + parser.add_argument('-o', action='store', type=argparse.FileType('wb'), + required=True, help="output JAR file (signed)") + args = parser.parse_args() + + db_dir = args.d + password = args.f.readline().strip() + cert_nickname = args.k + + (wincx, cert) = nss_load_cert(db_dir, password, cert_nickname) + try: + sign_zip(args.i, args.o, cert, wincx) + return 0 + finally: + nss_ctypes.CERT_DestroyCertificate(cert) + nss_ctypes.NSS_Shutdown() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/security/manager/ssl/tests/unit/test_signed_apps/simple/icon-128.png b/security/manager/ssl/tests/unit/test_signed_apps/simple/icon-128.png new file mode 100644 index 000000000000..d6fd07a4167d Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/simple/icon-128.png differ diff --git a/security/manager/ssl/tests/unit/test_signed_apps/simple/index.html b/security/manager/ssl/tests/unit/test_signed_apps/simple/index.html new file mode 100644 index 000000000000..2acf635e85d3 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps/simple/index.html @@ -0,0 +1,6 @@ + + +Simple App +

This is a Simple App. + + diff --git a/security/manager/ssl/tests/unit/test_signed_apps/simple/manifest.webapp b/security/manager/ssl/tests/unit/test_signed_apps/simple/manifest.webapp new file mode 100644 index 000000000000..1a3f5aa1352f --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps/simple/manifest.webapp @@ -0,0 +1,6 @@ +{ name: "Simple App" +, description: "A Simple Open Web App" +, launch_path: "/index.html" +, icons: { "128" : "icon-128.png" } +, installs_allowed_from: [ "https://marketplace.mozilla.com" ] +} diff --git a/security/manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der b/security/manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der new file mode 100644 index 000000000000..ac4a0976c5bb Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der differ diff --git a/security/manager/ssl/tests/unit/test_signed_apps/unknown_issuer.zip b/security/manager/ssl/tests/unit/test_signed_apps/unknown_issuer.zip new file mode 100644 index 000000000000..4b0fa51ed9b4 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/unknown_issuer.zip differ diff --git a/security/manager/ssl/tests/unit/test_signed_apps/unsigned.zip b/security/manager/ssl/tests/unit/test_signed_apps/unsigned.zip new file mode 100644 index 000000000000..7ba7b4591478 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/unsigned.zip differ diff --git a/security/manager/ssl/tests/unit/test_signed_apps/untrusted/examplla-app-signing-root-ca-1.der b/security/manager/ssl/tests/unit/test_signed_apps/untrusted/examplla-app-signing-root-ca-1.der new file mode 100644 index 000000000000..d98a88d1983f Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/untrusted/examplla-app-signing-root-ca-1.der differ diff --git a/security/manager/ssl/tests/unit/test_signed_apps/valid.zip b/security/manager/ssl/tests/unit/test_signed_apps/valid.zip new file mode 100644 index 000000000000..945a02558e33 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_apps/valid.zip differ diff --git a/security/manager/ssl/tests/unit/xpcshell.ini b/security/manager/ssl/tests/unit/xpcshell.ini index cf022ae614a8..28b7f85e1fb8 100644 --- a/security/manager/ssl/tests/unit/xpcshell.ini +++ b/security/manager/ssl/tests/unit/xpcshell.ini @@ -2,6 +2,7 @@ head = tail = +[test_signed_apps.js] [test_datasignatureverifier.js] # Bug 676972: test hangs consistently on Android skip-if = os == "android" diff --git a/xpcom/base/ErrorList.h b/xpcom/base/ErrorList.h index 50d9f61d4224..c16ce010dc62 100644 --- a/xpcom/base/ErrorList.h +++ b/xpcom/base/ErrorList.h @@ -859,6 +859,19 @@ ERROR(NS_ERROR_DOM_FILEHANDLE_READ_ONLY_ERR, FAILURE(5)), #undef MODULE + /* ======================================================================= */ + /* 35: NS_ERROR_MODULE_SIGNED_JAR */ + /* ======================================================================= */ +#define MODULE NS_ERROR_MODULE_SIGNED_JAR + ERROR(NS_ERROR_SIGNED_JAR_NOT_SIGNED, FAILURE(1)), + ERROR(NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY, FAILURE(2)), + ERROR(NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY, FAILURE(3)), + ERROR(NS_ERROR_SIGNED_JAR_ENTRY_MISSING, FAILURE(4)), + ERROR(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE, FAILURE(5)), + ERROR(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE, FAILURE(6)), + ERROR(NS_ERROR_SIGNED_JAR_ENTRY_INVALID, FAILURE(7)), + ERROR(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, FAILURE(8)), +#undef MODULE /* ======================================================================= */ /* 51: NS_ERROR_MODULE_GENERAL */ diff --git a/xpcom/base/nsError.h b/xpcom/base/nsError.h index e43d62eea9d2..819d436604ad 100644 --- a/xpcom/base/nsError.h +++ b/xpcom/base/nsError.h @@ -69,6 +69,7 @@ #define NS_ERROR_MODULE_DOM_FILE 32 #define NS_ERROR_MODULE_DOM_INDEXEDDB 33 #define NS_ERROR_MODULE_DOM_FILEHANDLE 34 +#define NS_ERROR_MODULE_SIGNED_JAR 35 /* NS_ERROR_MODULE_GENERAL should be used by modules that do not * care if return code values overlap. Callers of methods that