зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
e4a4d92ee8
Коммит
f118bb8b55
|
@ -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.")
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -21,6 +21,7 @@ LIBXUL_LIBRARY = 1
|
|||
|
||||
CPPSRCS = \
|
||||
CryptoTask.cpp \
|
||||
JARSignatureVerification.cpp \
|
||||
nsCERTValInParamWrapper.cpp \
|
||||
nsNSSCleaner.cpp \
|
||||
nsCertOverrideService.cpp \
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include "nsNSSComponent.h"
|
||||
#include "nsNSSCertificateDB.h"
|
||||
#include "mozilla/Base64.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsNSSCertificate.h"
|
||||
#include "nsNSSHelper.h"
|
||||
|
@ -1616,6 +1617,16 @@ NS_IMETHODIMP nsNSSCertificateDB::AddCertFromBase64(const char *aBase64, const c
|
|||
return (srv == SECSuccess) ? NS_OK : NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsNSSCertificateDB::AddCert(const nsACString & aCertDER, const char *aTrust,
|
||||
const char *aName)
|
||||
{
|
||||
nsCString base64;
|
||||
nsresult rv = Base64Encode(aCertDER, base64);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
return AddCertFromBase64(base64.get(), aTrust, aName);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsNSSCertificateDB::GetCerts(nsIX509CertList **_retval)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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())
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.6 KiB |
|
@ -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>
|
||||
|
|
@ -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" ]
|
||||
}
|
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/untrusted/examplla-app-signing-root-ca-1.der
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/untrusted/examplla-app-signing-root-ca-1.der
Normal 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"
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче