From 37499aa394fc596ff6b71a29fc0ec31440c24852 Mon Sep 17 00:00:00 2001 From: roytam1 Date: Sat, 30 Jul 2022 07:17:40 +0800 Subject: [PATCH] Revert "ported from UXP: Issue #1966 - Remove support for Firefox Marketplace "apps" (11680c89)" This reverts commit 111fb139c7c5c3b82d0046995841d4059d01ccdf. --- security/apps/AppSignatureVerification.cpp | 1561 ++++++++++++++++++ security/apps/AppTrustDomain.cpp | 382 +++++ security/apps/AppTrustDomain.h | 90 + security/apps/addons-public.crt | Bin 0 -> 1637 bytes security/apps/addons-stage.crt | Bin 0 -> 1895 bytes security/apps/gen_cert_header.py | 45 + security/apps/marketplace-dev-public.crt | Bin 0 -> 964 bytes security/apps/marketplace-dev-reviewers.crt | Bin 0 -> 1012 bytes security/apps/marketplace-prod-public.crt | Bin 0 -> 1177 bytes security/apps/marketplace-prod-reviewers.crt | Bin 0 -> 1171 bytes security/apps/marketplace-stage.crt | Bin 0 -> 1157 bytes security/apps/moz.build | 44 + security/apps/privileged-package-root.der | Bin 0 -> 930 bytes security/apps/trusted-app-public.der | 0 security/manager/ssl/nsIX509CertDB.idl | 73 +- toolkit/toolkit.mozbuild | 2 + 16 files changed, 2192 insertions(+), 5 deletions(-) create mode 100644 security/apps/AppSignatureVerification.cpp create mode 100644 security/apps/AppTrustDomain.cpp create mode 100644 security/apps/AppTrustDomain.h create mode 100644 security/apps/addons-public.crt create mode 100644 security/apps/addons-stage.crt create mode 100644 security/apps/gen_cert_header.py create mode 100644 security/apps/marketplace-dev-public.crt create mode 100644 security/apps/marketplace-dev-reviewers.crt create mode 100644 security/apps/marketplace-prod-public.crt create mode 100644 security/apps/marketplace-prod-reviewers.crt create mode 100644 security/apps/marketplace-stage.crt create mode 100644 security/apps/moz.build create mode 100644 security/apps/privileged-package-root.der create mode 100644 security/apps/trusted-app-public.der diff --git a/security/apps/AppSignatureVerification.cpp b/security/apps/AppSignatureVerification.cpp new file mode 100644 index 000000000..daef5a7ac --- /dev/null +++ b/security/apps/AppSignatureVerification.cpp @@ -0,0 +1,1561 @@ +/* -*- 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/. */ + +#include "nsNSSCertificateDB.h" + +#include "AppTrustDomain.h" +#include "CryptoTask.h" +#include "NSSCertDBTrustDomain.h" +#include "ScopedNSSTypes.h" +#include "base64.h" +#include "certdb.h" +#include "mozilla/Casting.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsDataSignatureVerifier.h" +#include "nsHashKeys.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIInputStream.h" +#include "nsIStringEnumerator.h" +#include "nsIZipReader.h" +#include "nsNSSCertificate.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsString.h" +#include "nsTHashtable.h" +#include "nssb64.h" +#include "pkix/pkix.h" +#include "pkix/pkixnss.h" +#include "plstr.h" +#include "secmime.h" + + +using namespace mozilla::pkix; +using namespace mozilla; +using namespace mozilla::psm; + +extern mozilla::LazyLogModule gPIPNSSLog; + +namespace { + +// Reads a maximum of 1MB from a stream into the supplied buffer. +// The reason for the 1MB limit is because this function is used to read +// signature-related files and we want to avoid OOM. The uncompressed length of +// an entry can be hundreds of times larger than the compressed version, +// especially if someone has specifically crafted the entry to cause OOM or to +// consume massive amounts of disk space. +// +// @param stream The input stream to read from. +// @param buf The buffer that we read the stream into, which must have +// already been allocated. +nsresult +ReadStream(const nsCOMPtr& stream, /*out*/ SECItem& buf) +{ + // 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 length; + nsresult rv = stream->Available(&length); + if (NS_WARN_IF(NS_FAILED(rv))) { + return 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. + static const uint32_t MAX_LENGTH = 1024 * 1024; + if (length > MAX_LENGTH) { + return NS_ERROR_FILE_TOO_BIG; + } + + // With bug 164695 in mind we +1 to leave room for null-terminating + // the buffer. + SECITEM_AllocItem(buf, static_cast(length + 1)); + + // buf.len == length + 1. We attempt to read length + 1 bytes + // instead of length, so that we can check whether the metadata for + // the entry is incorrect. + uint32_t bytesRead; + rv = stream->Read(BitwiseCast(buf.data), buf.len, + &bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (bytesRead != length) { + return NS_ERROR_FILE_CORRUPTED; + } + + buf.data[buf.len - 1] = 0; // null-terminate + + return NS_OK; +} + +// Finds exactly one (signature metadata) JAR 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); + + rv = ReadStream(stream, buf); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + + 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 stream an input stream from a JAR entry or file depending on whether +// it is from a signed archive or unpacked into a directory +// @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 +VerifyStreamContentDigest(nsIInputStream* stream, + const SECItem& digestFromManifest, SECItem& buf) +{ + MOZ_ASSERT(buf.len > 0); + if (digestFromManifest.len != SHA1_LENGTH) + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + + nsresult rv; + uint64_t len64; + rv = stream->Available(&len64); + NS_ENSURE_SUCCESS(rv, rv); + if (len64 > UINT32_MAX) { + return NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE; + } + + UniquePK11Context digestContext(PK11_CreateDigestContext(SEC_OID_SHA1)); + if (!digestContext) { + return mozilla::psm::GetXPCOMFromNSSError(PR_GetError()); + } + + rv = MapSECStatus(PK11_DigestBegin(digestContext.get())); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t totalBytesRead = 0; + for (;;) { + uint32_t bytesRead; + rv = stream->Read(BitwiseCast(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.get(), 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; +} + +nsresult +VerifyEntryContentDigest(nsIZipReader* zip, const nsACString& aFilename, + const SECItem& digestFromManifest, SECItem& buf) +{ + nsCOMPtr stream; + nsresult rv = zip->GetInputStream(aFilename, getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + return VerifyStreamContentDigest(stream, digestFromManifest, buf); +} + +// @oaram aDir directory containing the unpacked signed archive +// @param aFilename path of the target file relative to aDir +// @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 +nsresult +VerifyFileContentDigest(nsIFile* aDir, const nsAString& aFilename, + const SECItem& digestFromManifest, SECItem& buf) +{ + // Find the file corresponding to the manifest path + nsCOMPtr file; + nsresult rv = aDir->Clone(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + // We don't know how to handle JARs with signed directory entries. + // It's technically possible in the manifest but makes no sense on disk. + // Inside an archive we just ignore them, but here we have to treat it + // as an error because the signed bytes never got unpacked. + int32_t pos = 0; + int32_t slash; + int32_t namelen = aFilename.Length(); + if (namelen == 0 || aFilename[namelen - 1] == '/') { + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + + // Append path segments one by one + do { + slash = aFilename.FindChar('/', pos); + int32_t segend = (slash == kNotFound) ? namelen : slash; + rv = file->Append(Substring(aFilename, pos, (segend - pos))); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + pos = slash + 1; + } while (pos < namelen && slash != kNotFound); + + bool exists; + rv = file->Exists(&exists); + if (NS_FAILED(rv) || !exists) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + bool isDir; + rv = file->IsDirectory(&isDir); + if (NS_FAILED(rv) || isDir) { + // We only support signed files, not directory entries + return NS_ERROR_SIGNED_JAR_ENTRY_INVALID; + } + + // Open an input stream for that file and verify it. + nsCOMPtr stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file, -1, -1, + nsIFileInputStream::CLOSE_ON_EOF); + if (NS_FAILED(rv) || !stream) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + return VerifyStreamContentDigest(stream, digestFromManifest, buf); +} + +// 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(); + size_t previousLength = 0; + size_t currentLength = 0; + for (;;) { + const char* eol = PL_strpbrk(nextLineStart, "\r\n"); + + if (!eol) { // Reached end of file before newline + eol = nextLineStart + strlen(nextLineStart); + } + + previousLength = currentLength; + line.Append(nextLineStart, eol - nextLineStart); + currentLength = line.Length(); + + // The spec says "No line may be longer than 72 bytes (not characters)" + // in its UTF8-encoded form. + static const size_t lineLimit = 72; + if (currentLength - previousLength > lineLimit) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // The spec says: "Implementations should support 65535-byte + // (not character) header values..." + if (currentLength > 65535) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + 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_META_DIR "META-INF" +#define JAR_MF_HEADER "Manifest-Version: 1.0" +#define JAR_SF_HEADER "Signature-Version: 1.0" + +nsresult +ParseAttribute(const nsAutoCString & curLine, + /*out*/ nsAutoCString & attrName, + /*out*/ nsAutoCString & attrValue) +{ + // 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, NS_LITERAL_CSTRING(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, NS_LITERAL_CSTRING(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; +} + +struct VerifyCertificateContext { + AppTrustedRoot trustedRoot; + UniqueCERTCertList& builtChain; +}; + +nsresult +VerifyCertificate(CERTCertificate* signerCert, void* voidContext, void* pinArg) +{ + // TODO: null pinArg is tolerated. + if (NS_WARN_IF(!signerCert) || NS_WARN_IF(!voidContext)) { + return NS_ERROR_INVALID_ARG; + } + const VerifyCertificateContext& context = + *static_cast(voidContext); + + AppTrustDomain trustDomain(context.builtChain, pinArg); + nsresult rv = trustDomain.SetTrustedRoot(context.trustedRoot); + if (NS_FAILED(rv)) { + return rv; + } + Input certDER; + mozilla::pkix::Result result = certDER.Init(signerCert->derCert.data, + signerCert->derCert.len); + if (result != Success) { + return mozilla::psm::GetXPCOMFromNSSError(MapResultToPRErrorCode(result)); + } + + result = BuildCertChain(trustDomain, certDER, Now(), + EndEntityOrCA::MustBeEndEntity, + KeyUsage::digitalSignature, + KeyPurposeId::id_kp_codeSigning, + CertPolicyId::anyPolicy, + nullptr /*stapledOCSPResponse*/); + if (result == mozilla::pkix::Result::ERROR_EXPIRED_CERTIFICATE) { + // For code-signing you normally need trusted 3rd-party timestamps to + // handle expiration properly. The signer could always mess with their + // system clock so you can't trust the certificate was un-expired when + // the signing took place. The choice is either to ignore expiration + // or to enforce expiration at time of use. The latter leads to the + // user-hostile result that perfectly good code stops working. + // + // Our package format doesn't support timestamps (nor do we have a + // trusted 3rd party timestamper), but since we sign all of our apps and + // add-ons ourselves we can trust ourselves not to mess with the clock + // on the signing systems. We also have a revocation mechanism if we + // need it. It's OK to ignore cert expiration under these conditions. + // + // This is an invalid approach if + // * we issue certs to let others sign their own packages + // * mozilla::pkix returns "expired" when there are "worse" problems + // with the certificate or chain. + // (see bug 1267318) + result = Success; + } + if (result != Success) { + return mozilla::psm::GetXPCOMFromNSSError(MapResultToPRErrorCode(result)); + } + + return NS_OK; +} + +nsresult +VerifySignature(AppTrustedRoot trustedRoot, const SECItem& buffer, + const SECItem& detachedDigest, + /*out*/ UniqueCERTCertList& builtChain) +{ + // Currently, this function is only called within the CalculateResult() method + // of CryptoTasks. As such, NSS should not be shut down at this point and the + // CryptoTask implementation should already hold a nsNSSShutDownPreventionLock. + // We acquire a nsNSSShutDownPreventionLock here solely to prove we did to + // VerifyCMSDetachedSignatureIncludingCertificate(). + nsNSSShutDownPreventionLock locker; + VerifyCertificateContext context = { trustedRoot, builtChain }; + // XXX: missing pinArg + return VerifyCMSDetachedSignatureIncludingCertificate(buffer, detachedDigest, + VerifyCertificate, + &context, nullptr, + locker); +} + +NS_IMETHODIMP +OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, + /*out, optional */ nsIZipReader** aZipReader, + /*out, optional */ nsIX509Cert** 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; + } + + // 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; + } + + sigBuffer.type = siBuffer; + UniqueCERTCertList builtChain; + rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(), + builtChain); + if (NS_FAILED(rv)) { + return rv; + } + + ScopedAutoSECItem mfDigest; + rv = ParseSF(BitwiseCast(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; + + rv = ParseMF(BitwiseCast(manifestBuffer.data), zip, + items, buf); + if (NS_FAILED(rv)) { + return rv; + } + + // Verify every entry in the file. + nsCOMPtr entries; + rv = zip->FindEntries(EmptyCString(), 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); + + MOZ_LOG(gPIPNSSLog, LogLevel::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(item); + } + + // 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. + if (aSignerCert) { + CERTCertListNode* signerCertNode = CERT_LIST_HEAD(builtChain); + if (!signerCertNode || CERT_LIST_END(signerCertNode, builtChain) || + !signerCertNode->cert) { + return NS_ERROR_FAILURE; + } + nsCOMPtr signerCert = + nsNSSCertificate::Create(signerCertNode->cert); + NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY); + signerCert.forget(aSignerCert); + } + + return NS_OK; +} + +nsresult +VerifySignedManifest(AppTrustedRoot aTrustedRoot, + nsIInputStream* aManifestStream, + nsIInputStream* aSignatureStream, + /*out, optional */ nsIX509Cert** aSignerCert) +{ + NS_ENSURE_ARG(aManifestStream); + NS_ENSURE_ARG(aSignatureStream); + + if (aSignerCert) { + *aSignerCert = nullptr; + } + + // Load signature file in buffer + ScopedAutoSECItem signatureBuffer; + nsresult rv = ReadStream(aSignatureStream, signatureBuffer); + if (NS_FAILED(rv)) { + return rv; + } + signatureBuffer.type = siBuffer; + + // Load manifest file in buffer + ScopedAutoSECItem manifestBuffer; + rv = ReadStream(aManifestStream, manifestBuffer); + if (NS_FAILED(rv)) { + return rv; + } + + // Calculate SHA1 digest of the manifest buffer + Digest manifestCalculatedDigest; + rv = manifestCalculatedDigest.DigestBuf(SEC_OID_SHA1, + manifestBuffer.data, + manifestBuffer.len - 1); // buffer is null terminated + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Get base64 encoded string from manifest buffer digest + UniquePORTString + base64EncDigest(NSSBase64_EncodeItem(nullptr, nullptr, 0, + const_cast(&manifestCalculatedDigest.get()))); + if (NS_WARN_IF(!base64EncDigest)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Calculate SHA1 digest of the base64 encoded string + Digest doubleDigest; + rv = doubleDigest.DigestBuf(SEC_OID_SHA1, + BitwiseCast(base64EncDigest.get()), + strlen(base64EncDigest.get())); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Verify the manifest signature (signed digest of the base64 encoded string) + UniqueCERTCertList builtChain; + rv = VerifySignature(aTrustedRoot, signatureBuffer, + doubleDigest.get(), builtChain); + if (NS_FAILED(rv)) { + return rv; + } + + // Return the signer's certificate to the reader if they want it. + if (aSignerCert) { + CERTCertListNode* signerCertNode = CERT_LIST_HEAD(builtChain); + if (!signerCertNode || CERT_LIST_END(signerCertNode, builtChain) || + !signerCertNode->cert) { + return NS_ERROR_FAILURE; + } + nsCOMPtr signerCert = + nsNSSCertificate::Create(signerCertNode->cert); + if (NS_WARN_IF(!signerCert)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + signerCert.forget(aSignerCert); + } + + return NS_OK; +} + +class OpenSignedAppFileTask final : public CryptoTask +{ +public: + OpenSignedAppFileTask(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, + nsIOpenSignedAppFileCallback* aCallback) + : mTrustedRoot(aTrustedRoot) + , mJarFile(aJarFile) + , mCallback(new nsMainThreadPtrHolder(aCallback)) + { + } + +private: + virtual nsresult CalculateResult() override + { + return OpenSignedAppFile(mTrustedRoot, mJarFile, + getter_AddRefs(mZipReader), + getter_AddRefs(mSignerCert)); + } + + // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that + // needs to be released + virtual void ReleaseNSSResources() override { } + + virtual void CallCallback(nsresult rv) override + { + (void) mCallback->OpenSignedAppFileFinished(rv, mZipReader, mSignerCert); + } + + const AppTrustedRoot mTrustedRoot; + const nsCOMPtr mJarFile; + nsMainThreadPtrHandle mCallback; + nsCOMPtr mZipReader; // out + nsCOMPtr mSignerCert; // out +}; + +class VerifySignedmanifestTask final : public CryptoTask +{ +public: + VerifySignedmanifestTask(AppTrustedRoot aTrustedRoot, + nsIInputStream* aManifestStream, + nsIInputStream* aSignatureStream, + nsIVerifySignedManifestCallback* aCallback) + : mTrustedRoot(aTrustedRoot) + , mManifestStream(aManifestStream) + , mSignatureStream(aSignatureStream) + , mCallback( + new nsMainThreadPtrHolder(aCallback)) + { + } + +private: + virtual nsresult CalculateResult() override + { + return VerifySignedManifest(mTrustedRoot, mManifestStream, + mSignatureStream, getter_AddRefs(mSignerCert)); + } + + // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that + // needs to be released + virtual void ReleaseNSSResources() override { } + + virtual void CallCallback(nsresult rv) override + { + (void) mCallback->VerifySignedManifestFinished(rv, mSignerCert); + } + + const AppTrustedRoot mTrustedRoot; + const nsCOMPtr mManifestStream; + const nsCOMPtr mSignatureStream; + nsMainThreadPtrHandle mCallback; + nsCOMPtr mSignerCert; // out +}; + +} // unnamed namespace + +NS_IMETHODIMP +nsNSSCertificateDB::OpenSignedAppFileAsync( + AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, + nsIOpenSignedAppFileCallback* aCallback) +{ + NS_ENSURE_ARG_POINTER(aJarFile); + NS_ENSURE_ARG_POINTER(aCallback); + RefPtr task(new OpenSignedAppFileTask(aTrustedRoot, + aJarFile, + aCallback)); + return task->Dispatch("SignedJAR"); +} + +NS_IMETHODIMP +nsNSSCertificateDB::VerifySignedManifestAsync( + AppTrustedRoot aTrustedRoot, nsIInputStream* aManifestStream, + nsIInputStream* aSignatureStream, nsIVerifySignedManifestCallback* aCallback) +{ + NS_ENSURE_ARG_POINTER(aManifestStream); + NS_ENSURE_ARG_POINTER(aSignatureStream); + NS_ENSURE_ARG_POINTER(aCallback); + + RefPtr task( + new VerifySignedmanifestTask(aTrustedRoot, aManifestStream, + aSignatureStream, aCallback)); + return task->Dispatch("SignedManifest"); +} + + +// +// Signature verification for archives unpacked into a file structure +// + +// Finds the "*.rsa" signature file in the META-INF directory and returns +// the name. It is an error if there are none or more than one .rsa file +nsresult +FindSignatureFilename(nsIFile* aMetaDir, + /*out*/ nsAString& aFilename) +{ + nsCOMPtr entries; + nsresult rv = aMetaDir->GetDirectoryEntries(getter_AddRefs(entries)); + nsCOMPtr files = do_QueryInterface(entries); + if (NS_FAILED(rv) || !files) { + return NS_ERROR_SIGNED_JAR_NOT_SIGNED; + } + + bool found = false; + nsCOMPtr file; + rv = files->GetNextFile(getter_AddRefs(file)); + + while (NS_SUCCEEDED(rv) && file) { + nsAutoString leafname; + rv = file->GetLeafName(leafname); + if (NS_SUCCEEDED(rv)) { + if (StringEndsWith(leafname, NS_LITERAL_STRING(".rsa"))) { + if (!found) { + found = true; + aFilename = leafname; + } else { + // second signature file is an error + rv = NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + break; + } + } + rv = files->GetNextFile(getter_AddRefs(file)); + } + } + + if (!found) { + rv = NS_ERROR_SIGNED_JAR_NOT_SIGNED; + } + + files->Close(); + return rv; +} + +// Loads the signature metadata file that matches the given filename in +// the passed-in Meta-inf directory. If bufDigest is not null then on +// success bufDigest will contain the SHA-1 digest of the entry. +nsresult +LoadOneMetafile(nsIFile* aMetaDir, + const nsAString& aFilename, + /*out*/ SECItem& aBuf, + /*optional, out*/ Digest* aBufDigest) +{ + nsCOMPtr metafile; + nsresult rv = aMetaDir->Clone(getter_AddRefs(metafile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = metafile->Append(aFilename); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = metafile->Exists(&exists); + if (NS_FAILED(rv) || !exists) { + // we can call a missing .rsa file "unsigned" but FindSignatureFilename() + // already found one: missing other metadata files means a broken signature. + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + nsCOMPtr stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), metafile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ReadStream(stream, aBuf); + stream->Close(); + NS_ENSURE_SUCCESS(rv, rv); + + if (aBufDigest) { + rv = aBufDigest->DigestBuf(SEC_OID_SHA1, aBuf.data, aBuf.len - 1); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// Parses MANIFEST.MF and verifies the contents of the unpacked files +// listed in the manifest. +// The filenames of all entries will be returned in aMfItems. aBuf must +// be a pre-allocated scratch buffer that is used for doing I/O. +nsresult +ParseMFUnpacked(const char* aFilebuf, nsIFile* aDir, + /*out*/ nsTHashtable& aMfItems, + ScopedAutoSECItem& aBuf) +{ + nsresult rv; + + const char* nextLineStart = aFilebuf; + + rv = CheckManifestVersion(nextLineStart, NS_LITERAL_CSTRING(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; + } + } + + nsAutoString curItemName; + ScopedAutoSECItem digest; + + for (;;) { + nsAutoCString curLine; + rv = ReadLine(nextLineStart, curLine); + if (NS_FAILED(rv)) { + return 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 (aMfItems.Contains(curItemName)) { + // Duplicate entry + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Verify that the file's content digest matches the digest from this + // MF section. + rv = VerifyFileContentDigest(aDir, curItemName, digest, aBuf); + if (NS_FAILED(rv)) { + return rv; + } + + aMfItems.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 = NS_ConvertUTF8toUTF16(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; +} + +// recursively check a directory tree for files not in the list of +// verified files we found in the manifest. For each file we find +// Check it against the files found in the manifest. If the file wasn't +// in the manifest then it's unsigned and we can stop looking. Otherwise +// remove it from the collection so we can check leftovers later. +// +// @param aDir Directory to check +// @param aPath Relative path to that directory (to check against aItems) +// @param aItems All the files found +// @param *Filename signature files that won't be in the manifest +nsresult +CheckDirForUnsignedFiles(nsIFile* aDir, + const nsString& aPath, + /* in/out */ nsTHashtable& aItems, + const nsAString& sigFilename, + const nsAString& sfFilename, + const nsAString& mfFilename) +{ + nsCOMPtr entries; + nsresult rv = aDir->GetDirectoryEntries(getter_AddRefs(entries)); + nsCOMPtr files = do_QueryInterface(entries); + if (NS_FAILED(rv) || !files) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + bool inMeta = StringBeginsWith(aPath, NS_LITERAL_STRING(JAR_META_DIR)); + + while (NS_SUCCEEDED(rv)) { + nsCOMPtr file; + rv = files->GetNextFile(getter_AddRefs(file)); + if (NS_FAILED(rv) || !file) { + break; + } + + nsAutoString leafname; + rv = file->GetLeafName(leafname); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString curName(aPath + leafname); + + bool isDir; + rv = file->IsDirectory(&isDir); + if (NS_FAILED(rv)) { + return rv; + } + + // if it's a directory we need to recurse + if (isDir) { + curName.Append(NS_LITERAL_STRING("/")); + rv = CheckDirForUnsignedFiles(file, curName, aItems, + sigFilename, sfFilename, mfFilename); + } else { + // 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 (inMeta && ( leafname == sigFilename || + leafname == sfFilename || + leafname == mfFilename )) { + continue; + } + + // make sure the current file was found in the manifest + nsStringHashKey* item = aItems.GetEntry(curName); + if (!item) { + return NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY; + } + + // Remove the item so we can check for leftover items later + aItems.RemoveEntry(item); + } + } + files->Close(); + return rv; +} + +/* + * Verify the signature of a directory structure as if it were a + * signed JAR file (used for unpacked JARs) + */ +nsresult +VerifySignedDirectory(AppTrustedRoot aTrustedRoot, + nsIFile* aDirectory, + /*out, optional */ nsIX509Cert** aSignerCert) +{ + NS_ENSURE_ARG_POINTER(aDirectory); + + if (aSignerCert) { + *aSignerCert = nullptr; + } + + // Make sure there's a META-INF directory + + nsCOMPtr metaDir; + nsresult rv = aDirectory->Clone(getter_AddRefs(metaDir)); + if (NS_FAILED(rv)) { + return rv; + } + rv = metaDir->Append(NS_LITERAL_STRING(JAR_META_DIR)); + if (NS_FAILED(rv)) { + return rv; + } + + bool exists; + rv = metaDir->Exists(&exists); + if (NS_FAILED(rv) || !exists) { + return NS_ERROR_SIGNED_JAR_NOT_SIGNED; + } + bool isDirectory; + rv = metaDir->IsDirectory(&isDirectory); + if (NS_FAILED(rv) || !isDirectory) { + return NS_ERROR_SIGNED_JAR_NOT_SIGNED; + } + + // Find and load the Signature (RSA) file + + nsAutoString sigFilename; + rv = FindSignatureFilename(metaDir, sigFilename); + if (NS_FAILED(rv)) { + return rv; + } + + ScopedAutoSECItem sigBuffer; + rv = LoadOneMetafile(metaDir, sigFilename, sigBuffer, nullptr); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_NOT_SIGNED; + } + + // Load the signature (SF) file and verify the signature. + // The .sf and .rsa files must have the same name apart from the extension. + + nsAutoString sfFilename(Substring(sigFilename, 0, sigFilename.Length() - 3) + + NS_LITERAL_STRING("sf")); + + ScopedAutoSECItem sfBuffer; + Digest sfCalculatedDigest; + rv = LoadOneMetafile(metaDir, sfFilename, sfBuffer, &sfCalculatedDigest); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + sigBuffer.type = siBuffer; + UniqueCERTCertList builtChain; + rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(), + builtChain); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Get the expected manifest hash from the signed .sf file + + ScopedAutoSECItem mfDigest; + rv = ParseSF(BitwiseCast(sfBuffer.data), mfDigest); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Load manifest (MF) file and verify signature + + nsAutoString mfFilename(NS_LITERAL_STRING("manifest.mf")); + ScopedAutoSECItem manifestBuffer; + Digest mfCalculatedDigest; + rv = LoadOneMetafile(metaDir, mfFilename, manifestBuffer, &mfCalculatedDigest); + if (NS_FAILED(rv)) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + if (SECITEM_CompareItem(&mfDigest, &mfCalculatedDigest.get()) != SECEqual) { + return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID; + } + + // Parse manifest and verify signed hash of all listed files + + // 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; + rv = ParseMFUnpacked(BitwiseCast(manifestBuffer.data), + aDirectory, items, buf); + if (NS_FAILED(rv)){ + return rv; + } + + // We've checked that everything listed in the manifest exists and is signed + // correctly. Now check on disk for extra (unsigned) files. + // Deletes found entries from items as it goes. + rv = CheckDirForUnsignedFiles(aDirectory, EmptyString(), items, + sigFilename, sfFilename, mfFilename); + if (NS_FAILED(rv)) { + return rv; + } + + // 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 directory tree? (There shouldn't be given + // ParseMFUnpacked() checking them all, but it's a cheap sanity check.) + if (items.Count() != 0) { + return NS_ERROR_SIGNED_JAR_ENTRY_MISSING; + } + + // Return the signer's certificate to the reader if they want it. + // XXX: We should return an nsIX509CertList with the whole validated chain. + if (aSignerCert) { + CERTCertListNode* signerCertNode = CERT_LIST_HEAD(builtChain); + if (!signerCertNode || CERT_LIST_END(signerCertNode, builtChain) || + !signerCertNode->cert) { + return NS_ERROR_FAILURE; + } + nsCOMPtr signerCert = + nsNSSCertificate::Create(signerCertNode->cert); + NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY); + signerCert.forget(aSignerCert); + } + + return NS_OK; +} + +class VerifySignedDirectoryTask final : public CryptoTask +{ +public: + VerifySignedDirectoryTask(AppTrustedRoot aTrustedRoot, nsIFile* aUnpackedJar, + nsIVerifySignedDirectoryCallback* aCallback) + : mTrustedRoot(aTrustedRoot) + , mDirectory(aUnpackedJar) + , mCallback(new nsMainThreadPtrHolder(aCallback)) + { + } + +private: + virtual nsresult CalculateResult() override + { + return VerifySignedDirectory(mTrustedRoot, + mDirectory, + getter_AddRefs(mSignerCert)); + } + + // This class doesn't directly hold NSS resources so there's nothing that + // needs to be released + virtual void ReleaseNSSResources() override { } + + virtual void CallCallback(nsresult rv) override + { + (void) mCallback->VerifySignedDirectoryFinished(rv, mSignerCert); + } + + const AppTrustedRoot mTrustedRoot; + const nsCOMPtr mDirectory; + nsMainThreadPtrHandle mCallback; + nsCOMPtr mSignerCert; // out +}; + +NS_IMETHODIMP +nsNSSCertificateDB::VerifySignedDirectoryAsync( + AppTrustedRoot aTrustedRoot, nsIFile* aUnpackedJar, + nsIVerifySignedDirectoryCallback* aCallback) +{ + NS_ENSURE_ARG_POINTER(aUnpackedJar); + NS_ENSURE_ARG_POINTER(aCallback); + RefPtr task(new VerifySignedDirectoryTask(aTrustedRoot, + aUnpackedJar, + aCallback)); + return task->Dispatch("UnpackedJar"); +} diff --git a/security/apps/AppTrustDomain.cpp b/security/apps/AppTrustDomain.cpp new file mode 100644 index 000000000..e20dda580 --- /dev/null +++ b/security/apps/AppTrustDomain.cpp @@ -0,0 +1,382 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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/. */ + +#include "AppTrustDomain.h" +#include "MainThreadUtils.h" +#include "certdb.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Casting.h" +#include "mozilla/Preferences.h" +#include "nsComponentManagerUtils.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIX509CertDB.h" +#include "nsNSSCertificate.h" +#include "nsNetUtil.h" +#include "pkix/pkixnss.h" +#include "prerror.h" + +// Generated in Makefile.in +#include "marketplace-prod-public.inc" +#include "marketplace-prod-reviewers.inc" +#include "marketplace-dev-public.inc" +#include "marketplace-dev-reviewers.inc" +#include "marketplace-stage.inc" +#include "xpcshell.inc" +// Trusted Hosted Apps Certificates +#include "manifest-signing-root.inc" +#include "manifest-signing-test-root.inc" +// Add-on signing Certificates +#include "addons-public.inc" +#include "addons-stage.inc" +// Privileged Package Certificates +#include "privileged-package-root.inc" + +using namespace mozilla::pkix; + +extern mozilla::LazyLogModule gPIPNSSLog; + +static const unsigned int DEFAULT_MIN_RSA_BITS = 2048; +static char kDevImportedDER[] = + "network.http.signed-packages.developer-root"; + +namespace mozilla { namespace psm { + +StaticMutex AppTrustDomain::sMutex; +UniquePtr AppTrustDomain::sDevImportedDERData; +unsigned int AppTrustDomain::sDevImportedDERLen = 0; + +AppTrustDomain::AppTrustDomain(UniqueCERTCertList& certChain, void* pinArg) + : mCertChain(certChain) + , mPinArg(pinArg) + , mMinRSABits(DEFAULT_MIN_RSA_BITS) +{ +} + +nsresult +AppTrustDomain::SetTrustedRoot(AppTrustedRoot trustedRoot) +{ + SECItem trustedDER; + + // Load the trusted certificate into the in-memory NSS database so that + // CERT_CreateSubjectCertList can find it. + + switch (trustedRoot) + { + case nsIX509CertDB::AppMarketplaceProdPublicRoot: + trustedDER.data = const_cast(marketplaceProdPublicRoot); + trustedDER.len = mozilla::ArrayLength(marketplaceProdPublicRoot); + break; + + case nsIX509CertDB::AppMarketplaceProdReviewersRoot: + trustedDER.data = const_cast(marketplaceProdReviewersRoot); + trustedDER.len = mozilla::ArrayLength(marketplaceProdReviewersRoot); + break; + + case nsIX509CertDB::AppMarketplaceDevPublicRoot: + trustedDER.data = const_cast(marketplaceDevPublicRoot); + trustedDER.len = mozilla::ArrayLength(marketplaceDevPublicRoot); + break; + + case nsIX509CertDB::AppMarketplaceDevReviewersRoot: + trustedDER.data = const_cast(marketplaceDevReviewersRoot); + trustedDER.len = mozilla::ArrayLength(marketplaceDevReviewersRoot); + break; + + case nsIX509CertDB::AppMarketplaceStageRoot: + trustedDER.data = const_cast(marketplaceStageRoot); + trustedDER.len = mozilla::ArrayLength(marketplaceStageRoot); + // The staging root was generated with a 1024-bit key. + mMinRSABits = 1024u; + break; + + case nsIX509CertDB::AppXPCShellRoot: + trustedDER.data = const_cast(xpcshellRoot); + trustedDER.len = mozilla::ArrayLength(xpcshellRoot); + break; + + case nsIX509CertDB::AddonsPublicRoot: + trustedDER.data = const_cast(addonsPublicRoot); + trustedDER.len = mozilla::ArrayLength(addonsPublicRoot); + break; + + case nsIX509CertDB::AddonsStageRoot: + trustedDER.data = const_cast(addonsStageRoot); + trustedDER.len = mozilla::ArrayLength(addonsStageRoot); + break; + + case nsIX509CertDB::PrivilegedPackageRoot: + trustedDER.data = const_cast(privilegedPackageRoot); + trustedDER.len = mozilla::ArrayLength(privilegedPackageRoot); + break; + + case nsIX509CertDB::DeveloperImportedRoot: { + StaticMutexAutoLock lock(sMutex); + if (!sDevImportedDERData) { + MOZ_ASSERT(!NS_IsMainThread()); + nsCOMPtr file(do_CreateInstance("@mozilla.org/file/local;1")); + if (!file) { + return NS_ERROR_FAILURE; + } + nsresult rv = file->InitWithNativePath( + Preferences::GetCString(kDevImportedDER)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), file, -1, + -1, nsIFileInputStream::CLOSE_ON_EOF); + if (NS_FAILED(rv)) { + return rv; + } + + uint64_t length; + rv = inputStream->Available(&length); + if (NS_FAILED(rv)) { + return rv; + } + + auto data = MakeUnique(length); + rv = inputStream->Read(data.get(), length, &sDevImportedDERLen); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(length == sDevImportedDERLen); + sDevImportedDERData.reset( + BitwiseCast(data.release())); + } + + trustedDER.data = sDevImportedDERData.get(); + trustedDER.len = sDevImportedDERLen; + break; + } + + default: + return NS_ERROR_INVALID_ARG; + } + + mTrustedRoot.reset(CERT_NewTempCertificate(CERT_GetDefaultCertDB(), + &trustedDER, nullptr, false, true)); + if (!mTrustedRoot) { + return mozilla::psm::GetXPCOMFromNSSError(PR_GetError()); + } + + return NS_OK; +} + +Result +AppTrustDomain::FindIssuer(Input encodedIssuerName, IssuerChecker& checker, + Time) + +{ + MOZ_ASSERT(mTrustedRoot); + if (!mTrustedRoot) { + return Result::FATAL_ERROR_INVALID_STATE; + } + + // TODO(bug 1035418): If/when mozilla::pkix relaxes the restriction that + // FindIssuer must only pass certificates with a matching subject name to + // checker.Check, we can stop using CERT_CreateSubjectCertList and instead + // use logic like this: + // + // 1. First, try the trusted trust anchor. + // 2. Secondly, iterate through the certificates that were stored in the CMS + // message, passing each one to checker.Check. + SECItem encodedIssuerNameSECItem = + UnsafeMapInputToSECItem(encodedIssuerName); + UniqueCERTCertList + candidates(CERT_CreateSubjectCertList(nullptr, CERT_GetDefaultCertDB(), + &encodedIssuerNameSECItem, 0, + false)); + if (candidates) { + for (CERTCertListNode* n = CERT_LIST_HEAD(candidates); + !CERT_LIST_END(n, candidates); n = CERT_LIST_NEXT(n)) { + Input certDER; + Result rv = certDER.Init(n->cert->derCert.data, n->cert->derCert.len); + if (rv != Success) { + continue; // probably too big + } + + bool keepGoing; + rv = checker.Check(certDER, nullptr/*additionalNameConstraints*/, + keepGoing); + if (rv != Success) { + return rv; + } + if (!keepGoing) { + break; + } + } + } + + return Success; +} + +Result +AppTrustDomain::GetCertTrust(EndEntityOrCA endEntityOrCA, + const CertPolicyId& policy, + Input candidateCertDER, + /*out*/ TrustLevel& trustLevel) +{ + MOZ_ASSERT(policy.IsAnyPolicy()); + MOZ_ASSERT(mTrustedRoot); + if (!policy.IsAnyPolicy()) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + if (!mTrustedRoot) { + return Result::FATAL_ERROR_INVALID_STATE; + } + + // Handle active distrust of the certificate. + + // XXX: This would be cleaner and more efficient if we could get the trust + // information without constructing a CERTCertificate here, but NSS doesn't + // expose it in any other easy-to-use fashion. + SECItem candidateCertDERSECItem = + UnsafeMapInputToSECItem(candidateCertDER); + UniqueCERTCertificate candidateCert( + CERT_NewTempCertificate(CERT_GetDefaultCertDB(), &candidateCertDERSECItem, + nullptr, false, true)); + if (!candidateCert) { + return MapPRErrorCodeToResult(PR_GetError()); + } + + CERTCertTrust trust; + if (CERT_GetCertTrust(candidateCert.get(), &trust) == SECSuccess) { + uint32_t flags = SEC_GET_TRUST_FLAGS(&trust, trustObjectSigning); + + // For DISTRUST, we use the CERTDB_TRUSTED or CERTDB_TRUSTED_CA bit, + // because we can have active distrust for either type of cert. Note that + // CERTDB_TERMINAL_RECORD means "stop trying to inherit trust" so if the + // relevant trust bit isn't set then that means the cert must be considered + // distrusted. + uint32_t relevantTrustBit = endEntityOrCA == EndEntityOrCA::MustBeCA + ? CERTDB_TRUSTED_CA + : CERTDB_TRUSTED; + if (((flags & (relevantTrustBit | CERTDB_TERMINAL_RECORD))) + == CERTDB_TERMINAL_RECORD) { + trustLevel = TrustLevel::ActivelyDistrusted; + return Success; + } + } + + // mTrustedRoot is the only trust anchor for this validation. + if (CERT_CompareCerts(mTrustedRoot.get(), candidateCert.get())) { + trustLevel = TrustLevel::TrustAnchor; + return Success; + } + + trustLevel = TrustLevel::InheritsTrust; + return Success; +} + +Result +AppTrustDomain::DigestBuf(Input item, + DigestAlgorithm digestAlg, + /*out*/ uint8_t* digestBuf, + size_t digestBufLen) +{ + return DigestBufNSS(item, digestAlg, digestBuf, digestBufLen); +} + +Result +AppTrustDomain::CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration, + /*optional*/ const Input*, + /*optional*/ const Input*, + /*optional*/ const Input*) +{ + // We don't currently do revocation checking. If we need to distrust an Apps + // certificate, we will use the active distrust mechanism. + return Success; +} + +Result +AppTrustDomain::IsChainValid(const DERArray& certChain, Time time, + const CertPolicyId& requiredPolicy) +{ + SECStatus srv = ConstructCERTCertListFromReversedDERArray(certChain, + mCertChain); + if (srv != SECSuccess) { + return MapPRErrorCodeToResult(PR_GetError()); + } + return Success; +} + +Result +AppTrustDomain::CheckSignatureDigestAlgorithm(DigestAlgorithm, + EndEntityOrCA, + Time) +{ + // TODO: We should restrict signatures to SHA-256 or better. + return Success; +} + +Result +AppTrustDomain::CheckRSAPublicKeyModulusSizeInBits( + EndEntityOrCA /*endEntityOrCA*/, unsigned int modulusSizeInBits) +{ + if (modulusSizeInBits < mMinRSABits) { + return Result::ERROR_INADEQUATE_KEY_SIZE; + } + return Success; +} + +Result +AppTrustDomain::VerifyRSAPKCS1SignedDigest(const SignedDigest& signedDigest, + Input subjectPublicKeyInfo) +{ + // TODO: We should restrict signatures to SHA-256 or better. + return VerifyRSAPKCS1SignedDigestNSS(signedDigest, subjectPublicKeyInfo, + mPinArg); +} + +Result +AppTrustDomain::CheckECDSACurveIsAcceptable(EndEntityOrCA /*endEntityOrCA*/, + NamedCurve curve) +{ + switch (curve) { + case NamedCurve::secp256r1: // fall through + case NamedCurve::secp384r1: // fall through + case NamedCurve::secp521r1: + return Success; + } + + return Result::ERROR_UNSUPPORTED_ELLIPTIC_CURVE; +} + +Result +AppTrustDomain::VerifyECDSASignedDigest(const SignedDigest& signedDigest, + Input subjectPublicKeyInfo) +{ + return VerifyECDSASignedDigestNSS(signedDigest, subjectPublicKeyInfo, + mPinArg); +} + +Result +AppTrustDomain::CheckValidityIsAcceptable(Time /*notBefore*/, Time /*notAfter*/, + EndEntityOrCA /*endEntityOrCA*/, + KeyPurposeId /*keyPurpose*/) +{ + return Success; +} + +Result +AppTrustDomain::NetscapeStepUpMatchesServerAuth(Time /*notBefore*/, + /*out*/ bool& matches) +{ + matches = false; + return Success; +} + +void +AppTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/, + Input /*extensionData*/) +{ +} + +} } // namespace mozilla::psm diff --git a/security/apps/AppTrustDomain.h b/security/apps/AppTrustDomain.h new file mode 100644 index 000000000..942167e82 --- /dev/null +++ b/security/apps/AppTrustDomain.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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/. */ + +#ifndef AppTrustDomain_h +#define AppTrustDomain_h + +#include "pkix/pkixtypes.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/UniquePtr.h" +#include "nsDebug.h" +#include "nsIX509CertDB.h" +#include "ScopedNSSTypes.h" + +namespace mozilla { namespace psm { + +class AppTrustDomain final : public mozilla::pkix::TrustDomain +{ +public: + typedef mozilla::pkix::Result Result; + + AppTrustDomain(UniqueCERTCertList& certChain, void* pinArg); + + nsresult SetTrustedRoot(AppTrustedRoot trustedRoot); + + virtual Result GetCertTrust(mozilla::pkix::EndEntityOrCA endEntityOrCA, + const mozilla::pkix::CertPolicyId& policy, + mozilla::pkix::Input candidateCertDER, + /*out*/ mozilla::pkix::TrustLevel& trustLevel) + override; + virtual Result FindIssuer(mozilla::pkix::Input encodedIssuerName, + IssuerChecker& checker, + mozilla::pkix::Time time) override; + virtual Result CheckRevocation(mozilla::pkix::EndEntityOrCA endEntityOrCA, + const mozilla::pkix::CertID& certID, + mozilla::pkix::Time time, + mozilla::pkix::Duration validityDuration, + /*optional*/ const mozilla::pkix::Input* stapledOCSPresponse, + /*optional*/ const mozilla::pkix::Input* aiaExtension, + /*optional*/ const mozilla::pkix::Input* sctExtension) override; + virtual Result IsChainValid(const mozilla::pkix::DERArray& certChain, + mozilla::pkix::Time time, + const mozilla::pkix::CertPolicyId& requiredPolicy) override; + virtual Result CheckSignatureDigestAlgorithm( + mozilla::pkix::DigestAlgorithm digestAlg, + mozilla::pkix::EndEntityOrCA endEntityOrCA, + mozilla::pkix::Time notBefore) override; + virtual Result CheckRSAPublicKeyModulusSizeInBits( + mozilla::pkix::EndEntityOrCA endEntityOrCA, + unsigned int modulusSizeInBits) override; + virtual Result VerifyRSAPKCS1SignedDigest( + const mozilla::pkix::SignedDigest& signedDigest, + mozilla::pkix::Input subjectPublicKeyInfo) override; + virtual Result CheckECDSACurveIsAcceptable( + mozilla::pkix::EndEntityOrCA endEntityOrCA, + mozilla::pkix::NamedCurve curve) override; + virtual Result VerifyECDSASignedDigest( + const mozilla::pkix::SignedDigest& signedDigest, + mozilla::pkix::Input subjectPublicKeyInfo) override; + virtual Result CheckValidityIsAcceptable( + mozilla::pkix::Time notBefore, mozilla::pkix::Time notAfter, + mozilla::pkix::EndEntityOrCA endEntityOrCA, + mozilla::pkix::KeyPurposeId keyPurpose) override; + virtual Result NetscapeStepUpMatchesServerAuth( + mozilla::pkix::Time notBefore, + /*out*/ bool& matches) override; + virtual void NoteAuxiliaryExtension( + mozilla::pkix::AuxiliaryExtension extension, + mozilla::pkix::Input extensionData) override; + virtual Result DigestBuf(mozilla::pkix::Input item, + mozilla::pkix::DigestAlgorithm digestAlg, + /*out*/ uint8_t* digestBuf, + size_t digestBufLen) override; + +private: + /*out*/ UniqueCERTCertList& mCertChain; + void* mPinArg; // non-owning! + UniqueCERTCertificate mTrustedRoot; + unsigned int mMinRSABits; + + static StaticMutex sMutex; + static UniquePtr sDevImportedDERData; + static unsigned int sDevImportedDERLen; +}; + +} } // namespace mozilla::psm + +#endif // AppTrustDomain_h diff --git a/security/apps/addons-public.crt b/security/apps/addons-public.crt new file mode 100644 index 0000000000000000000000000000000000000000..6ab711b996fc25b144995af21726b08bc399996d GIT binary patch literal 1637 zcmXqLVoNk=V)0zS%*4pV#K>sC%f_kI=F#?@mywZ&mBFCaklTQhjX9KsO_(V(*ignm z3dG?O7WU1r%FM}0RB+BOD#$NNEXmBzGt@WG1<7&?tHER)ef<>zitVpiGLusc8Uwf{4Obs^*LwL&*1tFchyaw4SsV3uHLMCv1Q5jPxr9i!=Y-=C<5fb^d{WAdi}g7E;&jdBrpuxnXZ2fd zR-HY4|4Fg$qFF3ud__B}%!oPYiz%P=!R-rrlQUIn_O7wGKR z&BVyOxbdw)<0}ImU^-16eFAzcd| z?99vtjgvqU$}Ejt2950t8ylC@4{(Za0+tYb>4$TG98b(*pKH->FRLWU!qt$WmCkfkJhm#|F^Yz&N{c~ z?E+agtA|2DOMdJs{T09alGNJ_-PkSLcUM+uo-u6r^>#w~#U_!zC-?uFw*2)9_jP9b zOdlK*KO>bhCw9dmT~6JXcalrbI<8Y$b^4S~R?e&X+hLy=vJ}$8mbKgU%DT%7uXxhE zo9E5O$9CPi!PCMv7ayq&O=w*opX%Yars2)WLQT%psQdkMJ$?UP=ajZ@Gx3Wf-BvGXW zxf-jd`i1SSs(etkZtrc;|6I(w9HwR;nl)F-e0KljKTkhL+AtLRzBzW+Gv{ek-6JKj z5Ju(+YquAAd@ug^Q26nRcRulfuXiSgh2C5B>-xL3DJ)*SySMs16OjEYzxm+?1$*O) z?Pg~t{SG!~7v~XpaXsqGBi~1l5A4uCsJTe+lOo$AbcVdu(QYeZwTkY=i?EVA&c0T9(Jr^HHtihLV~VvBo5~4@(JW{FdJ(M9#WDfvN-Gk_Bj@p z?#m*&`7(nk1V~jnkH+Dwy}a10K;jW*AeZRk>%;P8`B1bWO=$|^Hgnge4e5VX3n=ze!qClR+mf)bp^xl~h;eDU!^Rxg)j3RglRngJAXx!3h= z8?Wwe8mC`rMSd*qNxaS8>D!d~Sy*#){%s5o6eWpP6(WmGLq>w4zOOEQjbGlBa=%9S zbV@%wyg4)}WN3p$Yv|}`v2p4VZ9DgeE|)9S$~wMr2YBGnF9(863Bt?TFMeyEu)CUb z-iqhV=d`7%hZ7dl`)#x4G0K~AGP3Fu>g4UgDKNTIv(ej)gUY+Y0+VfR{LwV>^y;Xc z!`t|xuAqD`^s{;=%6f^UacbyEkha{5&L)%~r7RJTc>tiG3LCc*c)sZGEP0aL*c%bUPx5N!^`k217Gd>Aji{8V-(tKmb^U z)=5cWZ3Ks}IgNzyQr;?M41`9?=_3IILKXxe10?EXv^^00W;cL^Yv9$K1DGrqm;J=O zOcs+v=Q6#BHcSrJ*V~t|29VJ|Z8%&9uYHQk4KOE@Kg#<_LpH~U%%GDQ96!SVraz=6 z-Fh7y4_Atu^z2~oPbN=xzn=d&_f`|8Hh%%)_eyguIKKnpw_f<)PByeW(`7)vz$X zjLzjlZ*uk9s#c_P50p_Pi->;BgQ~hkWtGvjmGlRNgyjAW1r6kv$0*xf!j9?+FOfZ~8jM`d z6`}-tZ0x!^BJ6^xiAHC{0pt#43*E`ULrZ%bFZKPtl59LK=eTYE+|cowoz)W#1Q)HY zAV*U@F~-t!-obu)+jla2Zf8`yOxx-=CvIkdGihTl&Gi(G|9G7tNwAG~pj~MSu1bBn zX#H!-;avldCp>G2OGA0Ej@=t#3(RG~c;uTJS9e3j!8QesrFsY4LNM4N&k(A_vU$O+ z%&r3Vh_x`Xt{2>Rl9&?q1VlF>>$dhO%vD9rHP0ILRpI)_8Geo}GnwCJa0(wEsW7w1 za3X4$Vh-A+$@kpec!7Spemeen2z65P>>Q7D_mwAe;lV;44ROlnrpRBZylf2 EpAWh8LI3~& literal 0 HcmV?d00001 diff --git a/security/apps/gen_cert_header.py b/security/apps/gen_cert_header.py new file mode 100644 index 000000000..0ffe25cf4 --- /dev/null +++ b/security/apps/gen_cert_header.py @@ -0,0 +1,45 @@ +# 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/. + +import binascii + +def _file_byte_generator(filename): + with open(filename, "rb") as f: + contents = f.read() + + # Treat empty files the same as a file containing a lone 0; + # a single-element array will fail cert verifcation just as an + # empty array would. + if not contents: + return ['\0'] + + return contents + +def _create_header(array_name, cert_bytes): + hexified = ["0x" + binascii.hexlify(byte) for byte in cert_bytes] + substs = { 'array_name': array_name, 'bytes': ', '.join(hexified) } + return "const uint8_t %(array_name)s[] = {\n%(bytes)s\n};\n" % substs + +# Create functions named the same as the data arrays that we're going to +# write to the headers, so we don't have to duplicate the names like so: +# +# def arrayName(header, cert_filename): +# header.write(_create_header("arrayName", cert_filename)) +array_names = [ + 'marketplaceProdPublicRoot', + 'marketplaceProdReviewersRoot', + 'marketplaceDevPublicRoot', + 'marketplaceDevReviewersRoot', + 'marketplaceStageRoot', + 'trustedAppPublicRoot', + 'trustedAppTestRoot', + 'xpcshellRoot', + 'addonsPublicRoot', + 'addonsStageRoot', + 'privilegedPackageRoot', +] + +for n in array_names: + # Make sure the lambda captures the right string. + globals()[n] = lambda header, cert_filename, name=n: header.write(_create_header(name, _file_byte_generator(cert_filename))) diff --git a/security/apps/marketplace-dev-public.crt b/security/apps/marketplace-dev-public.crt new file mode 100644 index 0000000000000000000000000000000000000000..490b8682b75cb9b6cee68708dd25a331a055d16d GIT binary patch literal 964 zcmXqLVm@Hd#I#}oGZP~d6CqgR zYXsyPT0*&l#hFcvO2`4o$jZRn#K_NJ(8S2a)WpchaBFSD&e!`N_X>z_)P8Sh`?uuh z6uIgAtAEcnYZ8x!dc9};rQnA_lBSDlEtG<-TVLwmI9kJE zeI+|saIGho&xt?p|1z@6{F&?NRVa6P@{~>Y9?X~ZY&ssaR(rjv{ExXj*6rs#{-J*Y7pM_sLbLXzxJ)a!24_2p^GSB~iD0}Dq z^8NbGsaIR){^1MQUn%+flQUzjZzBJRpvU2nvzFNFZGNe;+ty{;l;sz59s@2m4sAAIa%5*_Mh;$J z`UD0qBg6Cn?-PHRn2O77ie`)C_ikjgZ7kLEcD_C-$u4!0;&e0N+RG_?zxEe@a;jU$ z>~4Ew?Xe@1x9v>JO5iWklM&R4=J5BA?1}j*m3!AFK#u97z}#=msheJJ-*5cCP z`w6R<|5j~qJ$YXE{!H!bU0T))f2R7Z)jA}Z!C11PI;3sR+o^g}@*2z-lCOF`%&)z2 zG|W=_YVj}qyX^^z$0RgW%(glL=ij)1U(O)qXgf3*7QjeP|+X4^Ij>X~Hycs1>$!@t8*cc&fSYj&b@+LN>} U#rg8xj90z9byj#N>B~+40Kg|@)&Kwi literal 0 HcmV?d00001 diff --git a/security/apps/marketplace-dev-reviewers.crt b/security/apps/marketplace-dev-reviewers.crt new file mode 100644 index 0000000000000000000000000000000000000000..5b8bde93373b68ef86a6db12d6682507436f7ff4 GIT binary patch literal 1012 zcmXqLV*X&z#B^f;GZP~d6CkZ!c0K3pd2<~rqEymIdNV?BOo*} zG&eCbvWybvH3D&spxnXk{3b>v<~xCWD%K% zW%vHL?DLAu==kF*nKCs~-B2Q#+giQn?ZVqx)33~!rt(w*`ecN#~o9swEXMj3A!&`v%F8{ zHpFY(QSI)`RG&IQ>{h|-6xQz%T$xW>_my4A6_amGT(#a%P-*u5RcD>J79PmF!rsxL zC&=!@a$m1DX}XQ2b9>%*v1#wNX;z#5uGu{Mw##mJ>mLU$`b11k)&`aa{J;btE6m9FpM}+c8AuuMfdu$L0xZl-Oe_XsAigSy&tt&F#-YsyOup>Q z%*eqDOwYjJWn^HgtvVU`!OZQyR%CNZ)C>iq&kR1NwCi5&-g8K@XNFL9m*{o%T{dY; zIq!E>vbZgnt*5b0e89Wvh-&_nOqd^oo+${mK=UxnlWQXLDrFe7(D@yu4!H znow@O^OLKO8b|fLDO@$*SJ5m$_jal^uUyiCB?WhruFmaz!~5V_&9`jMPd9?Ox2th) z$nk9c+!Ohot?cvC1&%Ls0^gpUJ(rn}%`k%5#$ZKSX3vo&<$3d;yj^!A{P`}0RFj=u Y)0oq_*9B)xIVSISR(Z|=8K>>c0R8K2p#T5? literal 0 HcmV?d00001 diff --git a/security/apps/marketplace-prod-public.crt b/security/apps/marketplace-prod-public.crt new file mode 100644 index 0000000000000000000000000000000000000000..85c2fed92a963b2ff51ee92165d6ed59fa70aeff GIT binary patch literal 1177 zcmXqLVwq~t#9X_8nTe5!iILHOmyJ`a&7K4Ei>*gZNFpv}HH8eIb zGBP(XFf=hWixTHG0&z{D+(GBwCPoG1AZ27_U~XdMWiV)BllSAfuC1tT^{yQ}9YNR-anGUZ+RX0p}ztM|1DLEW6IwFNWg zCa|w!2w#4|vueMg!_~UAU%rKevf90ES;x6%!EXb<7k&;rviBXie}$nx&Ww-PYoWsQ#WXHV$nzMpjmK zW@dxNB_Ii9mc}^-jWZWEPF~VDVX&nEU^E-Zf?UbRBE}+;T>t68B_qWd|FCvzqwlVmoB`}I+qPu{+PJ?7^`^=CS5~PKF0O zndzVf5c_dI~(z~_3oGk?cDZ4)2ewZ1%)3Epo$G(TF zVug$T)vkR}`g(!!3iGI^ofY4oEL6#B{LWz&#$l)(y0ou!lluOvCP)70-rjTT%g2tO zZkNT6&OMP|v{;=dX7aappVdus7e=hIy3ldz%`w#tX(89!{Ywr+F5g-Dqi6NIdP~j2 dyW_t)oKBwC6Wwo?v0C11mg-`sge`VDw*irXxb*-4 literal 0 HcmV?d00001 diff --git a/security/apps/marketplace-prod-reviewers.crt b/security/apps/marketplace-prod-reviewers.crt new file mode 100644 index 0000000000000000000000000000000000000000..53be8c81ed601f2b054b55df1ab94eff1ee767ce GIT binary patch literal 1171 zcmXqLV(B+%VlH36%*4pV#K>sC%f_kI=F#?@mywZ&mBFB~+mPFUlZ`o)g-w_#G}uta zKnld+5*GH&ugc8HNmOvoFDl3{N-W9D&oeYLFagPO3+uvUeG`kaQ%eeR5|dLEf>O&e zQ_E9}iWP!0)AKU((iMVJi^?*SQw`M&R6zQeg=LHK^GkG-6LpJVYISoFCK$+x^BNi( z7#W!x7#JBCm`9288iBZ`Q0^deZ4;vca&R)TGB7tW@-i4SF>)|9F)}h-xLGG~!2P*Z zNA%<3eTzfi++<&LVpPEbMXHze6FAO%^5+8SfedZSj z2F)ie0W0QoNIaP~Cz5UEJFi7b9D>@KeQ)s4OL-SMc>Wnbqo|7cM_t-CY)#4k-1pSba?fu2g$EU_R^Bn5xBqX*Kc_I8g#C@Hr=))ov*P}H z%qBkX!(;8EpAIgq)z&q;{s{z`rAtuH4@q+N)7~e$Dkb`cZhX<5A0h#(JI<|qA7!8$`rxWw(Ahj~tEScez0a-X z(b*X>zw(f1)a%P!OHLf#HX+n&>)lAct%dFXw&*U9{;1H!;9u9dnl+>}ATuWXjLVGo z%QLp!()<s`IP@M8L(iWv_UJ+rWx huw9cOK7;%F&c^G8>PGEn4lB5D9+{bumVGZy6#xn8!_@!) literal 0 HcmV?d00001 diff --git a/security/apps/marketplace-stage.crt b/security/apps/marketplace-stage.crt new file mode 100644 index 0000000000000000000000000000000000000000..84504f3574afc7e2e2ebae5f6d7a73a88d2d50d6 GIT binary patch literal 1157 zcmXqLVrevJVt%!NnTe5!iILHOmyJ`a&7Q%!~^WfbS?6_+HYr|K1#CF|uvH0UMg=Ng(Dn8F>y$tb3gUr?Ny ztPNHR(wUr?i!jvzNuzel3$QgTpR<_%|`B;kR;~^dA;> zCoaENcGbft{Zg4tqiL2;Y}(TN7;%oYn>ScW=6*U27DkMKaj=30!(9U24Wz-Du~Zxz{SR)&Bn;e%FfJepu@(Q(B{F| z_QQ#hmqlJo*1sS%FF4po!96uEwJ5P9HATTWwWuUBEi)O&F|a_WW?|75)66I-DX`Ml zM@kX!q?BKjuAiK!pInrqSCE=(APcgek420{h!pPywz~qx=T#&}q^6~PdH}PKT>8B@2=nDVy zG~B|LYjGld?Jd{$5&P#v7iV{SU-@UYo~d2m|JMG~)e{*-%=j7X`CdPd4@fQ1%;E81 z-pFypd`Gj1XSlkEZ=%_@IUhT3Tu97&f9u|vyM~LWvo8MQ%KKws;FL)_)=U1$ykrqD O;oUaVC)JyNP6Pl@i+WW6 literal 0 HcmV?d00001 diff --git a/security/apps/moz.build b/security/apps/moz.build new file mode 100644 index 000000000..c26ce11ef --- /dev/null +++ b/security/apps/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + 'AppSignatureVerification.cpp', + 'AppTrustDomain.cpp', +] + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '/security/certverifier', + '/security/manager/ssl', + '/security/pkix/include', +] + +DEFINES['NSS_ENABLE_ECC'] = 'True' +for var in ('DLL_PREFIX', 'DLL_SUFFIX'): + DEFINES[var] = '"%s"' % CONFIG[var] + +test_ssl_path = '/security/manager/ssl/tests/unit' + +headers_arrays_certs = [ + ('marketplace-prod-public.inc', 'marketplaceProdPublicRoot', 'marketplace-prod-public.crt'), + ('marketplace-prod-reviewers.inc', 'marketplaceProdReviewersRoot', 'marketplace-prod-reviewers.crt'), + ('marketplace-dev-public.inc', 'marketplaceDevPublicRoot', 'marketplace-dev-public.crt'), + ('marketplace-dev-reviewers.inc', 'marketplaceDevReviewersRoot', 'marketplace-dev-reviewers.crt'), + ('marketplace-stage.inc', 'marketplaceStageRoot', 'marketplace-stage.crt'), + ('manifest-signing-root.inc', 'trustedAppPublicRoot', 'trusted-app-public.der'), + ('manifest-signing-test-root.inc', 'trustedAppTestRoot', test_ssl_path + '/test_signed_manifest/trusted_ca1.der'), + ('xpcshell.inc', 'xpcshellRoot', test_ssl_path + '/test_signed_apps/trusted_ca1.der'), + ('addons-public.inc', 'addonsPublicRoot', 'addons-public.crt'), + ('addons-stage.inc', 'addonsStageRoot', 'addons-stage.crt'), + ('privileged-package-root.inc', 'privilegedPackageRoot', 'privileged-package-root.der'), +] + +for header, array_name, cert in headers_arrays_certs: + GENERATED_FILES += [header] + h = GENERATED_FILES[header] + h.script = 'gen_cert_header.py:' + array_name + h.inputs = [cert] diff --git a/security/apps/privileged-package-root.der b/security/apps/privileged-package-root.der new file mode 100644 index 0000000000000000000000000000000000000000..9f77af5823fdbd297f0616cb45a6677a777c1328 GIT binary patch literal 930 zcmXqLVxDKv#MHKcnTe5!iILfWmyJ`a&7Gh=Wuy3k$$x!xD2cfodEL6vTNAO$`kVEe(tf3=K@9B>0UCOpyf! z##v2_O31;($jZRn#K_NJ(8S2a)Wpch@Q5?fp~ty*?x$IEJ9oRzzR!>{!)5*+Q?`d6 zdhYQanJAs)>804MQSNM&!Sl(J!?-WSfGJdaAAeBn3RN3X*^G?=t5O>q+xY%9YH`KZBg2xKc?pHm zY7!ntAI2{EdOfFS*^%i{~=E7-*NMalTN;l z(Ak&dRh+=~!COh^=cYG5x4-*;PP^^j&e?^}4!-|cs=7NNvf!DN%u4Irr7Xo=cR$pz z%y@e9_eaTZX2(w)5xiOWt7_WpoQ%ZqH?RLra$qf!5?W$4oohwGqtrFN=36#!9X0=b J^y|IqPykf>RwV!c literal 0 HcmV?d00001 diff --git a/security/apps/trusted-app-public.der b/security/apps/trusted-app-public.der new file mode 100644 index 000000000..e69de29bb diff --git a/security/manager/ssl/nsIX509CertDB.idl b/security/manager/ssl/nsIX509CertDB.idl index fdc020de0..1c030ab54 100644 --- a/security/manager/ssl/nsIX509CertDB.idl +++ b/security/manager/ssl/nsIX509CertDB.idl @@ -234,11 +234,74 @@ interface nsIX509CertDB : nsISupports { */ nsIX509Cert constructX509(in string certDER, in unsigned long length); - // Flags to indicate the type of cert root for signed extensions - // This can probably be removed eventually. - const AppTrustedRoot AddonsPublicRoot = 1; - const AppTrustedRoot AddonsStageRoot = 2; - const AppTrustedRoot PrivilegedPackageRoot = 3; + /** + * 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. + */ + const AppTrustedRoot AppMarketplaceProdPublicRoot = 1; + const AppTrustedRoot AppMarketplaceProdReviewersRoot = 2; + const AppTrustedRoot AppMarketplaceDevPublicRoot = 3; + const AppTrustedRoot AppMarketplaceDevReviewersRoot = 4; + const AppTrustedRoot AppMarketplaceStageRoot = 5; + const AppTrustedRoot AppXPCShellRoot = 6; + const AppTrustedRoot AddonsPublicRoot = 7; + const AppTrustedRoot AddonsStageRoot = 8; + const AppTrustedRoot PrivilegedPackageRoot = 9; + /* + * If DeveloperImportedRoot is set as trusted root, a CA from local file + * system will be imported. Only used when preference + * "network.http.packaged-apps-developer-mode" is set. + * The path of the CA is specified by preference + * "network.http.packaged-apps-developer-trusted-root". + */ + const AppTrustedRoot DeveloperImportedRoot = 10; + void openSignedAppFileAsync(in AppTrustedRoot trustedRoot, + in nsIFile aJarFile, + in nsIOpenSignedAppFileCallback callback); + + /** + * Verifies the signature on a directory representing an unpacked signed + * JAR file. To be considered valid, there must be exactly one signature + * on the directory structure 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 and the trusted certificate that signed the + * unpacked JAR are returned. + * + * On failure, an error code is returned. + */ + void verifySignedDirectoryAsync(in AppTrustedRoot trustedRoot, + in nsIFile aUnpackedDir, + in nsIVerifySignedDirectoryCallback callback); + + /** + * Given streams containing a signature and a manifest file, verifies + * that the signature is valid for the manifest. The signature must + * come from a certificate that is trusted for code signing and that + * was issued by the given trusted root. + * + * On success, NS_OK and the trusted certificate that signed the + * Manifest are returned. + * + * On failure, an error code is returned. + */ + void verifySignedManifestAsync(in AppTrustedRoot trustedRoot, + in nsIInputStream aManifestStream, + in nsIInputStream aSignatureStream, + in nsIVerifySignedManifestCallback callback); /* * Add a cert to a cert DB from a binary string. diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild index 5bb762bb6..bbd7ee2ef 100644 --- a/toolkit/toolkit.mozbuild +++ b/toolkit/toolkit.mozbuild @@ -11,6 +11,8 @@ DIRS += [ # Depends on NSS and NSPR, and must be built after sandbox or else B2G emulator # builds fail. '/security/certverifier', + # Depends on certverifier + '/security/apps', ] # the signing related bits of libmar depend on nss