michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #ifdef MOZ_LOGGING michael@0: #define FORCE_PR_LOG 1 michael@0: #endif michael@0: michael@0: #include "nsNSSCertificateDB.h" michael@0: michael@0: #include "pkix/pkix.h" michael@0: #include "mozilla/RefPtr.h" michael@0: #include "CryptoTask.h" michael@0: #include "AppTrustDomain.h" michael@0: #include "nsComponentManagerUtils.h" michael@0: #include "nsCOMPtr.h" michael@0: #include "nsHashKeys.h" michael@0: #include "nsIFile.h" michael@0: #include "nsIInputStream.h" michael@0: #include "nsIStringEnumerator.h" michael@0: #include "nsIZipReader.h" michael@0: #include "nsNSSCertificate.h" michael@0: #include "nsProxyRelease.h" michael@0: #include "nsString.h" michael@0: #include "nsTHashtable.h" michael@0: #include "ScopedNSSTypes.h" michael@0: michael@0: #include "base64.h" michael@0: #include "certdb.h" michael@0: #include "secmime.h" michael@0: #include "plstr.h" michael@0: #include "prlog.h" michael@0: michael@0: using namespace mozilla::pkix; michael@0: using namespace mozilla; michael@0: using namespace mozilla::psm; michael@0: michael@0: #ifdef PR_LOGGING michael@0: extern PRLogModuleInfo* gPIPNSSLog; michael@0: #endif michael@0: michael@0: namespace { michael@0: michael@0: // Finds exactly one (signature metadata) entry that matches the given michael@0: // search pattern, and then load it. Fails if there are no matches or if michael@0: // there is more than one match. If bugDigest is not null then on success michael@0: // bufDigest will contain the SHA-1 digeset of the entry. michael@0: nsresult michael@0: FindAndLoadOneEntry(nsIZipReader * zip, michael@0: const nsACString & searchPattern, michael@0: /*out*/ nsACString & filename, michael@0: /*out*/ SECItem & buf, michael@0: /*optional, out*/ Digest * bufDigest) michael@0: { michael@0: nsCOMPtr files; michael@0: nsresult rv = zip->FindEntries(searchPattern, getter_AddRefs(files)); michael@0: if (NS_FAILED(rv) || !files) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: bool more; michael@0: rv = files->HasMore(&more); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (!more) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: rv = files->GetNext(filename); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // Check if there is more than one match, if so then error! michael@0: rv = files->HasMore(&more); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (more) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: nsCOMPtr stream; michael@0: rv = zip->GetInputStream(filename, getter_AddRefs(stream)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // The size returned by Available() might be inaccurate so we need to check michael@0: // that Available() matches up with the actual length of the file. michael@0: uint64_t len64; michael@0: rv = stream->Available(&len64); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: michael@0: // Cap the maximum accepted size of signature-related files at 1MB (which is michael@0: // still crazily huge) to avoid OOM. The uncompressed length of an entry can be michael@0: // hundreds of times larger than the compressed version, especially if michael@0: // someone has speifically crafted the entry to cause OOM or to consume michael@0: // massive amounts of disk space. michael@0: // michael@0: // Also, keep in mind bug 164695 and that we must leave room for michael@0: // null-terminating the buffer. michael@0: static const uint32_t MAX_LENGTH = 1024 * 1024; michael@0: static_assert(MAX_LENGTH < UINT32_MAX, "MAX_LENGTH < UINT32_MAX"); michael@0: NS_ENSURE_TRUE(len64 < MAX_LENGTH, NS_ERROR_FILE_CORRUPTED); michael@0: NS_ENSURE_TRUE(len64 < UINT32_MAX, NS_ERROR_FILE_CORRUPTED); // bug 164695 michael@0: SECITEM_AllocItem(buf, static_cast(len64 + 1)); michael@0: michael@0: // buf.len == len64 + 1. We attempt to read len64 + 1 bytes instead of len64, michael@0: // so that we can check whether the metadata in the ZIP for the entry is michael@0: // incorrect. michael@0: uint32_t bytesRead; michael@0: rv = stream->Read(char_ptr_cast(buf.data), buf.len, &bytesRead); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (bytesRead != len64) { michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; michael@0: } michael@0: michael@0: buf.data[buf.len - 1] = 0; // null-terminate michael@0: michael@0: if (bufDigest) { michael@0: rv = bufDigest->DigestBuf(SEC_OID_SHA1, buf.data, buf.len - 1); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Verify the digest of an entry. We avoid loading the entire entry into memory michael@0: // at once, which would require memory in proportion to the size of the largest michael@0: // entry. Instead, we require only a small, fixed amount of memory. michael@0: // michael@0: // @param digestFromManifest The digest that we're supposed to check the file's michael@0: // contents against, from the manifest michael@0: // @param buf A scratch buffer that we use for doing the I/O, which must have michael@0: // already been allocated. The size of this buffer is the unit michael@0: // size of our I/O. michael@0: nsresult michael@0: VerifyEntryContentDigest(nsIZipReader * zip, const nsACString & aFilename, michael@0: const SECItem & digestFromManifest, SECItem & buf) michael@0: { michael@0: MOZ_ASSERT(buf.len > 0); michael@0: if (digestFromManifest.len != SHA1_LENGTH) michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: michael@0: nsresult rv; michael@0: michael@0: nsCOMPtr stream; michael@0: rv = zip->GetInputStream(aFilename, getter_AddRefs(stream)); michael@0: if (NS_FAILED(rv)) { michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; michael@0: } michael@0: michael@0: uint64_t len64; michael@0: rv = stream->Available(&len64); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (len64 > UINT32_MAX) { michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE; michael@0: } michael@0: michael@0: ScopedPK11Context digestContext(PK11_CreateDigestContext(SEC_OID_SHA1)); michael@0: if (!digestContext) { michael@0: return PRErrorCode_to_nsresult(PR_GetError()); michael@0: } michael@0: michael@0: rv = MapSECStatus(PK11_DigestBegin(digestContext)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: uint64_t totalBytesRead = 0; michael@0: for (;;) { michael@0: uint32_t bytesRead; michael@0: rv = stream->Read(char_ptr_cast(buf.data), buf.len, &bytesRead); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (bytesRead == 0) { michael@0: break; // EOF michael@0: } michael@0: michael@0: totalBytesRead += bytesRead; michael@0: if (totalBytesRead >= UINT32_MAX) { michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE; michael@0: } michael@0: michael@0: rv = MapSECStatus(PK11_DigestOp(digestContext, buf.data, bytesRead)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: if (totalBytesRead != len64) { michael@0: // The metadata we used for Available() doesn't match the actual size of michael@0: // the entry. michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; michael@0: } michael@0: michael@0: // Verify that the digests match. michael@0: Digest digest; michael@0: rv = digest.End(SEC_OID_SHA1, digestContext); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (SECITEM_CompareItem(&digestFromManifest, &digest.get()) != SECEqual) { michael@0: return NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // On input, nextLineStart is the start of the current line. On output, michael@0: // nextLineStart is the start of the next line. michael@0: nsresult michael@0: ReadLine(/*in/out*/ const char* & nextLineStart, /*out*/ nsCString & line, michael@0: bool allowContinuations = true) michael@0: { michael@0: line.Truncate(); michael@0: size_t previousLength = 0; michael@0: size_t currentLength = 0; michael@0: for (;;) { michael@0: const char* eol = PL_strpbrk(nextLineStart, "\r\n"); michael@0: michael@0: if (!eol) { // Reached end of file before newline michael@0: eol = nextLineStart + strlen(nextLineStart); michael@0: } michael@0: michael@0: previousLength = currentLength; michael@0: line.Append(nextLineStart, eol - nextLineStart); michael@0: currentLength = line.Length(); michael@0: michael@0: // The spec says "No line may be longer than 72 bytes (not characters)" michael@0: // in its UTF8-encoded form. michael@0: static const size_t lineLimit = 72; michael@0: if (currentLength - previousLength > lineLimit) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: // The spec says: "Implementations should support 65535-byte michael@0: // (not character) header values..." michael@0: if (currentLength > 65535) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: if (*eol == '\r') { michael@0: ++eol; michael@0: } michael@0: if (*eol == '\n') { michael@0: ++eol; michael@0: } michael@0: michael@0: nextLineStart = eol; michael@0: michael@0: if (*eol != ' ') { michael@0: // not a continuation michael@0: return NS_OK; michael@0: } michael@0: michael@0: // continuation michael@0: if (!allowContinuations) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: ++nextLineStart; // skip space and keep appending michael@0: } michael@0: } michael@0: michael@0: // The header strings are defined in the JAR specification. michael@0: #define JAR_MF_SEARCH_STRING "(M|/M)ETA-INF/(M|m)(ANIFEST|anifest).(MF|mf)$" michael@0: #define JAR_SF_SEARCH_STRING "(M|/M)ETA-INF/*.(SF|sf)$" michael@0: #define JAR_RSA_SEARCH_STRING "(M|/M)ETA-INF/*.(RSA|rsa)$" michael@0: #define JAR_MF_HEADER "Manifest-Version: 1.0" michael@0: #define JAR_SF_HEADER "Signature-Version: 1.0" michael@0: michael@0: nsresult michael@0: ParseAttribute(const nsAutoCString & curLine, michael@0: /*out*/ nsAutoCString & attrName, michael@0: /*out*/ nsAutoCString & attrValue) michael@0: { michael@0: // Find the colon that separates the name from the value. michael@0: int32_t colonPos = curLine.FindChar(':'); michael@0: if (colonPos == kNotFound) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: // set attrName to the name, skipping spaces between the name and colon michael@0: int32_t nameEnd = colonPos; michael@0: for (;;) { michael@0: if (nameEnd == 0) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; // colon with no name michael@0: } michael@0: if (curLine[nameEnd - 1] != ' ') michael@0: break; michael@0: --nameEnd; michael@0: } michael@0: curLine.Left(attrName, nameEnd); michael@0: michael@0: // Set attrValue to the value, skipping spaces between the colon and the michael@0: // value. The value may be empty. michael@0: int32_t valueStart = colonPos + 1; michael@0: int32_t curLineLength = curLine.Length(); michael@0: while (valueStart != curLineLength && curLine[valueStart] == ' ') { michael@0: ++valueStart; michael@0: } michael@0: curLine.Right(attrValue, curLineLength - valueStart); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Parses the version line of the MF or SF header. michael@0: nsresult michael@0: CheckManifestVersion(const char* & nextLineStart, michael@0: const nsACString & expectedHeader) michael@0: { michael@0: // The JAR spec says: "Manifest-Version and Signature-Version must be first, michael@0: // and in exactly that case (so that they can be recognized easily as magic michael@0: // strings)." michael@0: nsAutoCString curLine; michael@0: nsresult rv = ReadLine(nextLineStart, curLine, false); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: if (!curLine.Equals(expectedHeader)) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Parses a signature file (SF) as defined in the JDK 8 JAR Specification. michael@0: // michael@0: // The SF file *must* contain exactly one SHA1-Digest-Manifest attribute in michael@0: // the main section. All other sections are ignored. This means that this will michael@0: // NOT parse old-style signature files that have separate digests per entry. michael@0: // The JDK8 x-Digest-Manifest variant is better because: michael@0: // michael@0: // (1) It allows us to follow the principle that we should minimize the michael@0: // processing of data that we do before we verify its signature. In michael@0: // particular, with the x-Digest-Manifest style, we can verify the digest michael@0: // of MANIFEST.MF before we parse it, which prevents malicious JARs michael@0: // exploiting our MANIFEST.MF parser. michael@0: // (2) It is more time-efficient and space-efficient to have one michael@0: // x-Digest-Manifest instead of multiple x-Digest values. michael@0: // michael@0: // In order to get benefit (1), we do NOT implement the fallback to the older michael@0: // mechanism as the spec requires/suggests. Also, for simplity's sake, we only michael@0: // support exactly one SHA1-Digest-Manifest attribute, and no other michael@0: // algorithms. michael@0: // michael@0: // filebuf must be null-terminated. On output, mfDigest will contain the michael@0: // decoded value of SHA1-Digest-Manifest. michael@0: nsresult michael@0: ParseSF(const char* filebuf, /*out*/ SECItem & mfDigest) michael@0: { michael@0: nsresult rv; michael@0: michael@0: const char* nextLineStart = filebuf; michael@0: rv = CheckManifestVersion(nextLineStart, NS_LITERAL_CSTRING(JAR_SF_HEADER)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Find SHA1-Digest-Manifest michael@0: for (;;) { michael@0: nsAutoCString curLine; michael@0: rv = ReadLine(nextLineStart, curLine); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: if (curLine.Length() == 0) { michael@0: // End of main section (blank line or end-of-file), and no michael@0: // SHA1-Digest-Manifest found. michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: nsAutoCString attrName; michael@0: nsAutoCString attrValue; michael@0: rv = ParseAttribute(curLine, attrName, attrValue); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: if (attrName.LowerCaseEqualsLiteral("sha1-digest-manifest")) { michael@0: rv = MapSECStatus(ATOB_ConvertAsciiToItem(&mfDigest, attrValue.get())); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: // There could be multiple SHA1-Digest-Manifest attributes, which michael@0: // would be an error, but it's better to just skip any erroneous michael@0: // duplicate entries rather than trying to detect them, because: michael@0: // michael@0: // (1) It's simpler, and simpler generally means more secure michael@0: // (2) An attacker can't make us accept a JAR we would otherwise michael@0: // reject just by adding additional SHA1-Digest-Manifest michael@0: // attributes. michael@0: break; michael@0: } michael@0: michael@0: // ignore unrecognized attributes michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Parses MANIFEST.MF. The filenames of all entries will be returned in michael@0: // mfItems. buf must be a pre-allocated scratch buffer that is used for doing michael@0: // I/O. michael@0: nsresult michael@0: ParseMF(const char* filebuf, nsIZipReader * zip, michael@0: /*out*/ nsTHashtable & mfItems, michael@0: ScopedAutoSECItem & buf) michael@0: { michael@0: nsresult rv; michael@0: michael@0: const char* nextLineStart = filebuf; michael@0: michael@0: rv = CheckManifestVersion(nextLineStart, NS_LITERAL_CSTRING(JAR_MF_HEADER)); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: // Skip the rest of the header section, which ends with a blank line. michael@0: { michael@0: nsAutoCString line; michael@0: do { michael@0: rv = ReadLine(nextLineStart, line); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: } while (line.Length() > 0); michael@0: michael@0: // Manifest containing no file entries is OK, though useless. michael@0: if (*nextLineStart == '\0') { michael@0: return NS_OK; michael@0: } michael@0: } michael@0: michael@0: nsAutoCString curItemName; michael@0: ScopedAutoSECItem digest; michael@0: michael@0: for (;;) { michael@0: nsAutoCString curLine; michael@0: rv = ReadLine(nextLineStart, curLine); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (curLine.Length() == 0) { michael@0: // end of section (blank line or end-of-file) michael@0: michael@0: if (curItemName.Length() == 0) { michael@0: // '...Each section must start with an attribute with the name as michael@0: // "Name",...', so every section must have a Name attribute. michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: if (digest.len == 0) { michael@0: // We require every entry to have a digest, since we require every michael@0: // entry to be signed and we don't allow duplicate entries. michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: if (mfItems.Contains(curItemName)) { michael@0: // Duplicate entry michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: // Verify that the entry's content digest matches the digest from this michael@0: // MF section. michael@0: rv = VerifyEntryContentDigest(zip, curItemName, digest, buf); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: mfItems.PutEntry(curItemName); michael@0: michael@0: if (*nextLineStart == '\0') // end-of-file michael@0: break; michael@0: michael@0: // reset so we know we haven't encountered either of these for the next michael@0: // item yet. michael@0: curItemName.Truncate(); michael@0: digest.reset(); michael@0: michael@0: continue; // skip the rest of the loop below michael@0: } michael@0: michael@0: nsAutoCString attrName; michael@0: nsAutoCString attrValue; michael@0: rv = ParseAttribute(curLine, attrName, attrValue); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: // Lines to look for: michael@0: michael@0: // (1) Digest: michael@0: if (attrName.LowerCaseEqualsLiteral("sha1-digest")) michael@0: { michael@0: if (digest.len > 0) // multiple SHA1 digests in section michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: michael@0: rv = MapSECStatus(ATOB_ConvertAsciiToItem(&digest, attrValue.get())); michael@0: if (NS_FAILED(rv)) michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: michael@0: continue; michael@0: } michael@0: michael@0: // (2) Name: associates this manifest section with a file in the jar. michael@0: if (attrName.LowerCaseEqualsLiteral("name")) michael@0: { michael@0: if (MOZ_UNLIKELY(curItemName.Length() > 0)) // multiple names in section michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: michael@0: if (MOZ_UNLIKELY(attrValue.Length() == 0)) michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: michael@0: curItemName = attrValue; michael@0: michael@0: continue; michael@0: } michael@0: michael@0: // (3) Magic: the only other must-understand attribute michael@0: if (attrName.LowerCaseEqualsLiteral("magic")) { michael@0: // We don't understand any magic, so we can't verify an entry that michael@0: // requires magic. Since we require every entry to have a valid michael@0: // signature, we have no choice but to reject the entry. michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: // unrecognized attributes must be ignored michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: VerifySignature(AppTrustedRoot trustedRoot, michael@0: const SECItem& buffer, const SECItem& detachedDigest, michael@0: /*out*/ mozilla::pkix::ScopedCERTCertList& builtChain) michael@0: { michael@0: mozilla::pkix::ScopedPtr michael@0: cmsMsg(NSS_CMSMessage_CreateFromDER(const_cast(&buffer), nullptr, michael@0: nullptr, nullptr, nullptr, nullptr, michael@0: nullptr)); michael@0: if (!cmsMsg) { michael@0: return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING; michael@0: } michael@0: michael@0: if (!NSS_CMSMessage_IsSigned(cmsMsg.get())) { michael@0: PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("CMS message isn't signed")); michael@0: return NS_ERROR_CMS_VERIFY_NOT_SIGNED; michael@0: } michael@0: michael@0: NSSCMSContentInfo* cinfo = NSS_CMSMessage_ContentLevel(cmsMsg.get(), 0); michael@0: if (!cinfo) { michael@0: return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO; michael@0: } michael@0: michael@0: // signedData is non-owning michael@0: NSSCMSSignedData* signedData = michael@0: reinterpret_cast(NSS_CMSContentInfo_GetContent(cinfo)); michael@0: if (!signedData) { michael@0: return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO; michael@0: } michael@0: michael@0: // Set digest value. michael@0: if (NSS_CMSSignedData_SetDigestValue(signedData, SEC_OID_SHA1, michael@0: const_cast(&detachedDigest))) { michael@0: return NS_ERROR_CMS_VERIFY_BAD_DIGEST; michael@0: } michael@0: michael@0: // Parse the certificates into CERTCertificate objects held in memory, so that michael@0: // AppTrustDomain will be able to find them during path building. michael@0: mozilla::pkix::ScopedCERTCertList certs(CERT_NewCertList()); michael@0: if (!certs) { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: if (signedData->rawCerts) { michael@0: for (size_t i = 0; signedData->rawCerts[i]; ++i) { michael@0: mozilla::pkix::ScopedCERTCertificate michael@0: cert(CERT_NewTempCertificate(CERT_GetDefaultCertDB(), michael@0: signedData->rawCerts[i], nullptr, false, michael@0: true)); michael@0: // Skip certificates that fail to parse michael@0: if (cert) { michael@0: if (CERT_AddCertToListTail(certs.get(), cert.get()) == SECSuccess) { michael@0: cert.release(); // ownership transfered michael@0: } else { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Get the end-entity certificate. michael@0: int numSigners = NSS_CMSSignedData_SignerInfoCount(signedData); michael@0: if (NS_WARN_IF(numSigners != 1)) { michael@0: return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING; michael@0: } michael@0: // signer is non-owning. michael@0: NSSCMSSignerInfo* signer = NSS_CMSSignedData_GetSignerInfo(signedData, 0); michael@0: if (NS_WARN_IF(!signer)) { michael@0: return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING; michael@0: } michael@0: // cert is signerCert michael@0: CERTCertificate* signerCert = michael@0: NSS_CMSSignerInfo_GetSigningCertificate(signer, CERT_GetDefaultCertDB()); michael@0: if (!signerCert) { michael@0: return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING; michael@0: } michael@0: michael@0: // Verify certificate. michael@0: AppTrustDomain trustDomain(nullptr); // TODO: null pinArg michael@0: if (trustDomain.SetTrustedRoot(trustedRoot) != SECSuccess) { michael@0: return MapSECStatus(SECFailure); michael@0: } michael@0: if (BuildCertChain(trustDomain, signerCert, PR_Now(), MustBeEndEntity, michael@0: KeyUsage::digitalSignature, michael@0: SEC_OID_EXT_KEY_USAGE_CODE_SIGN, michael@0: SEC_OID_X509_ANY_POLICY, nullptr, builtChain) michael@0: != SECSuccess) { michael@0: return MapSECStatus(SECFailure); michael@0: } michael@0: michael@0: // See NSS_CMSContentInfo_GetContentTypeOID, which isn't exported from NSS. michael@0: SECOidData* contentTypeOidData = michael@0: SECOID_FindOID(&signedData->contentInfo.contentType); michael@0: if (!contentTypeOidData) { michael@0: return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING; michael@0: } michael@0: michael@0: return MapSECStatus(NSS_CMSSignerInfo_Verify(signer, michael@0: const_cast(&detachedDigest), michael@0: &contentTypeOidData->oid)); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, michael@0: /*out, optional */ nsIZipReader** aZipReader, michael@0: /*out, optional */ nsIX509Cert3** aSignerCert) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aJarFile); michael@0: michael@0: if (aZipReader) { michael@0: *aZipReader = nullptr; michael@0: } michael@0: michael@0: if (aSignerCert) { michael@0: *aSignerCert = nullptr; michael@0: } michael@0: michael@0: nsresult rv; michael@0: michael@0: static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); michael@0: nsCOMPtr zip = do_CreateInstance(kZipReaderCID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = zip->Open(aJarFile); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // Signature (RSA) file michael@0: nsAutoCString sigFilename; michael@0: ScopedAutoSECItem sigBuffer; michael@0: rv = FindAndLoadOneEntry(zip, nsLiteralCString(JAR_RSA_SEARCH_STRING), michael@0: sigFilename, sigBuffer, nullptr); michael@0: if (NS_FAILED(rv)) { michael@0: return NS_ERROR_SIGNED_JAR_NOT_SIGNED; michael@0: } michael@0: michael@0: // Signature (SF) file michael@0: nsAutoCString sfFilename; michael@0: ScopedAutoSECItem sfBuffer; michael@0: Digest sfCalculatedDigest; michael@0: rv = FindAndLoadOneEntry(zip, NS_LITERAL_CSTRING(JAR_SF_SEARCH_STRING), michael@0: sfFilename, sfBuffer, &sfCalculatedDigest); michael@0: if (NS_FAILED(rv)) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: sigBuffer.type = siBuffer; michael@0: mozilla::pkix::ScopedCERTCertList builtChain; michael@0: rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(), michael@0: builtChain); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: ScopedAutoSECItem mfDigest; michael@0: rv = ParseSF(char_ptr_cast(sfBuffer.data), mfDigest); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: // Manifest (MF) file michael@0: nsAutoCString mfFilename; michael@0: ScopedAutoSECItem manifestBuffer; michael@0: Digest mfCalculatedDigest; michael@0: rv = FindAndLoadOneEntry(zip, NS_LITERAL_CSTRING(JAR_MF_SEARCH_STRING), michael@0: mfFilename, manifestBuffer, &mfCalculatedDigest); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: if (SECITEM_CompareItem(&mfDigest, &mfCalculatedDigest.get()) != SECEqual) { michael@0: return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; michael@0: } michael@0: michael@0: // Allocate the I/O buffer only once per JAR, instead of once per entry, in michael@0: // order to minimize malloc/free calls and in order to avoid fragmenting michael@0: // memory. michael@0: ScopedAutoSECItem buf(128 * 1024); michael@0: michael@0: nsTHashtable items; michael@0: michael@0: rv = ParseMF(char_ptr_cast(manifestBuffer.data), zip, items, buf); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: // Verify every entry in the file. michael@0: nsCOMPtr entries; michael@0: rv = zip->FindEntries(EmptyCString(), getter_AddRefs(entries)); michael@0: if (NS_SUCCEEDED(rv) && !entries) { michael@0: rv = NS_ERROR_UNEXPECTED; michael@0: } michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: for (;;) { michael@0: bool hasMore; michael@0: rv = entries->HasMore(&hasMore); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (!hasMore) { michael@0: break; michael@0: } michael@0: michael@0: nsAutoCString entryFilename; michael@0: rv = entries->GetNext(entryFilename); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("Verifying digests for %s", michael@0: entryFilename.get())); michael@0: michael@0: // The files that comprise the signature mechanism are not covered by the michael@0: // signature. michael@0: // michael@0: // XXX: This is OK for a single signature, but doesn't work for michael@0: // multiple signatures, because the metadata for the other signatures michael@0: // is not signed either. michael@0: if (entryFilename == mfFilename || michael@0: entryFilename == sfFilename || michael@0: entryFilename == sigFilename) { michael@0: continue; michael@0: } michael@0: michael@0: if (entryFilename.Length() == 0) { michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; michael@0: } michael@0: michael@0: // Entries with names that end in "/" are directory entries, which are not michael@0: // signed. michael@0: // michael@0: // XXX: As long as we don't unpack the JAR into the filesystem, the "/" michael@0: // entries are harmless. But, it is not clear what the security michael@0: // implications of directory entries are if/when we were to unpackage the michael@0: // JAR into the filesystem. michael@0: if (entryFilename[entryFilename.Length() - 1] == '/') { michael@0: continue; michael@0: } michael@0: michael@0: nsCStringHashKey * item = items.GetEntry(entryFilename); michael@0: if (!item) { michael@0: return NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY; michael@0: } michael@0: michael@0: // Remove the item so we can check for leftover items later michael@0: items.RemoveEntry(entryFilename); michael@0: } michael@0: michael@0: // We verified that every entry that we require to be signed is signed. But, michael@0: // were there any missing entries--that is, entries that are mentioned in the michael@0: // manifest but missing from the archive? michael@0: if (items.Count() != 0) { michael@0: return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; michael@0: } michael@0: michael@0: // Return the reader to the caller if they want it michael@0: if (aZipReader) { michael@0: zip.forget(aZipReader); michael@0: } michael@0: michael@0: // Return the signer's certificate to the reader if they want it. michael@0: // XXX: We should return an nsIX509CertList with the whole validated chain, michael@0: // but we can't do that until we switch to libpkix. michael@0: if (aSignerCert) { michael@0: MOZ_ASSERT(CERT_LIST_HEAD(builtChain)); michael@0: nsCOMPtr signerCert = michael@0: nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert); michael@0: NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY); michael@0: signerCert.forget(aSignerCert); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: class OpenSignedAppFileTask MOZ_FINAL : public CryptoTask michael@0: { michael@0: public: michael@0: OpenSignedAppFileTask(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, michael@0: nsIOpenSignedAppFileCallback* aCallback) michael@0: : mTrustedRoot(aTrustedRoot) michael@0: , mJarFile(aJarFile) michael@0: , mCallback(new nsMainThreadPtrHolder(aCallback)) michael@0: { michael@0: } michael@0: michael@0: private: michael@0: virtual nsresult CalculateResult() MOZ_OVERRIDE michael@0: { michael@0: return OpenSignedAppFile(mTrustedRoot, mJarFile, michael@0: getter_AddRefs(mZipReader), michael@0: getter_AddRefs(mSignerCert)); michael@0: } michael@0: michael@0: // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that michael@0: // needs to be released michael@0: virtual void ReleaseNSSResources() { } michael@0: michael@0: virtual void CallCallback(nsresult rv) michael@0: { michael@0: (void) mCallback->OpenSignedAppFileFinished(rv, mZipReader, mSignerCert); michael@0: } michael@0: michael@0: const AppTrustedRoot mTrustedRoot; michael@0: const nsCOMPtr mJarFile; michael@0: nsMainThreadPtrHandle mCallback; michael@0: nsCOMPtr mZipReader; // out michael@0: nsCOMPtr mSignerCert; // out michael@0: }; michael@0: michael@0: } // unnamed namespace michael@0: michael@0: NS_IMETHODIMP michael@0: nsNSSCertificateDB::OpenSignedAppFileAsync( michael@0: AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, michael@0: nsIOpenSignedAppFileCallback* aCallback) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aJarFile); michael@0: NS_ENSURE_ARG_POINTER(aCallback); michael@0: RefPtr task(new OpenSignedAppFileTask(aTrustedRoot, michael@0: aJarFile, michael@0: aCallback)); michael@0: return task->Dispatch("SignedJAR"); michael@0: }