mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 896620: Make marketplace certs work on in all products, r=keeler
--HG-- extra : source : 86ec7137a8892f75918c77e605df970f5b96ef62 extra : histedit_source : 33326790804d49e6ec658626116ebf870d94d445
This commit is contained in:
parent
6f0d9dfd0a
commit
83e4eaa908
@ -3086,8 +3086,8 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL,
|
||||
_openSignedPackage: function(aZipFile, aCertDb) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
aCertDb.openSignedJARFileAsync(
|
||||
aZipFile,
|
||||
aCertDb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot, aZipFile,
|
||||
function(aRv, aZipReader) {
|
||||
deferred.resolve([aRv, aZipReader]);
|
||||
}
|
||||
|
@ -10,8 +10,10 @@
|
||||
|
||||
#include "nsNSSCertificateDB.h"
|
||||
|
||||
#include "insanity/pkix.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "CryptoTask.h"
|
||||
#include "AppTrustDomain.h"
|
||||
#include "nsComponentManagerUtils.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsHashKeys.h"
|
||||
@ -26,11 +28,14 @@
|
||||
#include "ScopedNSSTypes.h"
|
||||
|
||||
#include "base64.h"
|
||||
#include "certdb.h"
|
||||
#include "secmime.h"
|
||||
#include "plstr.h"
|
||||
#include "prlog.h"
|
||||
|
||||
using namespace insanity::pkix;
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::psm;
|
||||
|
||||
#ifdef PR_LOGGING
|
||||
extern PRLogModuleInfo* gPIPNSSLog;
|
||||
@ -517,31 +522,109 @@ ParseMF(const char* filebuf, nsIZipReader * zip,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Callback functions for decoder. For now, use empty/default functions.
|
||||
void
|
||||
ContentCallback(void *arg, const char *buf, unsigned long len)
|
||||
nsresult
|
||||
VerifySignature(AppTrustedRoot trustedRoot,
|
||||
const SECItem& buffer, const SECItem& detachedDigest,
|
||||
/*out*/ insanity::pkix::ScopedCERTCertList& builtChain)
|
||||
{
|
||||
}
|
||||
PK11SymKey *
|
||||
GetDecryptKeyCallback(void *, SECAlgorithmID *)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
PRBool
|
||||
DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *bulkkey)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void *
|
||||
GetPasswordKeyCallback(void *arg, void *handle)
|
||||
{
|
||||
return nullptr;
|
||||
insanity::pkix::ScopedPtr<NSSCMSMessage, NSS_CMSMessage_Destroy>
|
||||
cmsMsg(NSS_CMSMessage_CreateFromDER(const_cast<SECItem*>(&buffer), nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr));
|
||||
if (!cmsMsg) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
if (!NSS_CMSMessage_IsSigned(cmsMsg.get())) {
|
||||
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("CMS message isn't signed"));
|
||||
return NS_ERROR_CMS_VERIFY_NOT_SIGNED;
|
||||
}
|
||||
|
||||
NSSCMSContentInfo* cinfo = NSS_CMSMessage_ContentLevel(cmsMsg.get(), 0);
|
||||
if (!cinfo) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// signedData is non-owning
|
||||
NSSCMSSignedData* signedData =
|
||||
reinterpret_cast<NSSCMSSignedData*>(NSS_CMSContentInfo_GetContent(cinfo));
|
||||
if (!signedData) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// Set digest value.
|
||||
if (NSS_CMSSignedData_SetDigestValue(signedData, SEC_OID_SHA1,
|
||||
const_cast<SECItem*>(&detachedDigest))) {
|
||||
return NS_ERROR_CMS_VERIFY_BAD_DIGEST;
|
||||
}
|
||||
|
||||
// Parse the certificates into CERTCertificate objects held in memory, so that
|
||||
// AppTrustDomain will be able to find them during path building.
|
||||
insanity::pkix::ScopedCERTCertList certs(CERT_NewCertList());
|
||||
if (!certs) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (signedData->rawCerts) {
|
||||
for (size_t i = 0; signedData->rawCerts[i]; ++i) {
|
||||
insanity::pkix::ScopedCERTCertificate
|
||||
cert(CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
|
||||
signedData->rawCerts[i], nullptr, false,
|
||||
true));
|
||||
// Skip certificates that fail to parse
|
||||
if (cert) {
|
||||
if (CERT_AddCertToListTail(certs.get(), cert.get()) == SECSuccess) {
|
||||
cert.release(); // ownership transfered
|
||||
} else {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the end-entity certificate.
|
||||
int numSigners = NSS_CMSSignedData_SignerInfoCount(signedData);
|
||||
if (NS_WARN_IF(numSigners != 1)) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
// signer is non-owning.
|
||||
NSSCMSSignerInfo* signer = NSS_CMSSignedData_GetSignerInfo(signedData, 0);
|
||||
if (NS_WARN_IF(!signer)) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
// cert is signerCert
|
||||
CERTCertificate* signerCert =
|
||||
NSS_CMSSignerInfo_GetSigningCertificate(signer, CERT_GetDefaultCertDB());
|
||||
if (!signerCert) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
// Verify certificate.
|
||||
AppTrustDomain trustDomain(nullptr); // TODO: null pinArg
|
||||
if (trustDomain.SetTrustedRoot(trustedRoot) != SECSuccess) {
|
||||
return MapSECStatus(SECFailure);
|
||||
}
|
||||
if (BuildCertChain(trustDomain, signerCert, PR_Now(), MustBeEndEntity,
|
||||
KU_DIGITAL_SIGNATURE, SEC_OID_EXT_KEY_USAGE_CODE_SIGN,
|
||||
builtChain) != SECSuccess) {
|
||||
return MapSECStatus(SECFailure);
|
||||
}
|
||||
|
||||
// See NSS_CMSContentInfo_GetContentTypeOID, which isn't exported from NSS.
|
||||
SECOidData* contentTypeOidData =
|
||||
SECOID_FindOID(&signedData->contentInfo.contentType);
|
||||
if (!contentTypeOidData) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
return MapSECStatus(NSS_CMSSignerInfo_Verify(signer,
|
||||
const_cast<SECItem*>(&detachedDigest),
|
||||
&contentTypeOidData->oid));
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
OpenSignedJARFile(nsIFile * aJarFile,
|
||||
/*out, optional */ nsIZipReader ** aZipReader,
|
||||
/*out, optional */ nsIX509Cert3 ** aSignerCert)
|
||||
OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
||||
/*out, optional */ nsIZipReader** aZipReader,
|
||||
/*out, optional */ nsIX509Cert3** aSignerCert)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aJarFile);
|
||||
|
||||
@ -571,20 +654,6 @@ OpenSignedJARFile(nsIFile * aJarFile,
|
||||
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;
|
||||
@ -595,15 +664,11 @@ OpenSignedJARFile(nsIFile * aJarFile,
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
// Verify that the signature file is a valid signature of the SF file
|
||||
if (!SEC_PKCS7VerifyDetachedSignatureAtTime(p7_info, certUsageObjectSigner,
|
||||
&sfCalculatedDigest.get(),
|
||||
HASH_AlgSHA1, false, PR_Now())) {
|
||||
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);
|
||||
sigBuffer.type = siBuffer;
|
||||
insanity::pkix::ScopedCERTCertList builtChain;
|
||||
rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(),
|
||||
builtChain);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
@ -717,33 +782,32 @@ OpenSignedJARFile(nsIFile * aJarFile,
|
||||
// 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);
|
||||
MOZ_ASSERT(CERT_LIST_HEAD(builtChain));
|
||||
nsCOMPtr<nsIX509Cert3> signerCert =
|
||||
nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert);
|
||||
NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY);
|
||||
|
||||
signerCert.forget(aSignerCert);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
class OpenSignedJARFileTask MOZ_FINAL : public CryptoTask
|
||||
class OpenSignedAppFileTask MOZ_FINAL : public CryptoTask
|
||||
{
|
||||
public:
|
||||
OpenSignedJARFileTask(nsIFile * aJarFile,
|
||||
nsIOpenSignedJARFileCallback * aCallback)
|
||||
: mJarFile(aJarFile)
|
||||
, mCallback(new nsMainThreadPtrHolder<nsIOpenSignedJARFileCallback>(aCallback))
|
||||
OpenSignedAppFileTask(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
||||
nsIOpenSignedAppFileCallback* aCallback)
|
||||
: mTrustedRoot(aTrustedRoot)
|
||||
, mJarFile(aJarFile)
|
||||
, mCallback(new nsMainThreadPtrHolder<nsIOpenSignedAppFileCallback>(aCallback))
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
virtual nsresult CalculateResult() MOZ_OVERRIDE
|
||||
{
|
||||
return OpenSignedJARFile(mJarFile, getter_AddRefs(mZipReader),
|
||||
return OpenSignedAppFile(mTrustedRoot, mJarFile,
|
||||
getter_AddRefs(mZipReader),
|
||||
getter_AddRefs(mSignerCert));
|
||||
}
|
||||
|
||||
@ -753,11 +817,12 @@ private:
|
||||
|
||||
virtual void CallCallback(nsresult rv)
|
||||
{
|
||||
(void) mCallback->OpenSignedJARFileFinished(rv, mZipReader, mSignerCert);
|
||||
(void) mCallback->OpenSignedAppFileFinished(rv, mZipReader, mSignerCert);
|
||||
}
|
||||
|
||||
const AppTrustedRoot mTrustedRoot;
|
||||
const nsCOMPtr<nsIFile> mJarFile;
|
||||
nsMainThreadPtrHandle<nsIOpenSignedJARFileCallback> mCallback;
|
||||
nsMainThreadPtrHandle<nsIOpenSignedAppFileCallback> mCallback;
|
||||
nsCOMPtr<nsIZipReader> mZipReader; // out
|
||||
nsCOMPtr<nsIX509Cert3> mSignerCert; // out
|
||||
};
|
||||
@ -765,12 +830,14 @@ private:
|
||||
} // unnamed namespace
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsNSSCertificateDB::OpenSignedJARFileAsync(
|
||||
nsIFile * aJarFile, nsIOpenSignedJARFileCallback * aCallback)
|
||||
nsNSSCertificateDB::OpenSignedAppFileAsync(
|
||||
AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
||||
nsIOpenSignedAppFileCallback* aCallback)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aJarFile);
|
||||
NS_ENSURE_ARG_POINTER(aCallback);
|
||||
RefPtr<OpenSignedJARFileTask> task(new OpenSignedJARFileTask(aJarFile,
|
||||
RefPtr<OpenSignedAppFileTask> task(new OpenSignedAppFileTask(aTrustedRoot,
|
||||
aJarFile,
|
||||
aCallback));
|
||||
return task->Dispatch("SignedJAR");
|
||||
}
|
181
security/apps/AppTrustDomain.cpp
Normal file
181
security/apps/AppTrustDomain.cpp
Normal file
@ -0,0 +1,181 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifdef MOZ_LOGGING
|
||||
#define FORCE_PR_LOG 1
|
||||
#endif
|
||||
|
||||
#include "AppTrustDomain.h"
|
||||
#include "certdb.h"
|
||||
#include "insanity/pkix.h"
|
||||
#include "mozilla/ArrayUtils.h"
|
||||
#include "nsIX509CertDB.h"
|
||||
#include "prerror.h"
|
||||
#include "secerr.h"
|
||||
|
||||
// Generated in Makefile.in
|
||||
#include "marketplace-prod-public.inc"
|
||||
#include "marketplace-prod-reviewers.inc"
|
||||
#include "marketplace-dev-public.inc"
|
||||
#include "marketplace-dev-reviewers.inc"
|
||||
#include "xpcshell.inc"
|
||||
|
||||
using namespace insanity::pkix;
|
||||
|
||||
#ifdef PR_LOGGING
|
||||
extern PRLogModuleInfo* gPIPNSSLog;
|
||||
#endif
|
||||
|
||||
namespace mozilla { namespace psm {
|
||||
|
||||
AppTrustDomain::AppTrustDomain(void* pinArg)
|
||||
: mPinArg(pinArg)
|
||||
{
|
||||
}
|
||||
|
||||
SECStatus
|
||||
AppTrustDomain::SetTrustedRoot(AppTrustedRoot trustedRoot)
|
||||
{
|
||||
SECItem trustedDER;
|
||||
|
||||
// Load the trusted certificate into the in-memory NSS database so that
|
||||
// CERT_CreateSubjectCertList can find it.
|
||||
|
||||
switch (trustedRoot)
|
||||
{
|
||||
case nsIX509CertDB::AppMarketplaceProdPublicRoot:
|
||||
trustedDER.data = const_cast<uint8_t*>(marketplaceProdPublicRoot);
|
||||
trustedDER.len = mozilla::ArrayLength(marketplaceProdPublicRoot);
|
||||
break;
|
||||
|
||||
case nsIX509CertDB::AppMarketplaceProdReviewersRoot:
|
||||
trustedDER.data = const_cast<uint8_t*>(marketplaceProdReviewersRoot);
|
||||
trustedDER.len = mozilla::ArrayLength(marketplaceProdReviewersRoot);
|
||||
break;
|
||||
|
||||
case nsIX509CertDB::AppMarketplaceDevPublicRoot:
|
||||
trustedDER.data = const_cast<uint8_t*>(marketplaceDevPublicRoot);
|
||||
trustedDER.len = mozilla::ArrayLength(marketplaceDevPublicRoot);
|
||||
break;
|
||||
|
||||
case nsIX509CertDB::AppMarketplaceDevReviewersRoot:
|
||||
trustedDER.data = const_cast<uint8_t*>(marketplaceDevReviewersRoot);
|
||||
trustedDER.len = mozilla::ArrayLength(marketplaceDevReviewersRoot);
|
||||
break;
|
||||
|
||||
case nsIX509CertDB::AppXPCShellRoot:
|
||||
trustedDER.data = const_cast<uint8_t*>(xpcshellRoot);
|
||||
trustedDER.len = mozilla::ArrayLength(xpcshellRoot);
|
||||
break;
|
||||
|
||||
default:
|
||||
PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
|
||||
return SECFailure;
|
||||
}
|
||||
|
||||
mTrustedRoot = CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
|
||||
&trustedDER, nullptr, false, true);
|
||||
if (!mTrustedRoot) {
|
||||
return SECFailure;
|
||||
}
|
||||
|
||||
return SECSuccess;
|
||||
}
|
||||
|
||||
SECStatus
|
||||
AppTrustDomain::FindPotentialIssuers(const SECItem* encodedIssuerName,
|
||||
PRTime time,
|
||||
/*out*/ insanity::pkix::ScopedCERTCertList& results)
|
||||
{
|
||||
MOZ_ASSERT(mTrustedRoot);
|
||||
if (!mTrustedRoot) {
|
||||
PR_SetError(PR_INVALID_STATE_ERROR, 0);
|
||||
return SECFailure;
|
||||
}
|
||||
|
||||
results = CERT_CreateSubjectCertList(nullptr, CERT_GetDefaultCertDB(),
|
||||
encodedIssuerName, time, true);
|
||||
if (!results) {
|
||||
// NSS sometimes returns this unhelpful error code upon failing to find any
|
||||
// candidate certificates.
|
||||
if (PR_GetError() == SEC_ERROR_BAD_DATABASE) {
|
||||
PR_SetError(SEC_ERROR_UNKNOWN_ISSUER, 0);
|
||||
}
|
||||
return SECFailure;
|
||||
}
|
||||
|
||||
return SECSuccess;
|
||||
}
|
||||
|
||||
SECStatus
|
||||
AppTrustDomain::GetCertTrust(EndEntityOrCA endEntityOrCA,
|
||||
const CERTCertificate* candidateCert,
|
||||
/*out*/ TrustLevel* trustLevel)
|
||||
{
|
||||
MOZ_ASSERT(candidateCert);
|
||||
MOZ_ASSERT(trustLevel);
|
||||
MOZ_ASSERT(mTrustedRoot);
|
||||
if (!candidateCert || !trustLevel) {
|
||||
PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
|
||||
return SECFailure;
|
||||
}
|
||||
if (!mTrustedRoot) {
|
||||
PR_SetError(PR_INVALID_STATE_ERROR, 0);
|
||||
return SECFailure;
|
||||
}
|
||||
|
||||
// Handle active distrust of the certificate.
|
||||
CERTCertTrust trust;
|
||||
if (CERT_GetCertTrust(candidateCert, &trust) == SECSuccess) {
|
||||
PRUint32 flags = SEC_GET_TRUST_FLAGS(&trust, trustObjectSigning);
|
||||
|
||||
// For DISTRUST, we use the CERTDB_TRUSTED or CERTDB_TRUSTED_CA bit,
|
||||
// because we can have active distrust for either type of cert. Note that
|
||||
// CERTDB_TERMINAL_RECORD means "stop trying to inherit trust" so if the
|
||||
// relevant trust bit isn't set then that means the cert must be considered
|
||||
// distrusted.
|
||||
PRUint32 relevantTrustBit = endEntityOrCA == MustBeCA
|
||||
? CERTDB_TRUSTED_CA
|
||||
: CERTDB_TRUSTED;
|
||||
if (((flags & (relevantTrustBit | CERTDB_TERMINAL_RECORD)))
|
||||
== CERTDB_TERMINAL_RECORD) {
|
||||
*trustLevel = ActivelyDistrusted;
|
||||
return SECSuccess;
|
||||
}
|
||||
|
||||
#ifdef MOZ_B2G_CERTDATA
|
||||
// XXX(Bug 972201): We have to allow the old way of supporting additional
|
||||
// roots until we fix bug 889744. Remove this along with the rest of the
|
||||
// MOZ_B2G_CERTDATA stuff.
|
||||
|
||||
// For TRUST, we only use the CERTDB_TRUSTED_CA bit, because Gecko hasn't
|
||||
// needed to consider end-entity certs to be their own trust anchors since
|
||||
// Gecko implemented nsICertOverrideService.
|
||||
if (flags & CERTDB_TRUSTED_CA) {
|
||||
*trustLevel = TrustAnchor;
|
||||
return SECSuccess;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// mTrustedRoot is the only trust anchor for this validation.
|
||||
if (CERT_CompareCerts(mTrustedRoot.get(), candidateCert)) {
|
||||
*trustLevel = TrustAnchor;
|
||||
return SECSuccess;
|
||||
}
|
||||
|
||||
*trustLevel = InheritsTrust;
|
||||
return SECSuccess;
|
||||
}
|
||||
|
||||
SECStatus
|
||||
AppTrustDomain::VerifySignedData(const CERTSignedData* signedData,
|
||||
const CERTCertificate* cert)
|
||||
{
|
||||
return ::insanity::pkix::VerifySignedData(signedData, cert, mPinArg);
|
||||
}
|
||||
|
||||
} }
|
40
security/apps/AppTrustDomain.h
Normal file
40
security/apps/AppTrustDomain.h
Normal file
@ -0,0 +1,40 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_psm_AppsTrustDomain_h
|
||||
#define mozilla_psm_AppsTrustDomain_h
|
||||
|
||||
#include "insanity/pkixtypes.h"
|
||||
#include "nsDebug.h"
|
||||
#include "nsIX509CertDB.h"
|
||||
|
||||
namespace mozilla { namespace psm {
|
||||
|
||||
class AppTrustDomain MOZ_FINAL : public insanity::pkix::TrustDomain
|
||||
{
|
||||
public:
|
||||
AppTrustDomain(void* pinArg);
|
||||
|
||||
SECStatus SetTrustedRoot(AppTrustedRoot trustedRoot);
|
||||
|
||||
SECStatus GetCertTrust(insanity::pkix::EndEntityOrCA endEntityOrCA,
|
||||
const CERTCertificate* candidateCert,
|
||||
/*out*/ TrustLevel* trustLevel) MOZ_OVERRIDE;
|
||||
SECStatus FindPotentialIssuers(const SECItem* encodedIssuerName,
|
||||
PRTime time,
|
||||
/*out*/ insanity::pkix::ScopedCERTCertList& results)
|
||||
MOZ_OVERRIDE;
|
||||
SECStatus VerifySignedData(const CERTSignedData* signedData,
|
||||
const CERTCertificate* cert) MOZ_OVERRIDE;
|
||||
|
||||
private:
|
||||
void* mPinArg; // non-owning!
|
||||
insanity::pkix::ScopedCERTCertificate mTrustedRoot;
|
||||
};
|
||||
|
||||
} } // namespace mozilla::psm
|
||||
|
||||
#endif // mozilla_psm_AppsTrustDomain_h
|
29
security/apps/Makefile.in
Normal file
29
security/apps/Makefile.in
Normal file
@ -0,0 +1,29 @@
|
||||
#
|
||||
# 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/.
|
||||
|
||||
GEN_CERT_HEADER = $(srcdir)/gen_cert_header.py
|
||||
|
||||
marketplace-prod-public.inc: marketplace-prod-public.crt $(GEN_CERT_HEADER)
|
||||
$(PYTHON) $(GEN_CERT_HEADER) marketplaceProdPublicRoot $< > $@
|
||||
|
||||
marketplace-prod-reviewers.inc: marketplace-prod-reviewers.crt $(GEN_CERT_HEADER)
|
||||
$(PYTHON) $(GEN_CERT_HEADER) marketplaceProdReviewersRoot $< > $@
|
||||
|
||||
marketplace-dev-public.inc: marketplace-dev-public.crt $(GEN_CERT_HEADER)
|
||||
$(PYTHON) $(GEN_CERT_HEADER) marketplaceDevPublicRoot $< > $@
|
||||
|
||||
marketplace-dev-reviewers.inc: marketplace-dev-reviewers.crt $(GEN_CERT_HEADER)
|
||||
$(PYTHON) $(GEN_CERT_HEADER) marketplaceDevReviewersRoot $< > $@
|
||||
|
||||
xpcshell.inc: $(srcdir)/../manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der $(GEN_CERT_HEADER)
|
||||
$(PYTHON) $(GEN_CERT_HEADER) xpcshellRoot $< > $@
|
||||
|
||||
export:: \
|
||||
marketplace-prod-public.inc \
|
||||
marketplace-prod-reviewers.inc \
|
||||
marketplace-dev-public.inc \
|
||||
marketplace-dev-reviewers.inc \
|
||||
xpcshell.inc \
|
||||
$(NULL)
|
29
security/apps/gen_cert_header.py
Normal file
29
security/apps/gen_cert_header.py
Normal file
@ -0,0 +1,29 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
|
||||
def file_byte_generator(filename, block_size = 512):
|
||||
with open(filename, "rb") as f:
|
||||
while True:
|
||||
block = f.read(block_size)
|
||||
if block:
|
||||
for byte in block:
|
||||
yield byte
|
||||
else:
|
||||
break
|
||||
|
||||
def create_header(array_name, in_filename):
|
||||
hexified = ["0x" + binascii.hexlify(byte) for byte in file_byte_generator(in_filename)]
|
||||
print "const uint8_t " + array_name + "[] = {"
|
||||
print ", ".join(hexified)
|
||||
print "};"
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 3:
|
||||
print 'ERROR: usage: gen_cert_header.py array_name in_filename'
|
||||
sys.exit(1);
|
||||
sys.exit(create_header(sys.argv[1], sys.argv[2]))
|
BIN
security/apps/marketplace-dev-public.crt
Normal file
BIN
security/apps/marketplace-dev-public.crt
Normal file
Binary file not shown.
BIN
security/apps/marketplace-dev-reviewers.crt
Normal file
BIN
security/apps/marketplace-dev-reviewers.crt
Normal file
Binary file not shown.
BIN
security/apps/marketplace-prod-reviewers.crt
Normal file
BIN
security/apps/marketplace-prod-reviewers.crt
Normal file
Binary file not shown.
25
security/apps/moz.build
Normal file
25
security/apps/moz.build
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# These cannot be built in unified mode because they force NSPR logging.
|
||||
SOURCES += [
|
||||
'AppSignatureVerification.cpp',
|
||||
'AppTrustDomain.cpp',
|
||||
]
|
||||
|
||||
FAIL_ON_WARNINGS = True
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
'../certverifier',
|
||||
'../insanity/include',
|
||||
'../manager/ssl/src',
|
||||
]
|
||||
|
||||
DEFINES['NSS_ENABLE_ECC'] = 'True'
|
||||
for var in ('DLL_PREFIX', 'DLL_SUFFIX'):
|
||||
DEFINES[var] = '"%s"' % CONFIG[var]
|
@ -59,6 +59,11 @@ NSSCertDBTrustDomain::FindPotentialIssuers(
|
||||
results = CERT_CreateSubjectCertList(nullptr, CERT_GetDefaultCertDB(),
|
||||
encodedIssuerName, time, true);
|
||||
if (!results) {
|
||||
// NSS sometimes returns this unhelpful error code upon failing to find any
|
||||
// candidate certificates.
|
||||
if (PR_GetError() == SEC_ERROR_BAD_DATABASE) {
|
||||
PR_SetError(SEC_ERROR_UNKNOWN_ISSUER, 0);
|
||||
}
|
||||
return SECFailure;
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,12 @@ interface nsIX509CertList;
|
||||
#define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1"
|
||||
%}
|
||||
|
||||
[scriptable, function, uuid(25a048e8-bb1c-4c33-ad3a-eacf2ad9e9ee)]
|
||||
interface nsIOpenSignedJARFileCallback : nsISupports
|
||||
typedef uint32_t AppTrustedRoot;
|
||||
|
||||
[scriptable, function, uuid(e12aec59-7153-4e84-9376-2e851311b7a3)]
|
||||
interface nsIOpenSignedAppFileCallback : nsISupports
|
||||
{
|
||||
void openSignedJARFileFinished(in nsresult rv,
|
||||
void openSignedAppFileFinished(in nsresult rv,
|
||||
in nsIZipReader aZipReader,
|
||||
in nsIX509Cert3 aSignerCert);
|
||||
};
|
||||
@ -31,7 +33,7 @@ interface nsIOpenSignedJARFileCallback : nsISupports
|
||||
* This represents a service to access and manipulate
|
||||
* X.509 certificates stored in a database.
|
||||
*/
|
||||
[scriptable, uuid(38463592-8527-11e3-b240-180373d97f23)]
|
||||
[scriptable, uuid(398dfa21-f29d-4530-bb42-136c6e7d9486)]
|
||||
interface nsIX509CertDB : nsISupports {
|
||||
|
||||
/**
|
||||
@ -297,8 +299,14 @@ interface nsIX509CertDB : nsISupports {
|
||||
* 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);
|
||||
const AppTrustedRoot AppMarketplaceProdPublicRoot = 1;
|
||||
const AppTrustedRoot AppMarketplaceProdReviewersRoot = 2;
|
||||
const AppTrustedRoot AppMarketplaceDevPublicRoot = 3;
|
||||
const AppTrustedRoot AppMarketplaceDevReviewersRoot = 4;
|
||||
const AppTrustedRoot AppXPCShellRoot = 5;
|
||||
void openSignedAppFileAsync(in AppTrustedRoot trustedRoot,
|
||||
in nsIFile aJarFile,
|
||||
in nsIOpenSignedAppFileCallback callback);
|
||||
|
||||
/*
|
||||
* Add a cert to a cert DB from a binary string.
|
||||
|
@ -68,7 +68,6 @@ UNIFIED_SOURCES += [
|
||||
# The rest cannot be built in unified mode because they want to force NSPR
|
||||
# logging.
|
||||
SOURCES += [
|
||||
'JARSignatureVerification.cpp',
|
||||
'nsCryptoHash.cpp',
|
||||
'nsNSSCertificateDB.cpp',
|
||||
'nsNSSComponent.cpp',
|
||||
|
@ -70,6 +70,7 @@
|
||||
#include "certdb.h"
|
||||
#include "secmod.h"
|
||||
#include "ScopedNSSTypes.h"
|
||||
#include "insanity/pkixtypes.h"
|
||||
|
||||
#include "ssl.h" // For SSL_ClearSessionCache
|
||||
|
||||
@ -2384,9 +2385,10 @@ nsCrypto::ImportUserCertificates(const nsAString& aNickname,
|
||||
|
||||
//Import the root chain into the cert db.
|
||||
{
|
||||
ScopedCERTCertList caPubs(CMMF_CertRepContentGetCAPubs(certRepContent));
|
||||
insanity::pkix::ScopedCERTCertList
|
||||
caPubs(CMMF_CertRepContentGetCAPubs(certRepContent));
|
||||
if (caPubs) {
|
||||
int32_t numCAs = nsCertListCount(caPubs);
|
||||
int32_t numCAs = nsCertListCount(caPubs.get());
|
||||
|
||||
NS_ASSERTION(numCAs > 0, "Invalid number of CA's");
|
||||
if (numCAs > 0) {
|
||||
|
@ -22,6 +22,7 @@ const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
|
||||
|
||||
// Sort in numerical order
|
||||
const SEC_ERROR_REVOKED_CERTIFICATE = SEC_ERROR_BASE + 12;
|
||||
const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13;
|
||||
const SEC_ERROR_BAD_DATABASE = SEC_ERROR_BASE + 18;
|
||||
const SEC_ERROR_UNTRUSTED_ISSUER = SEC_ERROR_BASE + 20;
|
||||
const SEC_ERROR_EXTENSION_NOT_FOUND = SEC_ERROR_BASE + 35;
|
||||
|
@ -10,28 +10,9 @@ function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// 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) );
|
||||
|
||||
function check_open_result(name, expectedRv) {
|
||||
if (expectedRv == Cr.NS_OK && !isB2G) {
|
||||
// We do not trust the marketplace trust anchor on non-B2G builds
|
||||
expectedRv = NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER;
|
||||
}
|
||||
|
||||
return function openSignedJARFileCallback(rv, aZipReader, aSignerCert) {
|
||||
do_print("openSignedJARFileCallback called for " + name);
|
||||
return function openSignedAppFileCallback(rv, aZipReader, aSignerCert) {
|
||||
do_print("openSignedAppFileCallback called for " + name);
|
||||
do_check_eq(rv, expectedRv);
|
||||
do_check_eq(aZipReader != null, Components.isSuccessCode(expectedRv));
|
||||
do_check_eq(aSignerCert != null, Components.isSuccessCode(expectedRv));
|
||||
@ -46,15 +27,17 @@ function original_app_path(test_name) {
|
||||
// Test that we no longer trust the test root cert that was originally used
|
||||
// during development of B2G 1.0.
|
||||
add_test(function () {
|
||||
certdb.openSignedJARFileAsync(
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot,
|
||||
original_app_path("test-privileged-app-test-1.0"),
|
||||
check_open_result("test-privileged-app-test-1.0",
|
||||
NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER));
|
||||
getXPCOMStatusFromNSS(SEC_ERROR_UNKNOWN_ISSUER)));
|
||||
});
|
||||
|
||||
// Test that we trust the root cert used by by the Firefox Marketplace.
|
||||
add_test(function () {
|
||||
certdb.openSignedJARFileAsync(
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot,
|
||||
original_app_path("privileged-app-test-1.0"),
|
||||
check_open_result("privileged-app-test-1.0", Cr.NS_OK));
|
||||
});
|
||||
|
@ -111,16 +111,12 @@ function truncateEntry(entry, entryInput) {
|
||||
}
|
||||
|
||||
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);
|
||||
return function openSignedAppFileCallback(rv, aZipReader, aSignerCert) {
|
||||
do_print("openSignedAppFileCallback called for " + name);
|
||||
do_check_eq(rv, expectedRv);
|
||||
do_check_eq(aZipReader != null, Components.isSuccessCode(expectedRv));
|
||||
do_check_eq(aSignerCert != null, Components.isSuccessCode(expectedRv));
|
||||
@ -137,60 +133,54 @@ function tampered_app_path(test_name) {
|
||||
}
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedJARFileAsync(original_app_path("valid"),
|
||||
check_open_result("valid", Cr.NS_OK));
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, 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));
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, 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"),
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, original_app_path("unknown_issuer"),
|
||||
check_open_result("unknown_issuer",
|
||||
/*Cr.*/NS_ERROR_SEC_ERROR_UNKNOWN_ISSUER));
|
||||
getXPCOMStatusFromNSS(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"),
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, 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,
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, 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,
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, 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,
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("missing_manifest_mf",
|
||||
Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
|
||||
});
|
||||
@ -198,14 +188,16 @@ add_test(function () {
|
||||
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));
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, 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,
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("truncated_entry", Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY));
|
||||
});
|
||||
|
||||
@ -213,7 +205,8 @@ 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,
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("unsigned_entry", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY));
|
||||
});
|
||||
|
||||
@ -221,7 +214,8 @@ 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,
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("unsigned_metainf_entry",
|
||||
Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY));
|
||||
});
|
||||
|
@ -13,6 +13,9 @@ if CONFIG['MOZ_CONTENT_SANDBOX']:
|
||||
# builds fail.
|
||||
add_tier_dir('platform', 'security/certverifier')
|
||||
|
||||
# Depends on certverifier
|
||||
add_tier_dir('platform', 'security/apps')
|
||||
|
||||
# the signing related bits of libmar depend on nss
|
||||
if CONFIG['MOZ_UPDATER']:
|
||||
add_tier_dir('platform', 'modules/libmar')
|
||||
|
Loading…
Reference in New Issue
Block a user