mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
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:
parent
8b107fc7c0
commit
8b3350f6d2
@ -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) */
|
/* character converter related codes (from nsIUnicodeDecoder.h) */
|
||||||
XPC_MSG_DEF(NS_ERROR_ILLEGAL_INPUT , "The input characters have illegal sequences")
|
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 nsIArray;
|
||||||
interface nsIX509Cert;
|
interface nsIX509Cert;
|
||||||
|
interface nsIX509Cert3;
|
||||||
interface nsIFile;
|
interface nsIFile;
|
||||||
interface nsIInterfaceRequestor;
|
interface nsIInterfaceRequestor;
|
||||||
|
interface nsIZipReader;
|
||||||
|
|
||||||
%{C++
|
%{C++
|
||||||
#define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1"
|
#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
|
* This represents a service to access and manipulate
|
||||||
* X.509 certificates stored in a database.
|
* X.509 certificates stored in a database.
|
||||||
*/
|
*/
|
||||||
[scriptable, uuid(eb426311-69cd-4a74-a7db-a4a215854c78)]
|
[scriptable, uuid(735d0363-e219-4387-b5c6-72e800c3ea0b)]
|
||||||
interface nsIX509CertDB : nsISupports {
|
interface nsIX509CertDB : nsISupports {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -253,5 +263,33 @@ interface nsIX509CertDB : nsISupports {
|
|||||||
* @return The new certificate object.
|
* @return The new certificate object.
|
||||||
*/
|
*/
|
||||||
nsIX509Cert constructX509FromBase64(in string base64);
|
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);
|
||||||
|
};
|
||||||
|
767
security/manager/ssl/src/JARSignatureVerification.cpp
Normal file
767
security/manager/ssl/src/JARSignatureVerification.cpp
Normal 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");
|
||||||
|
}
|
@ -21,6 +21,7 @@ LIBXUL_LIBRARY = 1
|
|||||||
|
|
||||||
CPPSRCS = \
|
CPPSRCS = \
|
||||||
CryptoTask.cpp \
|
CryptoTask.cpp \
|
||||||
|
JARSignatureVerification.cpp \
|
||||||
nsCERTValInParamWrapper.cpp \
|
nsCERTValInParamWrapper.cpp \
|
||||||
nsNSSCleaner.cpp \
|
nsNSSCleaner.cpp \
|
||||||
nsCertOverrideService.cpp \
|
nsCertOverrideService.cpp \
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "nsNSSComponent.h"
|
#include "nsNSSComponent.h"
|
||||||
#include "nsNSSCertificateDB.h"
|
#include "nsNSSCertificateDB.h"
|
||||||
|
#include "mozilla/Base64.h"
|
||||||
#include "nsCOMPtr.h"
|
#include "nsCOMPtr.h"
|
||||||
#include "nsNSSCertificate.h"
|
#include "nsNSSCertificate.h"
|
||||||
#include "nsNSSHelper.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;
|
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
|
NS_IMETHODIMP
|
||||||
nsNSSCertificateDB::GetCerts(nsIX509CertList **_retval)
|
nsNSSCertificateDB::GetCerts(nsIX509CertList **_retval)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* 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 "nsNSSComponent.h"
|
||||||
#include "nsNSSCallbacks.h"
|
#include "nsNSSCallbacks.h"
|
||||||
#include "nsNSSIOLayer.h"
|
#include "nsNSSIOLayer.h"
|
||||||
@ -84,7 +88,7 @@
|
|||||||
using namespace mozilla;
|
using namespace mozilla;
|
||||||
using namespace mozilla::psm;
|
using namespace mozilla::psm;
|
||||||
|
|
||||||
#ifdef PR_LOGGING
|
#ifdef MOZ_LOGGING
|
||||||
PRLogModuleInfo* gPIPNSSLog = nullptr;
|
PRLogModuleInfo* gPIPNSSLog = nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
254
security/manager/ssl/tests/unit/test_signed_apps.js
Normal file
254
security/manager/ssl/tests/unit/test_signed_apps.js
Normal 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
|
79
security/manager/ssl/tests/unit/test_signed_apps/generate.sh
Normal file
79
security/manager/ssl/tests/unit/test_signed_apps/generate.sh
Normal 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
|
124
security/manager/ssl/tests/unit/test_signed_apps/nss_ctypes.py
Normal file
124
security/manager/ssl/tests/unit/test_signed_apps/nss_ctypes.py
Normal 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)
|
144
security/manager/ssl/tests/unit/test_signed_apps/sign_b2g_app.py
Normal file
144
security/manager/ssl/tests/unit/test_signed_apps/sign_b2g_app.py
Normal 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 |
@ -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" ]
|
||||||
|
}
|
BIN
security/manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der
Normal file
BIN
security/manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der
Normal file
Binary file not shown.
Binary file not shown.
BIN
security/manager/ssl/tests/unit/test_signed_apps/unsigned.zip
Normal file
BIN
security/manager/ssl/tests/unit/test_signed_apps/unsigned.zip
Normal file
Binary file not shown.
Binary file not shown.
BIN
security/manager/ssl/tests/unit/test_signed_apps/valid.zip
Normal file
BIN
security/manager/ssl/tests/unit/test_signed_apps/valid.zip
Normal file
Binary file not shown.
@ -2,6 +2,7 @@
|
|||||||
head =
|
head =
|
||||||
tail =
|
tail =
|
||||||
|
|
||||||
|
[test_signed_apps.js]
|
||||||
[test_datasignatureverifier.js]
|
[test_datasignatureverifier.js]
|
||||||
# Bug 676972: test hangs consistently on Android
|
# Bug 676972: test hangs consistently on Android
|
||||||
skip-if = os == "android"
|
skip-if = os == "android"
|
||||||
|
@ -859,6 +859,19 @@
|
|||||||
ERROR(NS_ERROR_DOM_FILEHANDLE_READ_ONLY_ERR, FAILURE(5)),
|
ERROR(NS_ERROR_DOM_FILEHANDLE_READ_ONLY_ERR, FAILURE(5)),
|
||||||
#undef MODULE
|
#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 */
|
/* 51: NS_ERROR_MODULE_GENERAL */
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
#define NS_ERROR_MODULE_DOM_FILE 32
|
#define NS_ERROR_MODULE_DOM_FILE 32
|
||||||
#define NS_ERROR_MODULE_DOM_INDEXEDDB 33
|
#define NS_ERROR_MODULE_DOM_INDEXEDDB 33
|
||||||
#define NS_ERROR_MODULE_DOM_FILEHANDLE 34
|
#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
|
/* NS_ERROR_MODULE_GENERAL should be used by modules that do not
|
||||||
* care if return code values overlap. Callers of methods that
|
* care if return code values overlap. Callers of methods that
|
||||||
|
Loading…
Reference in New Issue
Block a user