Bug 772365, Part 1: Implement JARSignatureVerification, r=honzab, parts r=ehsan

* * *
Bug 772365, Part 2.1: Generate test cases for signed app signature verification
* * *
Bug 772365, Part 2.2: Test JAR signature verification

--HG--
extra : rebase_source : 198be789e8b1565dad418e15760fa6dc90da843f
This commit is contained in:
Brian Smith 2012-11-14 15:31:39 -08:00
parent 8b107fc7c0
commit 8b3350f6d2
21 changed files with 1462 additions and 3 deletions

View File

@ -188,3 +188,13 @@ XPC_MSG_DEF(NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED, "Clearing site data by tim
/* character converter related codes (from nsIUnicodeDecoder.h) */
XPC_MSG_DEF(NS_ERROR_ILLEGAL_INPUT , "The input characters have illegal sequences")
/* Codes related to signd jars */
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_NOT_SIGNED , "The JAR is not signed.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY , "An entry in the JAR has been modified after the JAR was signed.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY , "An entry in the JAR has not been signed.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_MISSING , "An entry is missing from the JAR file.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE , "The JAR's signature is wrong.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE , "An entry in the JAR is too large.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_INVALID , "An entry in the JAR is invalid.")
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID , "The JAR's manifest or signature file is invalid.")

View File

@ -8,18 +8,28 @@
interface nsIArray;
interface nsIX509Cert;
interface nsIX509Cert3;
interface nsIFile;
interface nsIInterfaceRequestor;
interface nsIZipReader;
%{C++
#define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1"
%}
[scriptable, function, uuid(48411e2d-85a9-4b16-bec8-e30cde801f9e)]
interface nsIOpenSignedJARFileCallback : nsISupports
{
void openSignedJARFileFinished(in nsresult rv,
in nsIZipReader aZipReader,
in nsIX509Cert3 aSignerCert);
};
/**
* This represents a service to access and manipulate
* X.509 certificates stored in a database.
*/
[scriptable, uuid(eb426311-69cd-4a74-a7db-a4a215854c78)]
[scriptable, uuid(735d0363-e219-4387-b5c6-72e800c3ea0b)]
interface nsIX509CertDB : nsISupports {
/**
@ -253,5 +263,33 @@ interface nsIX509CertDB : nsISupports {
* @return The new certificate object.
*/
nsIX509Cert constructX509FromBase64(in string base64);
};
/**
* Verifies the signature on the given JAR file to verify that it has a
* valid signature. To be considered valid, there must be exactly one
* signature on the JAR file and that signature must have signed every
* entry. Further, the signature must come from a certificate that
* is trusted for code signing.
*
* On success, NS_OK, a nsIZipReader, and the trusted certificate that
* signed the JAR are returned.
*
* On failure, an error code is returned.
*
* This method returns a nsIZipReader, instead of taking an nsIZipReader
* as input, to encourage users of the API to verify the signature as the
* first step in opening the JAR.
*/
void openSignedJARFileAsync(in nsIFile aJarFile,
in nsIOpenSignedJARFileCallback callback);
/*
* Add a cert to a cert DB from a binary string.
*
* @param certDER The raw DER encoding of a certificate.
* @param aTrust decoded by CERT_DecodeTrustString. 3 comma separated characters,
* indicating SSL, Email, and Obj signing trust
* @param aName name of the cert for display purposes.
*/
void addCert(in ACString certDER, in string aTrust, in string aName);
};

View File

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

View File

@ -21,6 +21,7 @@ LIBXUL_LIBRARY = 1
CPPSRCS = \
CryptoTask.cpp \
JARSignatureVerification.cpp \
nsCERTValInParamWrapper.cpp \
nsNSSCleaner.cpp \
nsCertOverrideService.cpp \

View File

@ -9,6 +9,7 @@
#include "nsNSSComponent.h"
#include "nsNSSCertificateDB.h"
#include "mozilla/Base64.h"
#include "nsCOMPtr.h"
#include "nsNSSCertificate.h"
#include "nsNSSHelper.h"
@ -1616,6 +1617,16 @@ NS_IMETHODIMP nsNSSCertificateDB::AddCertFromBase64(const char *aBase64, const c
return (srv == SECSuccess) ? NS_OK : NS_ERROR_FAILURE;
}
NS_IMETHODIMP
nsNSSCertificateDB::AddCert(const nsACString & aCertDER, const char *aTrust,
const char *aName)
{
nsCString base64;
nsresult rv = Base64Encode(aCertDER, base64);
NS_ENSURE_SUCCESS(rv, rv);
return AddCertFromBase64(base64.get(), aTrust, aName);
}
NS_IMETHODIMP
nsNSSCertificateDB::GetCerts(nsIX509CertList **_retval)
{

View File

@ -4,6 +4,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifdef MOZ_LOGGING
#define FORCE_PR_LOG 1
#endif
#include "nsNSSComponent.h"
#include "nsNSSCallbacks.h"
#include "nsNSSIOLayer.h"
@ -84,7 +88,7 @@
using namespace mozilla;
using namespace mozilla::psm;
#ifdef PR_LOGGING
#ifdef MOZ_LOGGING
PRLogModuleInfo* gPIPNSSLog = nullptr;
#endif

View File

@ -0,0 +1,254 @@
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
/* To regenerate the certificates and apps for this test:
cd security/manager/ssl/tests/unit/test_signed_apps
PATH=$NSS/bin:$NSS/lib:$PATH ./generate.sh
cd ../../../../../..
make -C $OBJDIR/security/manager/ssl/tests
$NSS is the path to NSS binaries and libraries built for the host platform.
If you get error messages about "CertUtil" on Windows, then it means that
the Windows CertUtil.exe is ahead of the NSS certutil.exe in $PATH.
Check in the generated files. These steps are not done as part of the build
because we do not want to add a build-time dependency on the OpenSSL or NSS
tools or libraries built for the host platform.
*/
// XXX from prio.h
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
let tempScope = {};
Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
let NetUtil = tempScope.NetUtil;
Cu.import("resource://gre/modules/FileUtils.jsm"); // XXX: tempScope?
Cu.import("resource://gre/modules/Services.jsm"); // XXX: tempScope?
do_get_profile(); // must be called before getting nsIX509CertDB
const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB);
// Creates a new app package based in the inFilePath package, with a set of
// modifications (including possibly deletions) applied to the existing entries,
// and/or a set of new entries to be included.
function tamper(inFilePath, outFilePath, modifications, newEntries) {
var writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
writer.open(outFilePath, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
try {
var reader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
reader.open(inFilePath);
try {
var entries = reader.findEntries("");
while (entries.hasMore()) {
var entryName = entries.getNext();
var inEntry = reader.getEntry(entryName);
var entryInput = reader.getInputStream(entryName);
try {
var f = modifications[entryName];
var outEntry, outEntryInput;
if (f) {
[outEntry, outEntryInput] = f(inEntry, entryInput);
delete modifications[entryName];
} else {
[outEntry, outEntryInput] = [inEntry, entryInput];
}
// if f does not want the input entry to be copied to the output entry
// at all (i.e. it wants it to be deleted), it will return null.
if (outEntryInput) {
try {
writer.addEntryStream(entryName,
outEntry.lastModifiedTime,
outEntry.compression,
outEntryInput,
false);
} finally {
if (entryInput != outEntryInput)
outEntryInput.close();
}
}
} finally {
entryInput.close();
}
}
} finally {
reader.close();
}
// Any leftover modification means that we were expecting to modify an entry
// in the input file that wasn't there.
for(var name in modifications) {
if (modifications.hasOwnProperty(name)) {
throw "input file was missing expected entries: " + name;
}
}
// Now, append any new entries to the end
newEntries.forEach(function(newEntry) {
var sis = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
try {
sis.setData(newEntry.content, newEntry.content.length);
writer.addEntryStream(newEntry.name,
new Date(),
Ci.nsIZipWriter.COMPRESSION_BEST,
sis,
false);
} finally {
sis.close();
}
});
} finally {
writer.close();
}
}
function removeEntry(entry, entryInput) { return [null, null]; }
function truncateEntry(entry, entryInput) {
if (entryInput.available() == 0)
throw "Truncating already-zero length entry will result in identical entry.";
var content = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
content.data = "";
return [entry, content]
}
function readFile(file) {
let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
fstream.init(file, -1, 0, 0);
let data = NetUtil.readInputStreamToString(fstream, fstream.available());
fstream.close();
return data;
}
function run_test() {
var root_cert_der =
do_get_file("test_signed_apps/trusted_ca1.der", false);
var der = readFile(root_cert_der);
certdb.addCert(der, ",,CTu", "test-root");
run_next_test();
}
function check_open_result(name, expectedRv) {
return function openSignedJARFileCallback(rv, aZipReader, aSignerCert) {
do_print("openSignedJARFileCallback called for " + name);
do_check_eq(rv, expectedRv);
do_check_eq(aZipReader != null, Components.isSuccessCode(expectedRv));
do_check_eq(aSignerCert != null, Components.isSuccessCode(expectedRv));
run_next_test();
};
}
function original_app_path(test_name) {
return do_get_file("test_signed_apps/" + test_name + ".zip", false);
}
function tampered_app_path(test_name) {
return FileUtils.getFile("TmpD", ["test_signed_app-" + test_name + ".zip"]);
}
add_test(function () {
certdb.openSignedJARFileAsync(original_app_path("valid"),
check_open_result("valid", Cr.NS_OK));
});
add_test(function () {
certdb.openSignedJARFileAsync(original_app_path("unsigned"),
check_open_result("unsigned", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED));
});
add_test(function () {
// XXX: NSS has many possible error codes for this, e.g.
// SEC_ERROR_UNTRUSTED_ISSUER and others are also reasonable. Future versions
// of NSS may return one of these alternate errors; in that case, we need to
// update this test.
//
// XXX (bug 812089): Cr.NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER is undefined.
//
// XXX: Cannot use operator| instead of operator+ to combine bits because
// bit 31 trigger's JavaScript's crazy interpretation of the numbers as
// two's complement negative integers.
const NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER = 0x80000000 /* unsigned (1 << 31) */
+ ( (0x45 + 21) << 16)
+ (-(-0x2000 + 13) );
certdb.openSignedJARFileAsync(original_app_path("unknown_issuer"),
check_open_result("unknown_issuer",
/*Cr.*/NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER));
});
// Sanity check to ensure a no-op tampering gives a valid result
add_test(function () {
var tampered = tampered_app_path("identity_tampering");
tamper(original_app_path("valid"), tampered, { }, []);
certdb.openSignedJARFileAsync(original_app_path("valid"),
check_open_result("identity_tampering", Cr.NS_OK));
});
add_test(function () {
var tampered = tampered_app_path("missing_rsa");
tamper(original_app_path("valid"), tampered, { "META-INF/A.RSA" : removeEntry }, []);
certdb.openSignedJARFileAsync(tampered,
check_open_result("missing_rsa", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED));
});
add_test(function () {
var tampered = tampered_app_path("missing_sf");
tamper(original_app_path("valid"), tampered, { "META-INF/A.SF" : removeEntry }, []);
certdb.openSignedJARFileAsync(tampered,
check_open_result("missing_sf", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
});
add_test(function () {
var tampered = tampered_app_path("missing_manifest_mf");
tamper(original_app_path("valid"), tampered, { "META-INF/MANIFEST.MF" : removeEntry }, []);
certdb.openSignedJARFileAsync(tampered,
check_open_result("missing_manifest_mf",
Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
});
add_test(function () {
var tampered = tampered_app_path("missing_entry");
tamper(original_app_path("valid"), tampered, { "manifest.webapp" : removeEntry }, []);
certdb.openSignedJARFileAsync(tampered,
check_open_result("missing_entry", Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING));
});
add_test(function () {
var tampered = tampered_app_path("truncated_entry");
tamper(original_app_path("valid"), tampered, { "manifest.webapp" : truncateEntry }, []);
certdb.openSignedJARFileAsync(tampered,
check_open_result("truncated_entry", Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY));
});
add_test(function () {
var tampered = tampered_app_path("unsigned_entry");
tamper(original_app_path("valid"), tampered, {},
[ { "name": "unsigned.txt", "content": "unsigned content!" } ]);
certdb.openSignedJARFileAsync(tampered,
check_open_result("unsigned_entry", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY));
});
add_test(function () {
var tampered = tampered_app_path("unsigned_metainf_entry");
tamper(original_app_path("valid"), tampered, {},
[ { name: "META-INF/unsigned.txt", content: "unsigned content!" } ]);
certdb.openSignedJARFileAsync(tampered,
check_open_result("unsigned_metainf_entry",
Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY));
});
// TODO: tampered MF, tampered SF
// TODO: too-large MF, too-large RSA, too-large SF
// TODO: MF and SF that end immediately after the last main header
// (no CR nor LF)
// TODO: broken headers to exercise the parser

View File

@ -0,0 +1,79 @@
#!/bin/bash
# Usage:
# export NSS_PREFIX=<path to NSS tools> \
# PATH=$NSS_PREFIX/bin:$NSS_PREFIX/lib:$PATH ./generate.sh
set -e
srcdir=$PWD
tmpdir=$TMP/test_signed_apps
noisefile=$tmpdir/noise
passwordfile=$tmpdir/passwordfile
ca_responses=$tmpdir/ca_responses
ee_responses=$tmpdir/ee_responses
replace_zip()
{
zip=$1 # must be an absolute path
new_contents_dir=$2
rm -f $zip
oldpwd=$PWD
cd $new_contents_dir && zip -9 -o -r $zip *
cd $oldpwd
}
sign_app_with_new_cert()
{
label=$1
unsigned_zip=$2
out_signed_zip=$3
# XXX: We cannot give the trusted and untrusted versions of the certs the same
# subject names because otherwise we'll run into
# SEC_ERROR_REUSED_ISSUER_AND_SERIAL.
org="O=Examplla Corporation,L=Mountain View,ST=CA,C=US"
ca_subj="CN=Examplla Root CA $label,OU=Examplla CA,$org"
ee_subj="CN=Examplla Marketplace App Signing $label,OU=Examplla Marketplace App Signing,$org"
db=$tmpdir/$label
mkdir -p $db
certutil -d $db -N -f $passwordfile
make_cert="certutil -d $db -f $passwordfile -S -v 3 -g 2048 -Z SHA256 \
-z $noisefile -y 3 -2 --extKeyUsage critical,codeSigning"
$make_cert -n ca1 -m 1 -s "$ca_subj" \
--keyUsage critical,certSigning -t ",,CTu" -x < $ca_responses
$make_cert -n ee1 -c ca1 -m 2 -s "$ee_subj" \
--keyUsage critical,digitalSignature -t ",,," < $ee_responses
# In case we want to inspect the generated certs
certutil -d $db -L -n ca1 -r -o $db/ca1.der
certutil -d $db -L -n ee1 -r -o $db/ee1.der
python sign_b2g_app.py -d $db -f $passwordfile -k ee1 -i $unsigned_zip -o $out_signed_zip
}
rm -Rf $tmpdir
mkdir $tmpdir
echo password1 > $passwordfile
head --bytes 32 /dev/urandom > $noisefile
# XXX: certutil cannot generate basic constraints without interactive prompts,
# so we need to build response files to answer its questions
# XXX: certutil cannot generate AKI/SKI without interactive prompts so we just
# skip them.
echo y > $ca_responses # Is this a CA?
echo >> $ca_responses # Accept default path length constraint (no constraint)
echo y >> $ca_responses # Is this a critical constraint?
echo n > $ee_responses # Is this a CA?
echo >> $ee_responses # Accept default path length constraint (no constraint)
echo y >> $ee_responses # Is this a critical constraint?
replace_zip $srcdir/unsigned.zip $srcdir/simple
sign_app_with_new_cert trusted $srcdir/unsigned.zip $srcdir/valid.zip
sign_app_with_new_cert untrusted $srcdir/unsigned.zip $srcdir/unknown_issuer.zip
certutil -d $tmpdir/trusted -f $passwordfile -L -n ca1 -r -o $srcdir/trusted_ca1.der
rm -Rf $tmpdir

View File

@ -0,0 +1,124 @@
from ctypes import *
import os
import sys
if sys.platform == 'darwin':
libprefix = "lib"
libsuffix = ".dylib"
elif os.name == 'posix':
libprefix = "lib"
libsuffix = ".so"
else: # assume windows
libprefix = ""
libsuffix = ".dll"
nspr = cdll.LoadLibrary(libprefix + "nspr4" + libsuffix)
nss = cdll.LoadLibrary(libprefix + "nss3" + libsuffix)
smime = cdll.LoadLibrary(libprefix + "smime3" + libsuffix)
nspr.PR_GetError.argtypes = []
nspr.PR_GetError.restype = c_int32
nspr.PR_ErrorToName.argtypes = [c_int32]
nspr.PR_ErrorToName.restype = c_char_p
def raise_if_not_SECSuccess(rv):
SECSuccess = 0
if (rv != SECSuccess):
raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError()))
def raise_if_NULL(p):
if not p:
raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError()))
return p
PRBool = c_int
SECStatus = c_int
# from secoidt.h
SEC_OID_SHA1 = 4
# from certt.h
certUsageObjectSigner = 6
class SECItem(Structure):
_fields_ = [("type", c_int),
("data", c_char_p),
("len", c_uint)]
nss.NSS_Init.argtypes = [c_char_p]
nss.NSS_Init.restype = SECStatus
def NSS_Init(db_dir):
nss.NSS_Init.argtypes = [c_char_p]
nss.NSS_Init.restype = SECStatus
raise_if_not_SECSuccess(nss.NSS_Init(db_dir))
nss.NSS_Shutdown.argtypes = []
nss.NSS_Shutdown.restype = SECStatus
def NSS_Shutdown():
raise_if_not_SECSuccess(nss.NSS_Shutdown())
PK11PasswordFunc = CFUNCTYPE(c_char_p, c_void_p, PRBool, c_char_p)
# pass the result of this as the wincx parameter when a wincx is required
nss.PK11_SetPasswordFunc.argtypes = [PK11PasswordFunc]
nss.PK11_SetPasswordFunc.restype = None
def SetPasswordContext(password):
def callback(slot, retry, arg):
return password
wincx = PK11PasswordFunc(callback)
nss.PK11_SetPasswordFunc(wincx)
return wincx
nss.CERT_GetDefaultCertDB.argtypes = []
nss.CERT_GetDefaultCertDB.restype = c_void_p
def CERT_GetDefaultCertDB():
return raise_if_NULL(nss.CERT_GetDefaultCertDB())
nss.PK11_FindCertFromNickname.argtypes = [c_char_p, c_void_p]
nss.PK11_FindCertFromNickname.restype = c_void_p
def PK11_FindCertFromNickname(nickname, wincx):
return raise_if_NULL(nss.PK11_FindCertFromNickname(nickname, wincx))
nss.CERT_DestroyCertificate.argtypes = [c_void_p]
nss.CERT_DestroyCertificate.restype = None
def CERT_DestroyCertificate(cert):
nss.CERT_DestroyCertificate(cert)
smime.SEC_PKCS7CreateSignedData.argtypes = [c_void_p, c_int, c_void_p,
c_int, c_void_p,
c_void_p, c_void_p]
smime.SEC_PKCS7CreateSignedData.restype = c_void_p
def SEC_PKCS7CreateSignedData(cert, certusage, certdb, digestalg, digest, wincx):
item = SECItem(0, c_char_p(digest), len(digest))
return raise_if_NULL(smime.SEC_PKCS7CreateSignedData(cert, certusage, certdb,
digestalg,
pointer(item),
None, wincx))
smime.SEC_PKCS7AddSigningTime.argtypes = [c_void_p]
smime.SEC_PKCS7AddSigningTime.restype = SECStatus
def SEC_PKCS7AddSigningTime(p7):
raise_if_not_SECSuccess(smime.SEC_PKCS7AddSigningTime(p7))
smime.SEC_PKCS7IncludeCertChain.argtypes = [c_void_p, c_void_p]
smime.SEC_PKCS7IncludeCertChain.restype = SECStatus
def SEC_PKCS7IncludeCertChain(p7, wincx):
raise_if_not_SECSuccess(smime.SEC_PKCS7IncludeCertChain(p7, wincx))
SEC_PKCS7EncoderOutputCallback = CFUNCTYPE(None, c_void_p, c_void_p, c_long)
smime.SEC_PKCS7Encode.argtypes = [c_void_p, SEC_PKCS7EncoderOutputCallback,
c_void_p, c_void_p, c_void_p, c_void_p]
smime.SEC_PKCS7Encode.restype = SECStatus
def SEC_PKCS7Encode(p7, bulkkey, wincx):
outputChunks = []
def callback(chunks, data, len):
outputChunks.append(string_at(data, len))
callbackWrapper = SEC_PKCS7EncoderOutputCallback(callback)
raise_if_not_SECSuccess(smime.SEC_PKCS7Encode(p7, callbackWrapper,
None, None, None, wincx))
return "".join(outputChunks)
smime.SEC_PKCS7DestroyContentInfo.argtypes = [c_void_p]
smime.SEC_PKCS7DestroyContentInfo.restype = None
def SEC_PKCS7DestroyContentInfo(p7):
smime.SEC_PKCS7DestroyContentInfo(p7)

View File

@ -0,0 +1,144 @@
import argparse
from base64 import b64encode
from hashlib import sha1
import sys
import zipfile
import ctypes
import nss_ctypes
def nss_load_cert(nss_db_dir, nss_password, cert_nickname):
nss_ctypes.NSS_Init(nss_db_dir)
try:
wincx = nss_ctypes.SetPasswordContext(nss_password)
cert = nss_ctypes.PK11_FindCertFromNickname(cert_nickname, wincx)
return (wincx, cert)
except:
nss_ctypes.NSS_Shutdown()
raise
def nss_create_detached_signature(cert, dataToSign, wincx):
certdb = nss_ctypes.CERT_GetDefaultCertDB()
p7 = nss_ctypes.SEC_PKCS7CreateSignedData(cert,
nss_ctypes.certUsageObjectSigner,
certdb,
nss_ctypes.SEC_OID_SHA1,
sha1(dataToSign).digest(),
wincx )
try:
nss_ctypes.SEC_PKCS7AddSigningTime(p7)
nss_ctypes.SEC_PKCS7IncludeCertChain(p7, wincx)
return nss_ctypes.SEC_PKCS7Encode(p7, None, wincx)
finally:
nss_ctypes.SEC_PKCS7DestroyContentInfo(p7)
def sign_zip(in_zipfile_name, out_zipfile_name, cert, wincx):
mf_entries = []
seen_entries = set()
# Change the limits in JarSignatureVerification.cpp when you change the limits
# here.
max_entry_uncompressed_len = 100 * 1024 * 1024
max_total_uncompressed_len = 500 * 1024 * 1024
max_entry_count = 100 * 1000
max_entry_filename_len = 1024
max_mf_len = max_entry_count * 50
max_sf_len = 1024
total_uncompressed_len = 0
entry_count = 0
with zipfile.ZipFile(out_zipfile_name, 'w') as out_zip:
with zipfile.ZipFile(in_zipfile_name, 'r') as in_zip:
for entry_info in in_zip.infolist():
name = entry_info.filename
# Check for reserved and/or insane (potentially malicious) names
if name.endswith("/"):
pass
# Do nothing; we don't copy directory entries since they are just a
# waste of space.
elif name.lower().startswith("meta-inf/"):
# META-INF/* is reserved for our use
raise ValueError("META-INF entries are not allowed: %s" % (name))
elif len(name) > max_entry_filename_len:
raise ValueError("Entry's filename is too long: %s" % (name))
# TODO: elif name has invalid characters...
elif name in seen_entries:
# It is possible for a zipfile to have duplicate entries (with the exact
# same filenames). Python's zipfile module accepts them, but our zip
# reader in Gecko cannot do anything useful with them, and there's no
# sane reason for duplicate entries to exist, so reject them.
raise ValueError("Duplicate entry in input file: %s" % (name))
else:
entry_count += 1
if entry_count > max_entry_count:
raise ValueError("Too many entries in input archive")
seen_entries.add(name)
# Read in the input entry, but be careful to avoid going over the
# various limits we have, to minimize the likelihood that we'll run
# out of memory. Note that we can't use the length from entry_info
# because that might not be accurate if the input zip file is
# maliciously crafted to contain misleading metadata.
with in_zip.open(name, 'r') as entry_file:
contents = entry_file.read(max_entry_uncompressed_len + 1)
if len(contents) > max_entry_uncompressed_len:
raise ValueError("Entry is too large: %s" % (name))
total_uncompressed_len += len(contents)
if total_uncompressed_len > max_total_uncompressed_len:
raise ValueError("Input archive is too large")
# Copy the entry, using the same compression as used in the input file
out_zip.writestr(entry_info, contents)
# Add the entry to the manifest we're building
mf_entries.append('Name: %s\nSHA1-Digest: %s\n'
% (name, b64encode(sha1(contents).digest())))
mf_contents = 'Manifest-Version: 1.0\n\n' + '\n'.join(mf_entries)
if len(mf_contents) > max_mf_len:
raise ValueError("Generated MANIFEST.MF is too large: %d" % (len(mf_contents)))
sf_contents = ('Signature-Version: 1.0\nSHA1-Digest-Manifest: %s\n'
% (b64encode(sha1(mf_contents).digest())))
if len(sf_contents) > max_sf_len:
raise ValueError("Generated SIGNATURE.SF is too large: %d"
% (len(mf_contents)))
p7 = nss_create_detached_signature(cert, sf_contents, wincx)
# write the signature, SF, and MF
out_zip.writestr("META-INF/A.RSA", p7, zipfile.ZIP_DEFLATED)
out_zip.writestr("META-INF/A.SF", sf_contents, zipfile.ZIP_DEFLATED)
out_zip.writestr("META-INF/MANIFEST.MF", mf_contents, zipfile.ZIP_DEFLATED)
def main():
parser = argparse.ArgumentParser(description='Sign a B2G app.')
parser.add_argument('-d', action='store',
required=True, help='NSS database directory')
parser.add_argument('-f', action='store',
type=argparse.FileType('rb'),
required=True, help='password file')
parser.add_argument('-k', action='store',
required=True, help="nickname of signing cert.")
parser.add_argument('-i', action='store', type=argparse.FileType('rb'),
required=True, help="input JAR file (unsigned)")
parser.add_argument('-o', action='store', type=argparse.FileType('wb'),
required=True, help="output JAR file (signed)")
args = parser.parse_args()
db_dir = args.d
password = args.f.readline().strip()
cert_nickname = args.k
(wincx, cert) = nss_load_cert(db_dir, password, cert_nickname)
try:
sign_zip(args.i, args.o, cert, wincx)
return 0
finally:
nss_ctypes.CERT_DestroyCertificate(cert)
nss_ctypes.NSS_Shutdown()
if __name__ == "__main__":
sys.exit(main())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,6 @@
<!doctype html>
<html lang=en>
<head><meta charset=utf-8><title>Simple App</title></head>
<body><p>This is a Simple App.</body>
</html>

View File

@ -0,0 +1,6 @@
{ name: "Simple App"
, description: "A Simple Open Web App"
, launch_path: "/index.html"
, icons: { "128" : "icon-128.png" }
, installs_allowed_from: [ "https://marketplace.mozilla.com" ]
}

View File

@ -2,6 +2,7 @@
head =
tail =
[test_signed_apps.js]
[test_datasignatureverifier.js]
# Bug 676972: test hangs consistently on Android
skip-if = os == "android"

View File

@ -859,6 +859,19 @@
ERROR(NS_ERROR_DOM_FILEHANDLE_READ_ONLY_ERR, FAILURE(5)),
#undef MODULE
/* ======================================================================= */
/* 35: NS_ERROR_MODULE_SIGNED_JAR */
/* ======================================================================= */
#define MODULE NS_ERROR_MODULE_SIGNED_JAR
ERROR(NS_ERROR_SIGNED_JAR_NOT_SIGNED, FAILURE(1)),
ERROR(NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY, FAILURE(2)),
ERROR(NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY, FAILURE(3)),
ERROR(NS_ERROR_SIGNED_JAR_ENTRY_MISSING, FAILURE(4)),
ERROR(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE, FAILURE(5)),
ERROR(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE, FAILURE(6)),
ERROR(NS_ERROR_SIGNED_JAR_ENTRY_INVALID, FAILURE(7)),
ERROR(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, FAILURE(8)),
#undef MODULE
/* ======================================================================= */
/* 51: NS_ERROR_MODULE_GENERAL */

View File

@ -69,6 +69,7 @@
#define NS_ERROR_MODULE_DOM_FILE 32
#define NS_ERROR_MODULE_DOM_INDEXEDDB 33
#define NS_ERROR_MODULE_DOM_FILEHANDLE 34
#define NS_ERROR_MODULE_SIGNED_JAR 35
/* NS_ERROR_MODULE_GENERAL should be used by modules that do not
* care if return code values overlap. Callers of methods that